041: Modern CSS — Cascade Layers, Nesting, Scope, Subgrid, Logical Properties, and Progressive Features
Learning outcomes
By the end, you can use cascade layers to control precedence; use native nesting without deep selector coupling; understand @scope conceptually and practically where supported; build subgrid alignment; use logical properties for writing-mode resilience; and adopt modern CSS through progressive enhancement.
Prerequisites and retrieval
Retrieve cascade/specificity from 020, Grid from 029, architecture from 039, and compatibility from 040.
Cascade layers
Declare priority zones:
@layer reset, base, theme, components, utilities;
Then assign rules:
@layer base {
a {
color: #2563eb;
}
}
@layer components {
.button {
color: white;
background: #2563eb;
}
}
@layer utilities {
.text-danger {
color: #b91c1c;
}
}
Layer order is determined before selector specificity within a layer. This can prevent third-party styles from winning just because they contain aggressive selectors.
Third-party example
@layer reset, vendor, base, components, utilities;
@import url("vendor.css") layer(vendor);
Your later layers can intentionally outrank vendor normal declarations without reproducing their specificity.
Native nesting
.card {
padding: 1rem;
border: 1px solid #cbd5e1;
& .card__title {
margin: 0;
}
&:hover {
border-color: #94a3b8;
}
@media (width >= 40rem) {
padding: 1.5rem;
}
}
Nesting improves locality but can recreate the old Sass problem of deep descendant selectors.
Avoid:
.page {
.main {
.dashboard {
.panel {
.title {
/* too coupled */
}
}
}
}
}
Prefer component ownership:
.panel__title { ... }
Scope
@scope lets rules target a bounded subtree without requiring every selector to repeat a wrapper.
Conceptual example:
@scope (.article) {
h2 {
margin-block-start: 2em;
}
a {
text-decoration-thickness: 0.12em;
}
}
A scope can also define an upper boundary depending on syntax/support.
Use scope for local styling domains, especially content regions where styling semantic elements directly is useful.
Do not confuse scope with Shadow DOM. It is a CSS cascade/matching boundary, not a separate DOM tree.
Logical properties
Physical:
.card {
margin-left: 1rem;
padding-right: 2rem;
border-left: 4px solid blue;
}
Logical:
.card {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-inline-start: 4px solid blue;
}
Logical properties map to the inline/block axes according to writing mode and direction.
High-value pairs:
inline-size/block-sizemin-inline-size/max-inline-sizemargin-inline/margin-blockpadding-inline/padding-blockinset-inline-start/inset-block-startborder-inline-start
Worked example: notification that supports direction
.notice {
padding: 1rem;
border-inline-start: 4px solid #2563eb;
margin-inline: auto;
max-inline-size: 42rem;
}
No separate left/right override is needed just to mirror the accent edge in RTL.
Writing mode and direction
.vertical-label {
writing-mode: vertical-rl;
}
Direction should normally come from document language/HTML (dir) rather than be forced in CSS for entire content regions. CSS direction exists, but semantic reading direction belongs in markup.
Subgrid
Parent:
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
Cards that need aligned internal rows:
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}
Each card participates in the parent row tracks, allowing title/body/action alignment across separate cards.
Worked example
<div class="plans">
<article class="plan">
<h2>Starter</h2>
<p>For individuals.</p>
<a href="/starter">Choose</a>
</article>
<article class="plan">
<h2>Professional</h2>
<p>For teams that need reporting and shared workspaces.</p>
<a href="/pro">Choose</a>
</article>
</div>
.plans {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
grid-auto-rows: auto;
gap: 1rem;
}
.plan {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
gap: 0.75rem;
}
Test current browser support for the exact behavior your project requires.
Modern color helpers
Where supported and appropriate:
.button:hover {
background: color-mix(in srgb, var(--action) 85%, black);
}
Use modern functions to derive variants carefully; verify contrast rather than assuming a darker/lighter mix automatically meets requirements.
Modern viewport units
.hero {
min-block-size: 100dvh;
}
dvh tracks dynamic viewport height. svh uses the small viewport and lvh the large viewport. These help account for mobile browser UI changes better than relying only on classic vh.
Feature-query adoption pattern
Baseline:
.card-list {
display: flex;
flex-wrap: wrap;
}
Enhancement:
@supports (grid-template-rows: subgrid) {
.card-list {
display: grid;
}
}
You may not need a fallback if your official browser baseline already supports the feature. Progressive enhancement is a product decision, not ceremonial extra code.
Modern CSS decision rule
Before adding JavaScript or a build dependency for a presentation problem, check whether the platform now offers:
- custom properties;
:has();- container queries;
- cascade layers;
- native nesting;
- subgrid;
- logical properties;
clamp()/math functions;- media preference queries;
@supports.
But platform CSS is not automatically simpler. Use a feature only when the team can explain it and the support target permits it.
Perceptual color: oklch() and modern color systems
Modern CSS supports perceptual color spaces that can be easier to reason about for lightness and chroma.
:root {
--brand: oklch(62% 0.18 255);
--brand-strong: oklch(52% 0.18 255);
}
oklch() uses:
- lightness;
- chroma;
- hue;
- optional alpha.
It does not guarantee accessibility. Always measure contrast in the actual foreground/background pair.
Modern color features can be combined with color-mix():
.button:hover {
background:
color-mix(in oklch, var(--brand) 85%, black);
}
Use an explicit fallback where your browser support policy requires one.
Color scheme and automatic light/dark decisions
:root {
color-scheme: light dark;
}
This tells the browser that the page supports those schemes and can improve native form-control/system color rendering.
Where supported, light-dark() can select a value according to the active color scheme:
:root {
color-scheme: light dark;
--surface: light-dark(#ffffff, #0f172a);
}
For a product with custom user-selected themes, explicit semantic tokens may still be clearer than relying only on system preference.
Scroll behavior and containment
Scroll snapping
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 80%;
overflow-x: auto;
scroll-snap-type: inline mandatory;
}
.carousel > * {
scroll-snap-align: start;
}
Use scroll snapping when the content model genuinely has discrete stops. Test keyboard, touch, trackpad, zoom, and reduced motion. Do not trap users in an aggressive snap experience.
Overscroll behavior
.modal-body {
overflow: auto;
overscroll-behavior: contain;
}
This can prevent scrolling inside a contained region from chaining unexpectedly to the page.
Stable scrollbar space
.page {
scrollbar-gutter: stable;
}
This can reduce layout movement when a scrollbar appears/disappears.
Entry and exit transitions
@starting-style provides a starting value when an element has no previous rendered style.
[popover] {
opacity: 1;
transform: translateY(0);
transition:
opacity 160ms,
transform 160ms,
display 160ms allow-discrete;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-0.5rem);
}
}
Top-layer elements such as popovers and dialogs are common use cases. Always preserve a usable state when animation is unsupported or reduced.
View transitions
The View Transition API combines JavaScript/browser navigation behavior with CSS pseudo-elements and properties.
For same-document transitions, JavaScript can request a transition:
document.startViewTransition(() => {
renderNextView();
});
CSS can name an element:
.product-image {
view-transition-name: product-image;
}
Cross-document transitions can opt in using @view-transition where supported.
Treat view transitions as progressive enhancement. They should never become the only way a user understands state or navigation.
CSS anchor positioning
Anchor positioning can tether an absolutely positioned element to another element without manually reading geometry in JavaScript.
.help-button {
anchor-name: --help-anchor;
}
.help-popover {
position: absolute;
position-anchor: --help-anchor;
position-area: block-end span-inline-end;
}
Or use anchor() for inset calculations:
.help-popover {
top: calc(anchor(bottom) + 0.5rem);
left: anchor(left);
}
Anchor positioning is particularly useful for popovers, callouts, menus, and teaching tips. Verify your required browser baseline and supply a simpler fallback when necessary.
Rendering containment and content-visibility
For very long pages:
.report-section {
content-visibility: auto;
contain-intrinsic-size: auto 700px;
}
This can let browsers skip offscreen rendering work. It is a performance feature with behavioral consequences, not a default style. Test fragment navigation, find-in-page, layout-dependent JavaScript, printing, and accessibility behavior.
Modern form and intrinsic sizing helpers
Newer platform features can remove JavaScript that existed only to measure presentation.
Example:
textarea {
field-sizing: content;
max-block-size: 15rem;
}
For intrinsic-size transitions, newer sizing/interpolation features may allow smoother transitions between explicit sizes and intrinsic values. These are progressive enhancements: check exact support before relying on them.
Adoption ladder for new CSS
For every modern feature:
- identify the user problem;
- define the baseline experience;
- check project browser support;
- implement the smallest enhancement;
- test keyboard, zoom, motion preference, contrast, and RTL where relevant;
- measure performance if the feature changes rendering cost;
- document why the feature exists.
“Modern” should mean less fragile code, not more novelty.
Common mistakes
- Putting every rule in a cascade layer without a clear order.
- Assuming layers make specificity irrelevant.
- Nesting selectors until components depend on remote DOM.
- Using CSS
directioninstead of correct HTML directionality. - Using subgrid when independent card layouts would be simpler.
- Adding
@supportsfallbacks for browsers the product does not support. - Adopting new features solely because they are new.
Practice set
- Put reset/vendor/base/components/utilities into cascade layers.
- Convert a flat component to native nesting, then keep nesting no deeper than necessary.
- Build a prose scope with
@scope. - Replace left/right spacing with logical properties.
- Build pricing cards with subgrid.
- Add one modern feature through progressive enhancement and document the support reason.
Recap
Modern CSS gives authors more control over cascade architecture, component responsiveness, local scoping, nested authoring, international layout, and cross-component alignment. The goal is not maximum feature usage; it is simpler, more resilient interfaces.
Official references
-
MDN:
oklch()— https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch -
MDN: View Transition API — https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API
-
MDN: CSS anchor positioning — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning
-
MDN:
content-visibility— https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility -
MDN:
@starting-style— https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style -
MDN: Cascade layers — https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
-
MDN: CSS nesting — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting
-
MDN:
@scope— https://developer.mozilla.org/en-US/docs/Web/CSS/@scope -
MDN: Subgrid — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid
-
MDN: Logical properties — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values
