CSS Stack Overflow Answers

Selected technical solutions from my Stack Overflow history. These cover dialogs, animation, selectors, and layout — not a vote leaderboard.

Problem

dialog { opacity: 0; transition } plus dialog[open] { opacity: 1 } snapped instead of fading. showModal() flipped display.

Solution

Keep closed dialogs as display: none (they must not stay in the tab order). Animate dialog[open] with @keyframes instead of transition. Forcing display: block on a closed dialog looks like a fade but still receives Tab.

Why it works

transition does not interpolate display: none → block. animation on [open] starts after the element is shown.

Code

dialog[open] {
    animation: fadein 2s ease-in forwards;
}
@keyframes fadein {
    0% { opacity: 0; }
    100% { opacity: 1; background-color: green; }
}
15 score · Accepted · 10K question views
View on Stack Overflow ↗

Problem

nth-child(n) { transform: translateX(calc(n * 10%)) } is invalid. n is not a CSS value.

Solution

Stamp --child-index on :nth-child(1), :nth-child(2), … (Sass can generate the list). Then transform: translateX(calc(var(--child-index) * 10%)).

Why it works

The An+B microsyntax does not leak n into calc(). A custom property is a real number the rest of the rule can use.

Code

:nth-child(1) { --child-index: 1; }
:nth-child(2) { --child-index: 2; }
li { transform: translateX(calc(var(--child-index) * 10%)); }
5 score · Accepted · 475 question views
View on Stack Overflow ↗

Problem

An old <dialog> fade used visibility/opacity transitions. Setting display on a closed dialog left it focusable.

Solution

Same as the modal case: do not un-none a closed dialog. Use an open-state animation. A snippet showed Tab reaching a “hidden” dialog when display was forced.

Why it works

display: none is what removes the dialog from the accessibility tree and tab order. Opacity 0 is only paint.

Code

dialog[open] {
    animation: fadein 2s ease-in forwards;
}
4 score
View on Stack Overflow ↗

Problem

Buttons sat outside an invisible hover-area. mouseleave on the pad hid them before they could be clicked.

Solution

Put the button group inside .hover-target, flush to the box (padding-top for a visual gap). .hover-target:hover .button-group { display: block }. cursor: pointer on the buttons.

Why it works

:hover stays on while the pointer is over descendants. A separate overlay that does not contain the buttons fires mouseleave when you move toward them.

Code

.hover-target:hover .button-group {
    display: block;
}
.button-group button {
    cursor: pointer;
}
3 score · Accepted
View on Stack Overflow ↗

Problem

Nested CSS in source became flattened in the Vite build.

Solution

Vite’s Vue SFC pipeline pulls in PostCSS, which flattens nesting. Nested CSS in the browser was still weak then; flattened output is expected.

Why it works

compiler-sfc depends on postcss. Nesting is compiled unless you change that pipeline.

3 score · Accepted
View on Stack Overflow ↗

Problem

A label’s text changed but width did not animate. display was not a length to transition.

Solution

inline-block, explicit width in px, overflow: hidden, box-sizing: border-box, nowrap. Measure the next title as inline, restore the old text, then set width to the measured value. Inner span with margin: 0 -100% keeps text centered while the box moves.

Why it works

transition: width only runs between two used pixel widths. Auto shrink-to-fit is not a interpolable length.

Code

change.style.display = 'inline';
change.innerText = title[i];
const widthTo = change.offsetWidth;
change.style.display = 'inline-block';
change.style.width = widthTo + 'px';
2 score · Accepted
View on Stack Overflow ↗

Problem

Draggable/resizable widgets could leave the purple parent. The drag origin also had to stick until the pointer came back inside.

Solution

Clamp left/top: x < 0 → 0, x + width > container → container - width (same for y). Raise z-index on mousedown from the current max among siblings.

Why it works

left/top are offsets in the positioned containing block. Clamping against offsetWidth keeps the box inside that block.

Code

if (x < 0) x = 0;
else if (x + draggable.offsetWidth > container.offsetWidth) {
    x = container.offsetWidth - draggable.offsetWidth;
}
2 score · Accepted · 516 question views
View on Stack Overflow ↗

Problem

setInterval plus getElementsByClassName('Bad').style failed. HTMLCollection has no style, and randomizing two colors is not a 50/50 blink.

Solution

