CSS Reference Guide

All TopicsPlayground

Modern CSS Features

Explore the latest CSS features that are transforming how we build for the web — container queries, :has(), cascade layers, native nesting, and much more.

Modern CSSContainer Queries:has()Cascade LayersNesting

Container Queries

Container queries let components respond to their parent container's size, not the viewport. This makes truly reusable, context-aware components possible.

Container Query Syntax
/* 1. Define a containment context */
.card-container {
  container-type: inline-size;  /* respond to width */
  container-name: card-wrap;    /* optional: name the container */
}

/* Shorthand */
.card-container {
  container: card-wrap / inline-size;
}

/* 2. Query the container */
@container (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

/* Query a named container */
@container card-wrap (min-width: 600px) {
  .card__title {
    font-size: 1.5rem;
  }
}

/* container-type values:
   inline-size  — query inline dimension (width in horizontal)
   size         — query both dimensions
   normal       — no containment (default) */
Container Query Demo — Resize to see layout change

Drag the bottom-right corner to resize the container:

Container Query Card

This card's layout changes based on the container width, not the viewport. Narrow = stacked, wide = side-by-side. Resize the dashed border to see it in action.

Container queries vs media queries: Media queries respond to the viewport size. Container queries respond to the parent element size. This means a component can be narrow in a sidebar and wide in a main content area, adapting its layout independently.

:has() — The Parent Selector

The :has() pseudo-class selects an element based on its descendants or subsequent siblings. Often called the "parent selector" — something CSS lacked for over 20 years.

:has() Use Cases
/* Select a parent that contains a specific child */
.card:has(img) {
  padding-top: 0;  /* remove top padding when card has an image */
}

/* Style a label when its input is focused */
label:has(+ input:focus) {
  color: #6366f1;
}

/* Highlight a form group with an invalid field */
.form-group:has(:invalid) {
  border-color: #ef4444;
}

/* Select an element that does NOT contain something */
.card:not(:has(img)) {
  background: #f0f0f0;  /* text-only cards */
}

/* Style body based on modal being open */
body:has(.modal.open) {
  overflow: hidden;
}

/* Style a grid differently based on child count */
.grid:has(> :nth-child(4)) {
  grid-template-columns: repeat(2, 1fr);  /* 4+ items = 2 columns */
}
:has() — Interactive Checkbox Demo

The parent container changes style when the checkbox is checked — powered by :has(input:checked). No JavaScript needed!

:has() — Form Validation Styling

Cascade Layers (@layer)

Cascade layers give you explicit control over the cascade order, independent of specificity and source order. Layers declared first have lower priority.

@layer Syntax
/* 1. Declare layer order (low → high priority) */
@layer reset, base, components, utilities;

/* 2. Add styles to layers */
@layer reset {
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
  }
}

@layer base {
  a { color: #6366f1; }
  h1 { font-size: 2rem; }
}

@layer components {
  .btn {
    padding: 0.5rem 1rem;
    background: #6366f1;
    color: white;
  }
}

@layer utilities {
  .text-center { text-align: center; }
  .hidden { display: none; }
}

/* Utilities layer wins over components, even with
   lower specificity, because it's declared later! */

/* Import a stylesheet into a layer */
@import url('vendor.css') layer(vendor);
@layer Priority Demo

This text is styled by @layer demo-override (green, bold)

Layer order: demo-reset (gray) < demo-base (pink) < demo-override (green). The last layer wins regardless of specificity.

Native CSS Nesting

CSS now supports nesting natively — no preprocessor required. Use & to reference the parent selector, just like Sass.

Native CSS Nesting Syntax
/* Native CSS nesting — works in all modern browsers */
.card {
  padding: 1.5rem;
  border-radius: 12px;
  background: var(--bg-surface);

  /* Nested element */
  & .card-title {
    font-size: 1.25rem;
    font-weight: 600;
  }

  /* Pseudo-classes */
  &:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
  }

  /* Pseudo-elements */
  &::before {
    content: "";
    display: block;
  }

  /* Media queries can nest too! */
  @media (width >= 768px) {
    display: grid;
    grid-template-columns: 200px 1fr;
  }

  /* Modifier patterns */
  &.card--featured {
    border-color: var(--color-primary);
  }
}

/* Deeply nested (but keep it shallow for readability!) */
.nav {
  & .nav-list {
    display: flex;

    & .nav-item {
      &:hover {
        color: var(--color-primary);
      }
    }
  }
}
Native Nesting — Live Styled Component
Styled with Native Nesting
This entire component is styled using native CSS nesting. Hover over it to see the nested :hover styles activate. No Sass, no PostCSS — just plain CSS.
Native CSS

Subgrid

Subgrid allows a grid item's children to participate in the parent grid's track sizing. This solves the alignment problem where cards in a grid have different-height sections.

Subgrid Syntax
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}

