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 ↗