CSS animation on the class: 0–49.9% white, 50–99.9% red, 300ms infinite.

Why it works

The animation thread does not depend on a live HTMLCollection. Keyframe ranges give a hard blink without JS.

Code

.bad {
    animation: blink 300ms infinite;
}
@keyframes blink {
    0%, 49.9% { background: white; }
    50%, 99.9% { background: red; }
}
1 score · Accepted · 1.9K question views
View on Stack Overflow ↗

Problem

Light-dismiss closed the popover before custom JS could take over.

Solution

Treat the popover element as a full-size transparent backdrop wrapping the real .popover box. Click on the box stays; click on the backdrop hidePopover() and optionally retarget elementFromPoint.

Why it works

You own the dismiss hit-testing. The platform light-dismiss is no longer the only closer.

Code

popover.addEventListener('click', e =>
    e.target.closest('.popover') || popover.hidePopover()
);
1 score · Accepted · 459 question views
View on Stack Overflow ↗

Problem

The text node beside a checkbox inside a label needed a highlight without wrapping it in a span.

Solution

position: relative on the label, ::before absolutely behind the text (left: 22px to clear the control). :has(:checked) restyles the pseudo and the label.

Why it works

You cannot select a text node. A pseudo on the label is a box you can paint. :has() keys off the input.

Code

.myLabel::before {
    z-index: -1;
    content: '';
    position: absolute;
    left: 22px;
    right: -2px;
    top: 0;
    bottom: 0;
}
.myLabel:has(:checked)::before {
    background: lime;
}
1 score · Accepted
View on Stack Overflow ↗

Problem

A circle around a square used animation-direction: alternate and a 100% keyframe at the last corner, so the loop did not close.

Solution

Drop alternate. Duplicate the start at 0% and 100%. Put the last corner at 75% so 100% can return home.

Why it works

alternate plays the timeline backwards, which is not a circuit. A loop needs the last keyframe equal to the first.

Code

@keyframes slideMe {
    0%, 100% { top: 0; left: 0; }
    25% { top: 0; left: 85%; }
    50% { top: 85%; left: 85%; }
    75% { top: 85%; left: 0; }
}
1 score · Accepted
View on Stack Overflow ↗

Problem

display:flex on td centered the checkbox and destroyed table layout.

Solution

Leave td as table-cell. position: relative on the cell, absolute the input at 50%/50% with negative half-size margins, or wrap the input in a flex filler with inset: 0.

Why it works

display on a table cell is not “flex the contents”; it changes the box into a flex item and drops row/column geometry.

Code

.checkbox-cell { position: relative; }
.checkbox-cell input {
    position: absolute;
    top: 50%;
    left: 50%;
    margin: -6px 0 0 -6px;
}
1 score · Accepted
View on Stack Overflow ↗

Problem

Bootstrap row classes fought a flex “pill” so cells stacked and the vertical rule missed.

Solution

Drop the Bootstrap classes on that row. Flex the bordered container; children align-items: center; first-child { border-right }.

Why it works

Two layout systems on one node split the row. One flex row plus a border on the first child is the divider.

Code

.bordered-element { display: flex; }
.bordered-element > :first-child {
    border-right: 1px solid black;
}
1 score · Accepted · 29 question views
View on Stack Overflow ↗

Problem

Hovering a later menu item needed to hide earlier siblings, CSS only.

Solution

.dropDownMenus:has(~ .levelFive:hover) { display: none }. The subject is any menu that has a following .levelFive:hover sibling.

Why it works

:has() can look forward along ~. Preceding siblings cannot be selected with +/~ alone.

Code

.dropDownMenus:has(~ .levelFive:hover) {
    display: none;
}
1 score
View on Stack Overflow ↗

Problem

getComputedStyle returns used px. The author wanted the specified formula (e.g. calc / %).

Solution

Walk document.styleSheets → cssRules, keep rules where elem.matches(selectorText) and style[name] is set, sort by specificity, return the first specified value.

Why it works

The CSSOM specified value is still on the rule. Computed style is after cascade + layout.

Code

for (const { cssRules } of document.styleSheets) {
    for (const { style, selectorText } of cssRules) {
        const value = style?.[name];
        value && elem.matches(selectorText) && found.push([selectorText, value]);
    }
}
1 score
View on Stack Overflow ↗
View All Answers ↗