.grid-item {
  display: grid;
  /* Inherit row tracks from parent grid */
  grid-row: span 3;               /* item spans 3 row tracks */
  grid-template-rows: subgrid;    /* children use parent's rows */
}

/* Now all .grid-item children align perfectly across the grid,
   even if their content has different lengths! */

/* Works for columns too */
.wide-item {
  grid-column: span 2;
  grid-template-columns: subgrid;
}
The classic problem subgrid solves: In a card grid, each card has a title, description, and button. Without subgrid, the titles, descriptions, and buttons don't align across cards because each card sizes its rows independently. Subgrid forces them all to use the same row tracks.

color-mix()

The color-mix() function blends two colors in a specified color space. Perfect for generating tints, shades, and transparent variations.

color-mix() Syntax
/* color-mix(in colorspace, color1 percentage, color2 percentage) */

/* Mix colors 50/50 */
background: color-mix(in srgb, #6366f1, #f472b6);

/* Create a tint (mix with white) */
--primary-light: color-mix(in srgb, #6366f1 30%, white);

/* Create a shade (mix with black) */
--primary-dark: color-mix(in srgb, #6366f1 70%, black);

/* Create semi-transparent version */
--primary-50: color-mix(in srgb, #6366f1 50%, transparent);

/* Use oklch for perceptually uniform mixing */
--blend: color-mix(in oklch, #6366f1, #34d399);

/* Works with CSS variables! */
--hover-bg: color-mix(in srgb, var(--color-primary) 15%, transparent);
color-mix() Live Swatches
100%
80% + white
60% + white
40% + white
20% + white
mix pink
mix green
oklch mix

accent-color

The accent-color property themes native form controls (checkboxes, radio buttons, range sliders, progress bars) with a single line of CSS.

accent-color Usage
/* Theme all form controls globally */
:root {
  accent-color: #6366f1;
}

/* Or per-element */
.custom-checkbox {
  accent-color: #34d399;
}

.danger-checkbox {
  accent-color: #ef4444;
}
accent-color — Live Form Controls
Indigo accent-color
Pink accent-color
Green accent-color

New Viewport Units

The classic vh unit is unreliable on mobile because it doesn't account for browser chrome (address bar). New units solve this.

New Viewport Units
/* Classic (problematic on mobile) */
height: 100vh;      /* includes space behind address bar on mobile */

/* Dynamic viewport height — changes as browser chrome shows/hides */
height: 100dvh;     /* updates dynamically — RECOMMENDED for most cases */

/* Small viewport height — minimum visible area (address bar showing) */
height: 100svh;     /* safe: never extends behind browser chrome */

/* Large viewport height — maximum visible area (address bar hidden) */
height: 100lvh;     /* same as 100vh on desktop */

/* Width equivalents also exist: dvw, svw, lvw */
/* Block/inline equivalents: dvb, svb, lvb, dvi, svi, lvi */

/* Common pattern for mobile-safe full-height layouts */
.hero {
  min-height: 100svh;  /* or 100dvh */
}
Rule of thumb: Use 100dvh for hero sections and full-screen layouts. Use 100svh when you need a guaranteed minimum height that won't cause content to jump as the address bar animates.

Individual Transform Properties

Instead of a single transform with multiple functions, you can now use individual properties that can be animated independently.

Individual Transform Properties
/* Old way: single transform property */
.element {
  transform: translateX(50px) rotate(45deg) scale(1.2);
}

/* New way: individual properties */
.element {
  translate: 50px 0;
  rotate: 45deg;
  scale: 1.2;
}

/* The big advantage: animate them independently! */
.card {
  translate: 0 0;
  scale: 1;
  transition: translate 0.3s ease, scale 0.2s ease;
}

.card:hover {
  translate: 0 -4px;  /* lifts up slowly */
  scale: 1.02;         /* grows quickly */
}

/* No more overriding the entire transform chain */
.base { rotate: 10deg; }
.base:hover { scale: 1.1; }  /* rotation is preserved! */

@property — Typed Custom Properties

@property registers custom properties with a type, initial value, and inheritance. This enables animating custom properties — something impossible with regular -- variables.

@property Syntax
/* Register a custom property with type information */
@property --gradient-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

@property --color-start {
  syntax: "<color>";
  initial-value: #6366f1;
  inherits: false;
}

/* Now you can ANIMATE the gradient! */
.gradient-box {
  background: linear-gradient(
    var(--gradient-angle),
    var(--color-start),
    #f472b6
  );
  transition: --gradient-angle 1s, --color-start 0.5s;
}

.gradient-box:hover {
  --gradient-angle: 180deg;
  --color-start: #34d399;
}

/* Supported syntax types:
   "<length>"    "<number>"     "<percentage>"
   "<color>"     "<angle>"      "<time>"
   "<integer>"   "<length-percentage>"
   "<custom-ident>"  "<image>"  "*" (any) */

@starting-style

@starting-style defines styles for when an element first appears (entry animation). It enables CSS-only appear transitions for elements added to the DOM or changing from display: none.

@starting-style Syntax
/* Element's normal state */
.toast {
  opacity: 1;
  translate: 0 0;
  transition: opacity 0.3s, translate 0.3s;
}

/* State when element first appears */
@starting-style {
  .toast {
    opacity: 0;
    translate: 0 20px;
  }
}

/* Works with display transitions too! */
.dialog[open] {
  opacity: 1;
  scale: 1;
  transition: opacity 0.3s, scale 0.3s, display 0.3s allow-discrete;
}

@starting-style {
  .dialog[open] {
    opacity: 0;
    scale: 0.9;
  }
}

text-wrap: balance

Automatically balances the number of characters per line in a text block so lines are roughly equal length. Perfect for headings.

text-wrap: balance
/* Apply to headings for balanced line wrapping */
h1, h2, h3 {
  text-wrap: balance;     /* balances lines evenly */
}

/* For body text that should fill all available space */
p {
  text-wrap: pretty;      /* avoids orphans on last line */
}

/* Note: balance works best on short text (max ~6 lines).
   For longer text, the browser falls back to normal wrapping. */
text-wrap: balance vs normal
text-wrap: normal (default)

A Long Heading That Wraps Awkwardly Across Lines

text-wrap: balance

A Long Heading That Wraps Elegantly Across Lines

@supports — Feature Queries

Use @supports to conditionally apply CSS based on browser support. Essential for progressive enhancement.

@supports Syntax
/* Check if a property is supported */
@supports (container-type: inline-size) {
  .card-wrapper {
    container-type: inline-size;
  }
}

/* Check if NOT supported (fallback) */
@supports not (container-type: inline-size) {
  .card {
    /* media query fallback */
  }
}

/* Check for a selector */
@supports selector(:has(*)) {
  /* use :has() safely */
}

/* Combine with AND / OR */
@supports (display: grid) and (gap: 1rem) {
  /* grid with gap support */
}

@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
  .glass {
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
}

Browser Support Table

Feature Chrome Firefox Safari Edge
Container Queries105+110+16+105+
:has()105+121+15.4+105+
@layer99+97+15.4+99+
Native Nesting120+117+17.2+120+
Subgrid117+71+16+117+
color-mix()111+113+16.2+111+
accent-color93+92+15.4+93+
dvh/svh/lvh108+101+15.4+108+
Individual Transforms104+72+14.1+104+
@property85+128+15.4+85+
@starting-style117+129+17.5+117+
text-wrap: balance114+121+17.5+114+
@supports selector()83+69+14.1+83+

Gotchas

Container queries require containment. You must set container-type: inline-size on the parent. Without it, @container queries won't work. This also means the container's inline size can't depend on its children's size.
:has() can be performance-sensitive. Complex :has() selectors (especially with deep descendant checks) can be expensive. Browsers are optimizing rapidly, but keep selectors as simple as possible.
Unlayered styles beat layered styles. Any CSS not inside a @layer has higher priority than all layered styles, regardless of layer order. This is by design but can be surprising.
Native nesting increases specificity. .card { & .title {} } is equivalent to .card .title with specificity (0,0,2,0), not (0,0,1,0). Be aware of this when migrating from Sass.
@property requires all three descriptors. syntax, initial-value, and inherits are all mandatory. Omitting any one of them will cause the rule to be ignored.
text-wrap: balance has a line limit. It only works on blocks with approximately 6 or fewer wrapped lines. Longer text blocks fall back to normal wrapping behavior.

Pro Tips

Use container queries for component libraries. Design systems built with container queries are truly portable — drop a card component in any layout context and it adapts automatically.
Use :has() to eliminate JavaScript state classes. Instead of toggling .has-image via JS, use .card:has(img). Less JS, more declarative, automatically stays in sync.
Layer your architecture. Use @layer reset, base, layout, components, utilities to create a clear priority order. Third-party CSS should always go in its own low-priority layer.
Recommended @layer Architecture
/* Declare order from lowest to highest priority */
@layer reset, vendor, base, layout, components, utilities;

/* Import third-party CSS into a low-priority layer */
@import url('normalize.css') layer(reset);
@import url('library.css') layer(vendor);

/* Utility classes always win (highest layer) */
@layer utilities {
  .hidden { display: none !important; }
  .sr-only { /* screen reader only */ }
}
Use @property for gradient animations. Regular CSS can't animate gradients. But with @property, you register the angle or color as typed custom properties and animate those instead.
Progressively enhance with @supports. Use modern features freely behind @supports, with sensible fallbacks for older browsers. Most modern CSS features degrade gracefully.
Apply text-wrap: balance to all headings globally. It's a safe enhancement: browsers that don't support it simply ignore it. Add h1, h2, h3, h4 { text-wrap: balance; } to your base styles.
PreviousArchitecture & Best Practices