CSS, layout & accessibility

Cascade, layout and modern CSS

Specificity and cascade layers, custom properties and logical properties, flexbox versus grid and subgrid, container queries, :has(), nesting and @scope, modern colour, view transitions, anchor positioning, popover and dialog, and the Baseline vocabulary for support.

"Why does my style not apply" is the cascade; "how do I lay this out" is grid and flexbox; "what is new" is the list below. A senior answers all three in terms of the mechanism, and knows which features are safe to ship without fallbacks in 2026.

Know

  • The cascade: origin and importance → cascade layers → specificity → source order. @layer reset, base, components, utilities declares order once; a later layer wins regardless of specificity, and unlayered styles beat all layers. Layers replace the specificity wars that BEM and !important fought. :where() has zero specificity (resets), :is() takes the highest of its arguments.
  • Specificity is (ids, classes/attributes/pseudo-classes, elements); inline styles beat everything but !important; !important inverts layer order. Nesting (&) is native; @scope (.card) to (.content) limits selectors to a subtree and stops "donut" leakage, with proximity resolving ties.
  • Custom properties cascade and inherit, are resolved at computed-value time, and are how tokens travel; @property registers a type so they animate and have a default; var(--x, fallback).
  • Logical properties (margin-inline-start, inset-block, padding-block) follow writing direction; use them by default so RTL and vertical scripts work without duplicated rules.
  • Flexbox versus grid: flexbox for one-dimensional distribution along a line (toolbars, nav, centring); grid for two-dimensional layout and for placing items on both axes at once (grid-template-areas, minmax(), auto-fit/auto-fill, repeat()); subgrid lets nested items align to the parent's tracks (cards with aligned titles and prices). gap works in both; aspect-ratio reserves space.
  • Container queries: container-type: inline-size on the parent, @container (min-width: 32rem) in the child, cqi/cqw units; components respond to their container, which is what media queries never did. :has() is the parent selector (.field:has(:invalid), li:has(> img)), also usable for state without JavaScript.
  • Colour and typography: oklch()/oklab() for perceptually uniform palettes, color-mix() for tints, light-dark() with color-scheme for theme values, relative colour syntax; text-wrap: balance/pretty, font-display, variable fonts, clamp() for fluid type.
  • Motion and state: @starting-style plus transition-behavior: allow-discrete to animate display: none and popovers; scroll-driven animations (animation-timeline: scroll()/view()); view transitions same-document (document.startViewTransition, Nuxt's experimental.viewTransition) and cross-document (@view-transition { navigation: auto }); prefers-reduced-motion respected always.
  • Overlays: the Popover API (popover, popovertarget, top layer, light dismiss) and <dialog showModal()> (top layer, focus trapping, ::backdrop); anchor positioning (anchor-name, position-anchor, position-area, anchor(), position-try-fallbacks) is Baseline Newly available since January 2026 and replaces Popper/Floating UI for tooltips and menus where the browser matrix allows.
  • Rendering performance in CSS: contain, content-visibility: auto for long pages, will-change sparingly, animate transform/opacity, avoid layout-triggering properties in animations, field-sizing: content for auto-growing inputs.
  • Support vocabulary: Baseline Newly available (all major engines ship) versus Widely available (30 months later); @supports for progressive enhancement; the if() function and a few others are shipping but not Baseline yet.

How it works

app/assets/css/main.css
@layer reset, tokens, base, components, utilities;

@layer tokens {
  :root {
    color-scheme: light dark;
    --space-3: 0.75rem;
    --radius: 0.5rem;
    --brand: oklch(62% 0.19 260);
    --bg: light-dark(oklch(99% 0 0), oklch(18% 0.02 260));
    --text: light-dark(oklch(20% 0.02 260), oklch(95% 0.01 260));
  }
  @property --progress { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
}

@layer components {
  .product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
    gap: var(--space-3);
  }

  .product {
    container-type: inline-size;
    display: grid;
    grid-template-rows: subgrid;               /* align title/price rows across cards */
    grid-row: span 3;
    border-radius: var(--radius);
    background: color-mix(in oklch, var(--bg) 96%, var(--brand));
    padding-inline: var(--space-3);            /* logical: flips for RTL */

    &:has(.badge--sale) { outline: 2px solid var(--brand); }
    .title { text-wrap: balance; font-size: clamp(1rem, 0.9rem + 1cqi, 1.25rem); }
    @container (min-width: 24rem) { grid-template-columns: 1fr auto; }
  }
}

@scope (.product) to (.reviews) {
  a { color: var(--brand); }                   /* styles product links but not links inside nested .reviews */
}

@layer utilities {
  .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}

A tooltip with anchor positioning and popover, no JavaScript positioning library:

app/components/Ui/Tip.vue
<script setup lang="ts">
const id = useId()
</script>

<template>
  <button type="button" :popovertarget="id" :style="{ anchorName: `--tip-${id}` }"><slot name="trigger" /></button>
  <div :id="id" popover role="tooltip" class="tip" :style="{ positionAnchor: `--tip-${id}` }"><slot /></div>
</template>

<style scoped>
.tip {
  position-area: top center;
  position-try-fallbacks: flip-block;           /* flip below when there is no room above */
  margin: 0; inset: auto;
  @starting-style { opacity: 0; }
  transition: opacity 0.12s, display 0.12s allow-discrete;
}
@supports not (position-area: top) {
  .tip { position: fixed; inset: auto 1rem 1rem auto; }  /* graceful fallback */
}
</style>
Gotcha· !important inside layers

!important declarations flip the layer order: an important declaration in an earlier layer beats an important one in a later layer. If you adopt layers, ban !important except in a documented utilities layer, or the mental model breaks.

Gotcha· Container queries need a container

@container rules silently do nothing when no ancestor has container-type. Set container-type: inline-size on the component root (not on the element being sized), and remember that a container's size cannot depend on its contents on the queried axis.

Verify before the interview:

Current Baseline status for anchor positioning (Newly available since January 2026), the CSS if() function, cross-document view transitions and field-sizing; check the interview week.

docs ↗

Exercise

Exercise
  • Rebuild a component's styles with @layer and remove every !important; verify the order with the DevTools Styles panel.
  • Convert a card grid from media queries to a container query and drop the same card into a sidebar and a main column.
  • Replace a Floating UI tooltip with anchor positioning plus @supports fallback and test in a browser without support.

Be able to say

Be able to say· What decides which CSS rule wins?

"The cascade, in order: origin and importance first, then cascade layers, then specificity, then source order. Layers are the modern control: I declare @layer reset, base, components, utilities once, and a rule in a later layer wins over any specificity in an earlier one, while unlayered styles beat all layers. Within a layer specificity is ids, then classes, attributes and pseudo-classes, then elements, with :where() contributing zero and :is() the maximum of its arguments. Inline styles beat everything except !important, which inverts the layer order, which is why I ban it outside a utilities layer. And @scope adds proximity as a tie-breaker inside a subtree."

Be able to say· Which modern CSS features do you use in production?

"Container queries so components respond to their own width, :has() for parent and state selectors, cascade layers for order, native nesting and @scope for structure, logical properties for RTL, oklch and color-mix for palettes with light-dark() for themes, clamp() and text-wrap: balance for typography, @starting-style with discrete transitions for popovers, and view transitions for navigation where motion is welcome. Anchor positioning became Baseline in January 2026, so I use it for tooltips and menus with an @supports fallback, and I check Baseline before adopting anything newer such as if()."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.