Engineering Journal
Schema Editor
Schema Editor

When an Open Panel Stays Offscreen: A CSS Specificity Trap

2026-08-31

When an Open Panel Stays Offscreen: A CSS Specificity Trap

The primary reader is any frontend developer layering a new theme over an existing UI. The bug class is a state class that updates correctly while the visual state does not.

The bug

A right inspector used a familiar pattern:

.side-panel { right: -286px; }
.side-panel.open { right: 0; }

JavaScript added open, but the panel remained outside the viewport. DOM inspection showed the class was present, so the event handler looked correct.

The redesign had also introduced this theme rule:

body.dark-mode .side-panel { right: -286px; }

body.dark-mode .side-panel has greater specificity than .side-panel.open. Source order could not rescue the state selector. The closed theme position continued to win.

The fix

Express the state at the same theme scope:

.side-panel.open,
body.dark-mode .side-panel.open {
  right: 0;
}

The same correction must appear inside responsive rules if those rules redefine the closed position.

An alternative is to lower theme specificity with :where():

:where(body.dark-mode) .side-panel {
  background: #0c1d29;
}

Selectors inside :where() contribute no specificity, so ordinary component states remain easy to override. That is useful in a new system, but changing the specificity model of a mature stylesheet can have broad effects. Matching the existing theme scope was the safer targeted repair.

How to prevent it

Keep base position rules as theme-neutral as possible. Themes should usually change color, not state geometry. When a theme must alter geometry, pair every state selector explicitly and test computed bounds rather than checking class names alone.

button.click()
const box = panel.getBoundingClientRect()
assert(box.right === innerWidth)

Repeat the geometry test across media-query boundaries. Responsive rules commonly redefine right, transform, or width, quietly recreating a desktop bug on mobile. A class-name assertion cannot detect any of those failures.

The general lesson is that state is behavior expressed through CSS. Testing only the DOM state proves half the feature; computed geometry proves that the user can actually see it. When a click appears to do nothing, inspect the class, the computed property, and the bounding rectangle in that order. Those three observations quickly separate an event failure from a cascade failure and a transform or clipping failure. It is a small debugging routine, but it prevents unnecessary rewrites of JavaScript that was already behaving correctly.

Read this post in the full Engineering Journal →