CSS Features Worth Another Look
CSS keeps adding capable features, yet some of the most useful ones rarely get the attention they deserve. Some are newer additions that arrived with little fanfare; others have been around for years but are consistently overlooked in favor of JavaScript workarounds or preprocessor logic. The following properties and selectors are worth revisiting — they can simplify stylesheets, remove dependencies, and make front-end code more maintainable.
Resetting Everything With all
The all shorthand property resets every CSS property on an element to a single value. It’s particularly handy when you need to stop inheritance entirely or force inheritance across the board. The property accepts the same keywords you’d use elsewhere: initial sets everything back to its initial value, inherit forces inherited values, unset applies whichever of those two is the default for each property, and revert rolls values back based on the stylesheet origin. The newer revert-layer value moves the cascade to a previous layer or the next matching rule.
See the Pen [all property](https://codepen.io/smashingmag/pen/NWyRvZL) by Adrian Bece.
This makes all a solid tool for resetting styles during a refactor, preventing unwanted style leakage. With revert-layer, you can deliberately skip the closest source of cascade and pull inherited styles from another selector without bringing in the immediately surrounding rules.
h2 {
color: var(--color-primary);
font-size: var(--font-size-large);
line-height: 1.5;
text-decoration: underline;
margin-bottom: 2rem;
}
.article h2 {
padding: 2em;
border-bottom: 2px solid currentColor;
}
.article__title {
/* We don't want styles from previous selector. We only need a margin and a font size. */
all: unset;
margin-bottom: 2rem;
font-size: var(--font-size-medium);
}
One subtle behavior emerges when using all on elements with text decoration: the underline color may not update to the current color value unless text-decoration: underline is applied again on the same selector alongside the all property.
See the Pen [all property - revert-layer](https://codepen.io/smashingmag/pen/bGLwoGx) by Adrian Bece.
The First CSS Variable: currentColor
Long before custom properties existed, currentColor provided a way to reuse an element’s color value in other color-accepting properties like border-color, background, or box-shadow. It effectively forces those properties to inherit the color value you’ve already set, eliminating duplicate declarations within a single rule.
See the Pen [curentColor alerts](https://codepen.io/smashingmag/pen/MWQjEYN) by Adrian Bece.
Inline SVG styling is one of the strongest applications. Icons exported from design tools typically ship with hard-coded fill values on individual paths. Replacing those values with currentColor means the icon follows whatever text color it sits next to, so you never have to write verbose, path-level selectors just to recolor an SVG.
<!-- Before -->
<path fill="#bbdb44" d="..."/>
<!-- After -->
<path fill="currentColor" d="..."/>
/* Before */
.icon:hover path {
fill: #112244;
}
/* After */
.icon {
color: #bbdb44;
}
.icon:hover {
color: #112244;
}
See the Pen [currentColor svg](https://codepen.io/smashingmag/pen/MWQjEKN) by Adrian Bece.
Custom Property Fallbacks
Custom properties are everywhere now, but the fallback value built into the var() function often goes unnoticed. It’s simply the second argument you can pass — the value that applies when the first one hasn’t been defined.
color: var(--color-icon, #9eeb34);
You can also chain a variable as the fallback.
color: var(--color-icon-primary, var(--color-icon-default));
That secondary value becomes a clean way to provide default styling while still allowing customization. It is especially useful for theming without inflating specificity. Consider a global theme override that reassigns a custom property:
:root {
--theme-color-background: #f5f5f5;
--theme-color-text: #111111;
}
/* Global override on a parent class on <body> or <html> element */
.theme--dark {
--theme-color-background: #111111;
--theme-color-text: #f5f5f5;
}
But a blanket override isn’t always right for every component. The usual fix is to override styles on the component itself, which increases specificity and introduces parent-class dependencies into the component’s stylesheet. Using fallback values instead keeps the component’s internal selectors at their natural specificity, while still permitting a theme to swap in different colors through a single variable reassignment.
.box {
color: var(--color-theme-default);
}
.theme--dark .box {
color: var(--color-component-override);
}
:root {
--theme-color-default: darkgoldenrod;
--color-some-other-color: cyan;
}
.theme--dark {
/* Dark theme */
--color-component-override: var(--color-some-other-color);
}
.box {
color: var(--color-component-override, var(--theme-color-default));
}
See the Pen [Custom properties fallback theme](https://codepen.io/smashingmag/pen/wvyzroQ) by Adrian Bece.
Counting Without JavaScript
CSS counters let you maintain numbered state entirely in the stylesheet. The core pieces are counter-reset, which initializes one or more counters (and can set a starting number); counter-increment, which bumps or decrements a counter by a set amount; and the counter() function, which reads the current value for use inside the content property. A reversed keyword can also be passed to counter-reset to make a counter count downward, and negative values work in counter-increment as well.
See the Pen [counters articles and notes](https://codepen.io/smashingmag/pen/RwQGLpQ) by Adrian Bece.
For documents with nested elements, multiple counters can be combined into one displayed label. Say you have a page with sections of articles, each containing a set of notes. If each note should read 3.2 (the second note of the third article), initialize separate counters for both levels and concatenate them inside a single content declaration.
See the Pen [counters articles and notes - nested](https://codepen.io/smashingmag/pen/vYdXemd) by Adrian Bece.
With counters, rearranging or renumbering content no longer means manually updating numbers or writing a script to do it — the browser handles the arithmetic.
Interaction Media Queries
Screen size is a poor proxy for input method. A 1920px-wide display can belong to a desktop with a mouse, a touchscreen laptop, or a smart TV. Interaction media features let you query the primary input device directly, so you can tailor hover states, tap targets, dropdowns, and menus to the way a visitor actually navigates the page.
@media (pointer: fine) {
/* using a mouse or stylus */
}
@media (pointer: coarse) {
/* using touch */
}
@media (hover: hover) {
/* can be hovered */
}
@media (hover: none) {
/* can't be hovered */
}
Controlling Size With aspect-ratio
The aspect-ratio property initially looks useful only for media elements, but it earns its keep in component sizing. Give an element an aspect ratio of 1, for example, and it keeps equal width and height while its content grows or shrinks. That’s perfect for icon buttons that need to adapt to varying label lengths or icon sizes without losing a circular or square shape.
See the Pen [aspect-radio buttons](https://codepen.io/smashingmag/pen/qBxaPoX) by Adrian Bece.
Richer Gradients
Standard browser gradients interpolate between colors in RGB space, which places gray, muddy tones in the middle range — most noticeable with strongly saturated start and end colors. Until browsers allow choosing a different interpolation color space, a practical fix is adding extra midpoint stops. The difference is obvious: comparing a simple two-stop gradient between green and red with a multi-stop version shows the latter’s midrange appearing as a vibrant yellow-orange rather than washed-out brown.
See the Pen [Gradients](https://codepen.io/smashingmag/pen/BaYLwxM) by Adrian Bece.
Balancing Specificity With :is() and :where()
Both pseudo-classes group selectors, but they treat specificity very differently. :is() resolves to the highest specificity among the selectors it contains. That solves repetitive rule-writing. Rather than spelling out every combination of ordered and unordered lists and their nestings, you can compress the pattern into a single expression.
ol li,
ul li {
margin-bottom: 0.25em;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin: 0.25em 0 1em;
}
:is(ol,ul) li {
margin-bottom: 0.25em;
}
:is(ol,ul) :is(ol,ul) {
margin: 0.25em 0 1em;
}
See the Pen [Nested lists](https://codepen.io/smashingmag/pen/jOZMGvO) by Adrian Bece.
Setting default margins for many list combinations with :is() can unexpectedly block a utility class. Give the nested list a helper class like .list-highlight to adjust background, padding, and margin, and nothing changes—the :is() rule’s specificity beats the utility class, no matter how specific the class sounds.
/* Default styles for nested lists */
.list :is(ol,ul) {
margin: 0.25em 0 1em;
}
/* Utility class for a nested list */
.list-highlight {
background: #eeeeee;
padding: 1em 1em 1em 2em;
margin: 0.5em 0;
}
Switching that same selector to :where() drops the entire expression’s specificity to zero. That way, your shared list defaults still apply anywhere they’re needed, but a straightforward utility class can override margins without requiring higher-specificity hacks or !important. That’s the practical division of labor: :is() for grouping when a rule must win, :where() for grouping when the rule must stay easy to override.
.list :where(ol,ul) {
/* ... */
}
See the Pen [Nested lists - :where](https://codepen.io/smashingmag/pen/mdXrBzz) by Adrian Bece.
Offsetting Fixed Headers With scroll-padding
Anchor links that scroll to page sections used to require JavaScript when a fixed header overlapped the scroll target. The standard workaround involved measuring the header height and passing it to scrollTo(), repeating the process at every breakpoint where the header changed size. CSS now handles that offset natively: set scroll-padding-top on the scrolling element, and the browser accounts for the fixed header when bringing a section into view. The value is just another CSS property, so it changes within plain media queries when the header’s height changes.
html {
scroll-padding-top: 6rem;
scroll-behavior: smooth;
}
See the Pen [Scroll offset](https://codepen.io/smashingmag/pen/QWQKqzW) by Adrian Bece.
The shorthand scroll-padding sets all sides at once if you need offsets in multiple directions.
scroll-padding: /* ... */;
scroll-padding-top: /* ... */;
scroll-padding-right: /* ... */;
scroll-padding-bottom: /* ... */;
scroll-padding-left: /* ... */;
Typography Beyond Text Rendering
Animating numeric counters often causes jittery layouts because digits have varying widths — each tick shifts the text slightly left or right. The clean fix is font-variant-numeric: tabular-nums, which assigns equal width to every numeric glyph so the text stays stable during updates.
After applying tabular-nums, the incrementing value occupies constant width throughout the animation:
See the Pen [font-variant-numeric](https://codepen.io/smashingmag/pen/ZErpayJ) by Adrian Bece.
Numerical glyph variants only appear if the loaded font actually includes them; a font that lacks the feature will simply ignore the declaration. For broader glyph control beyond numerals, the parent font-variant property can enable multiple variants simultaneously. Source Sans 3, for example, exposes several numeric styles that can be toggled individually:
See the Pen [font-variant](https://codepen.io/smashingmag/pen/ExQgbvE) by Adrian Bece.
Isolating z-index Conflicts
Struggling with z-index layering becomes routine once a project grows beyond a handful of components. The usual remedy — bumping the z-index of the element that should appear on top — only postpones the problem. A new stacking context can neutralize these fights entirely without relying on arbitrary numeric escalation.
Consider a styled title whose decorative background sits at z-index: 1 while the title text is at z-index: 2. In isolation (pun intended), that component behaves perfectly. Later, a tooltip component with z-index: 1 is added; the intent is that the tooltip appears just above the surrounding text. In an edge case, however, the title’s z-index: 2 text can overlay the tooltip, breaking the expected visual hierarchy.
See the Pen [stacking context - no isolate](https://codepen.io/smashingmag/pen/ZErpaXX) by Adrian Bece.
The typical response is to increase the tooltip’s z-index or to wrap each component in a positioned parent with its own z-index — both work but both rely on magic numbers that can collide with future additions.
isolation: isolate solves this cleanly by telling the browser that the component’s stacking context ends at its boundary. No matter how high a nested element’s z-index climbs, it will never escape that context to compete with elements in a different isolated group. Applying isolation at the root of the title and tooltip components keeps their internal z-indexs low and predictable:
.title {
isolation: isolate;
/* ... */
}
.tooltip-root {
isolation: isolate;
/* ... */
}
See the Pen [stacking context - isolate](https://codepen.io/smashingmag/pen/oNEzooJ) by Adrian Bece.
With both components locked inside separate stacking contexts, the layering conflict disappears — no guessing at z-values needed:
Skipping Rendering Work Deliberately
Most projects will never encounter paint bottlenecks from normal DOM sizes. But when a page holds tens of thousands of nodes — analytics dashboards, data grids, or long-form content feeds — browsers can struggle with scroll responsiveness and frame rates. CSS offers two properties that let the browser skip rendering operations it doesn’t need to perform.
contain communicates to the browser exactly which parts of the render pipeline won’t be affected by changes inside a subtree. Thus the browser can safely skip style recalculation and layout for that area. The property is powerful yet precise: you must be certain about what won’t change, and validate that no visual regressions appear. Rachel Andrew’s detailed write-up on containment is worth reading before applying it to real code.
.container {
/* child elements won't display outside of this container so only the contents of this container should be rendered*/
contain: paint;
{
The usefulness of contain tends to surface in specific, heavy edge cases — one reported example involved over 38,000 elements on a single page causing significant scroll lag in Google Search Console, fixed with a single line applying containment.
A more approachable sibling is content-visibility, which defers rendering of off-screen and below-the-fold content automatically. Developers sometimes call this “lazy-rendering” because the browser skips paint until an element approaches the viewport. The technique can be added to sections that appear later in a page:
.story {
content-visibility: auto; /* Behaves like overflow: hidden; */
contain-intrinsic-size: 100px 1000px;
}
The companion contain-intrinsic-size value supplies an estimated height for the deferred section before it actually renders. Without it, the browser initially treats the section as having zero height, causing layout jumps and an unstable scrollbar while the page loads:
See the Pen [Content-visibility Demo: Base (With Content Visibility)](https://codepen.io/smashingmag/pen/jOZMapm) by Vladimir Levin.
Ways to compute that intrinsic size include server-side scripting — PHP can generate estimated values for many pages and screen sizes automatically — or measuring with JavaScript. Regardless of method, these optimizations are best saved for real, measurable performance problems; there is no need to add them preemptively to ordinary, fast-loading pages. Keep them in your toolset for when you see lag or dropped frames in very large DOMs, then apply deliberately and test thoroughly.



