Module: CSS
CSS·041·6 MIN READ

041: Modern CSS — Cascade Layers, Nesting, Scope, Subgrid, Logical Properties, and Progressive Features

TOPICS COVERED: 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:

css
@layer reset, base, theme, components, utilities;

Then assign rules:

css
@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

css
@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

css
.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:

css
.page {
  .main {
    .dashboard {
      .panel {
        .title {
          /* too coupled */
        }
      }
    }
  }
}

Prefer component ownership:

css
.panel__title { ... }

Scope

@scope lets rules target a bounded subtree without requiring every selector to repeat a wrapper.

Conceptual example:

css
@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:

css
.card {
  margin-left: 1rem;
  padding-right: 2rem;
  border-left: 4px solid blue;
}

Logical:

css
.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-size
  • min-inline-size / max-inline-size
  • margin-inline / margin-block
  • padding-inline / padding-block
  • inset-inline-start / inset-block-start
  • border-inline-start

Worked example: notification that supports direction

css
.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

css
.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:

css
.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

Cards that need aligned internal rows:

css
.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

html
<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>
css
.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:

css
.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

css
.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:

css
.card-list {
  display: flex;
  flex-wrap: wrap;
}

Enhancement:

css
@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.

css
:root {
  --brand: oklch(62% 0.18 255);
  --brand-strong: oklch(52% 0.18 255);
}

oklch() uses:

  1. lightness;
  2. chroma;
  3. hue;
  4. 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():

css
.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

css
: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:

css
: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

css
.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

css
.modal-body {
  overflow: auto;
  overscroll-behavior: contain;
}

This can prevent scrolling inside a contained region from chaining unexpectedly to the page.

Stable scrollbar space

css
.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.

css
[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:

js
document.startViewTransition(() => {
  renderNextView();
});

CSS can name an element:

css
.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.

css
.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:

css
.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:

css
.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:

css
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:

  1. identify the user problem;
  2. define the baseline experience;
  3. check project browser support;
  4. implement the smallest enhancement;
  5. test keyboard, zoom, motion preference, contrast, and RTL where relevant;
  6. measure performance if the feature changes rendering cost;
  7. 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 direction instead of correct HTML directionality.
  • Using subgrid when independent card layouts would be simpler.
  • Adding @supports fallbacks for browsers the product does not support.
  • Adopting new features solely because they are new.

Practice set

  1. Put reset/vendor/base/components/utilities into cascade layers.
  2. Convert a flat component to native nesting, then keep nesting no deeper than necessary.
  3. Build a prose scope with @scope.
  4. Replace left/right spacing with logical properties.
  5. Build pricing cards with subgrid.
  6. 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