The Year CSS Ships as a Platform

2022 is shaping up to be a landmark year for CSS, not just because of the volume of new features landing, but because browser vendors are coordinating their release efforts. The joint goal is to implement fourteen features together, a scale of collaboration that has been rare in the platform’s history.

This is a high-level survey of the styling landscape as presented recently, intended to provide breadth rather than deep implementation detail. Where a feature sparks your interest, follow the linked resources for deeper dives.

How We Got Here: Browser Alignment

The current cooperative push owes a debt to the groundwork laid by Compat 2021. That effort, driven by developer feedback gathered through surveys, focused on stabilizing five existing, heavily-used features:

  1. sticky positioning
  2. aspect-ratio sizing
  3. flex layout
  4. grid layout
  5. transform positioning and animation

Those areas saw meaningful test score improvements across all browsers, which translated into measurably more stable and reliable behavior for developers. It was a necessary foundation: you can’t build new capabilities on shifting ground.

Building on that success, the Interop 2022 initiative goes a step further. Rather than each browser vendor independently prioritizing features, the major engines sat down together and agreed on a shared set of targets. The roadmap they committed to includes:

  • @layer
  • Color spaces and functions
  • Containment
  • <dialog>
  • Form compatibility
  • Scrolling
  • Subgrid
  • Typography
  • Viewport units
  • Web compat

This list is notable for its ambition. It mixes long-requested features like subgrid with structural changes like layer-based cascade control, indicating a focus on helping developers architect stylesheets more deliberately rather than just adding polish at the edges.

The hope is that by shipping these features in lockstep, developers can begin using them with confidence. The fragmentation that plagued previous generations of CSS—where you had to support a feature only in some browsers for years—should be far less of a barrier as this roadmap unfolds.

Interop 2022’s Cascade and Layout Features

The CSS features shipping in 2022 are heavily shaped by the Interop 2022 initiative, which has pushed browser vendors to converge on a shared set of layout and color capabilities. Several of the most significant additions address long-standing authoring pain points around the cascade, grid alignment, and responsive design.

Cascade layers with @layer

Before cascade layers, the order in which stylesheets loaded determined which declarations won the cascade. Authors had to carefully orchestrate the loading of less important styles before more important ones, often relying on methodologies such as ITCSS to keep the whole system manageable.

The @layer at-rule removes that fragility. A stylesheet can predefine layers and their priority order up front. As styles load, they are assigned to a named layer, so the intended override behavior is preserved without requiring a meticulously managed load sequence. Chrome DevTools highlights which styles originate from which layer, making the effect of the cascade visible during debugging.

Screenshot of the Styles sidebar of Chrome Devtools, highlighting how styles appear within new Layer groups.

Subgrid

Before subgrid, a grid nested inside another grid could not align its tracks to the parent’s cells or lines. Each grid was an isolated layout system, which made it impossible for designers to maintain a single underlying column structure across an entire page.

With subgrid, a grid item can adopt the parent grid’s columns or rows as its own. Named lines defined on the parent become usable within the child grid, allowing descendants to align to the same structural columns. The following markup shows a body element creating a three-column grid, where the left and right columns are named fullbleed and the middle is named main. Both the <nav> and <main> elements then inherit those columns by setting grid-template-columns: subgrid.

​​body {
  display: grid;
  grid-template-columns:
    [fullbleed-start]
    auto [main-start] min(90%, 60ch) [main-end] auto
    [fullbleed-end]
  ;
}

body > * {
  display: grid;
  grid-template-columns: subgrid;
}

Children of those elements can now size themselves against the fullbleed and main column lines directly.

.main-content {
  grid-column: main;
}

.fullbleed {
  grid-column: fullbleed;
}

Firefox DevTools currently offers the clearest visualization of these nested grids, overlaying the parent grid and its subgrids so the alignment relationship is obvious. The elements panel also flags which nodes are grids and subgrids, which is useful for validating a layout during development.

Screenshot of the Chrome Devtools Elements panel labelling which elements have grid or subgrid layouts.
Screenshot from Firefox Devtools

Container queries

Historically, an element could only respond to viewport size. That worked fine for macro layouts, but did nothing for components whose context is a smaller container rather than the full window. The @container rule lets elements respond to the size or style of a named ancestor container. The only requirement is that the container opts in with the container-type property, a small cost for what many now call the new responsive.

/* establish a container */
.day {
  container-type: inline-size;
  container-name: calendar-day;
}

The styles above mark the weekday columns in a calendar demo as queryable containers. Later rules then adjust the events sitting inside those columns:

@container calendar-day (max-width: 200px) {
  .date {
    display: block;
  }

  .date-num {
    font-size: 2.5rem;
    display: block;
  }
}

The same pattern scales to any reusable component. A book card, for instance, can adapt its internal layout and typography to the column width it happens to occupy at any moment, including when it is dragged to a new area of the page.

Demo by Max Böck

Form Controls and Color

Beyond layout, 2022 brought changes to how form controls look and a major expansion of the CSS color toolbox. The latter is especially notable: sRGB has dominated the web for decades, but that is no longer sufficient for high-definition displays, let alone for design systems that need adaptable, accessible palettes.

accent-color

Matching a form to a brand color used to mean replacing native components with custom libraries or overlay CSS. The accent-color property reduces that to a single declaration. Native controls pick up the specified color, and the browser automatically calculates appropriate contrasting colors for internal parts, while respecting light or dark system color schemes.

Light and dark accented HTML elements side by side for comparison.

New color spaces and functions

CSS now offers a series of color spaces beyond sRGB, each with different trade-offs. Some reach into the wider gamut of high-definition displays; others are built for perceptual uniformity or for better gradient interpolation. The language also gains new color functions that allow mixing, contrast selection, and channel-wise manipulation directly in the browser, removing the need for preprocessors or JavaScript to orchestrate color theming.

@media (dynamic-range: high) {
  .neon-pink {
    --neon-glow: color(display-p3 1 0 1);
  }
}

@supports (color: lab(0% 0 0)) {
  .neon-pink {
    --neon-glow: lab(150% 160 0);
  }
}

hwb()

HWB — hue, whiteness, and blackness — is a new syntax that may feel familiar to artists who lighten or darken a base hue by adding white or black. It produces sRGB colors, so it brings no new color gamut, but the mental model can make certain color adjustments more straightforward.

color-mix()

Previously, mixing colors meant running a preprocessor such as Sass or shipping JavaScript to compute the result. color-mix() performs the mix in the browser, and it accepts a color space argument. If the color space is omitted, mixing happens in LCH. A typical use is deriving lighter and darker variants of a brand color for hover states:

.color-mix-example {
  --brand: #0af;

  --darker: color-mix(var(--brand) 25%, black);
  --lighter: color-mix(var(--brand) 25%, white);
}

Switching the mix to the sRGB space is as simple as changing that argument:

.color-mix-example {
  --brand: #0af;

  --darker: color-mix(in srgb, var(--brand) 25%, black);
  --lighter: color-mix(in srgb, var(--brand) 25%, white);
}

The result is that theming systems can be built around a single base color, and the browser handles variant generation on the fly.

color-contrast()

Choosing readable text against a given background usually required a precomputed palette. The color-contrast() function offloads that decision to the browser. The function can select between black and white, or it can be given a list of candidate colors. In the simplest case, the browser picks whichever option has the highest contrast:

color: color-contrast(gray);

A preferred list can be supplied, and if the goal is only to clear a minimum readability threshold, a target contrast ratio can be specified. The browser then returns the first color from the list that passes:

color: color-contrast(
  var(--bg-blue-1)
  vs
  var(--text-lightest), var(--text-light), var(--text-subdued)
  to AA /* 4.5 could also be passed */
);

Expect legibility to improve as the choice of accessible text colors becomes a first-class part of the language.

Relative color syntax

Manipulating a color channel used to require storing each channel in its own custom property, often forcing authors into HSL just to make use of calc(). Relative color syntax changes that by allowing any color to destructure, modify, and return another color in a single expression, within any supported color space.

In the syntax below, a base hex color is converted to LCH and used as the source for two derived colors. The first overwrites the lightness channel with 75%; the second reduces chroma by 20%:

.relative-color-syntax {
  --color: #0af;
  --absolute-change: lch(from var(--color) 75% c h);
  --relative-change: lch(from var(--color) l calc(c-20%) h);
}

The feature lends itself to generating color variants and even full palettes. A single set of CSS rules, fed a base color, can generate an entire perceptually even palette when operating in LCH.

:root {
  --_color-base: #339af0;

  --color-0:  lch(from var(--_color-base) 98% 10 h);
  --color-1:  lch(from var(--_color-base) 93% 20 h);
  --color-2:  lch(from var(--_color-base) 85% 40 h);
  --color-3:  lch(from var(--_color-base) 75% 46 h);
  --color-4:  lch(from var(--_color-base) 66% 51 h);
  --color-5:  lch(from var(--_color-base) 61% 52 h);
  --color-6:  lch(from var(--_color-base) 55% 57 h);
  --color-7:  lch(from var(--_color-base) 49% 58 h);
  --color-8:  lch(from var(--_color-base) 43% 55 h);
  --color-9:  lch(from var(--_color-base) 39% 52 h);
  --color-10: lch(from var(--_color-base) 32% 48 h);
  --color-11: lch(from var(--_color-base) 25% 45 h);
  --color-12: lch(from var(--_color-base) 17% 40 h);
  --color-13: lch(from var(--_color-base) 10% 30 h);
  --color-14: lch(from var(--_color-base) 5% 20 h);
  --color-15: lch(from var(--_color-base) 1% 5 h);
}

Screenshot of 15 palettes all generated dynamically by CSS.
Try the demo

Gradient color spaces

Gradients traditionally interpolated in sRGB, whose path between two colors can pass through a murky "gray dead zone". CSS now lets authors specify the interpolation color space via a new in keyword. The syntax follows the gradient direction and is optional, with LCH set as the default:

background-image: linear-gradient(
  to right in hsl,
  black, white
);

background-image: linear-gradient(
  to right in lch,
  black, white
);

The effect is visible in something as basic as a black-to-white gradient, where the transition curve differs dramatically per color space. The more striking case is a black-to-blue gradient, where several spaces drift into purple along the way due to the straight line their interpolation path takes through the color space.

11 color spaces shown comparing blue to black.

User Experience Primitives

The remaining features in this group address focus management, font rendering, viewport adaptivity, and selector expressiveness. Each solves a practical problem without requiring auxiliary libraries.

inert

Keeping a user focused on a dialog or an open side menu traditionally relied on focus trapping: listening for focus changes and forcibly returning focus when it escaped the intended region. The global inert HTML attribute takes a different approach. Mark an area as inert, and it is completely removed from the tab order and click target consideration. It is more like guarding a section of the page than trapping a user within another.

The behavior mirrors that of the native JavaScript alert() dialog: while the alert is open, everything else on the page becomes unavailable. A code sample makes the implementation straightforward:

<body>
  <div class="modal">
    <h2>Modal Title</h2>
    <p>...<p>
    <button>Save</button>
    <button>Discard</button>
  </div>
  <main inert>
    <!-- cannot be keyboard focused or clicked -->
  </main>
</body>

For a non-modal side menu, making the backdrop page inert while the menu is open guarantees mouse and keyboard interactions stay in the menu, eliminating the confusion of users losing their place behind an open drawer.

COLRv1 fonts

Earlier OT-SVG fonts supported gradients and built-in effects, but file sizes could balloon and in-font customization was limited. COLRv1 delivers vector-scalable, gradient-capable color fonts with a smaller footprint, plus support for blend modes and palette parameters.

Loading a COLRv1 font is no different than loading any other font:

@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);

Customization happens with the @font-palette-values at-rule, which bundles a set of palette options under a custom name:

@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);

@font-palette-values --colorized {
  font-family: "Bungee Spice";
  base-palette: 0;
  override-colors: 0 hotpink, 1 cyan, 2 white;
}

That bundle is then applied to elements using the color font family:

@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);

@font-palette-values --colorized {
  font-family: "Bungee Spice";
  base-palette: 0;
  override-colors: 0 hotpink, 1 cyan, 2 white;
}

.spicy {
  font-family: "Bungee Spice";
  font-palette: --colorized;
}

Emoji — historically bitmap images that blur when scaled — render sharp as vector art in COLRv1, opening the door to duo-tone icon fonts and other rich typographic expression.

New viewport units

Mobile browsers complicated the original vh unit by toggling the visibility of their URL bar. The resulting viewport height changes caused jarring layout shifts as vh was forced to settle on representing the largest of the two possible viewport sizes.

.original-viewport-units {
  height: 100vh;
  width: 100vw;
  --size: 100vmin;
  --size: 100vmax;
}

The new small, large, and dynamic viewport units give authors the control to choose their trade-offs. Opting into the dynamic height unit (dvh) accepts a possible layout shift when the browser UI retracts, but in exchange gets a unit that tracks the actual visible area. The full set includes logical (inline and block) equivalences for each physical category:

Height viewport units:

​​.new-height-viewport-units {
  height: 100vh;
  height: 100dvh;
  height: 100svh;
  height: 100lvh;
  block-size: 100vb;
  block-size: 100dvb;
  block-size: 100svb;
  block-size: 100lvb;
}

Width viewport units:

.new-width-viewport-units {
  width: 100vw;
  width: 100dvw;
  width: 100svw;
  width: 100lvw;
  inline-size: 100vi;
  inline-size: 100dvi;
  inline-size: 100svi;
  inline-size: 100lvi;
}

Smallest viewport side units:

.new-min-viewport-units {
  --size: 100vmin;
  --size: 100dvmin;
  --size: 100svmin;
  --size: 100lvmin;
}

Largest viewport side units:

.new-max-viewport-units {
  --size: 100vmax;
  --size: 100dvmax;
  --size: 100svmax;
  --size: 100lvmax;
}

:has()

Selectors always targeted the subject at the end of the chain. That made it impossible to select a parent element based on its children, a pattern so common that :has() gained the shorthand nickname of the “parent selector”. It is more general than that, but the naming stuck for good reason.

The basic syntax keeps the subject as the element before :has(), whose argument is a child query:

.parent:has(.child) {...}

One practical use is styling a <section> only when it contains a child with focus:

section:has(*:focus-visible) {...}

The pattern extends beyond parents. A common request is styling an anchor differently when it wraps an image, which can now be written as:

a:has(> img) {...}

More subtly, :has() can alter the subject down the tree. Figures with a <figcaption> can cause styles to apply to the images inside them, without selecting the figure itself:

figure:has(figcaption) img {...}

Combined with quantity queries, :has() can switch a grid layout based on the number of items. Combined with interactive pseudo-classes, it opens creative responsive patterns. Feature detection is safe thanks to the selector() function inside @supports:

@supports (selector(:has(works))) {
  /* safe to use :has() */
}

Typing custom properties with @property

Custom properties are famously flexible: any value can be stored in a named variable, then extended, calculated upon, and shared. But that flexibility has a failure mode. A box-shadow built from custom properties breaks the moment one of those properties receives a value CSS doesn't accept in that position — setting --x: red where a length is expected kills the entire shadow.

@property closes that gap by letting you register a custom property with a defined type, an initial value, and control over inheritance:

@property --x {
  syntax: '<length>';
  initial-value: 0px;
  inherits: false;
}

With the property typed as <length>, an attempt to assign red is ignored; the property falls back to its initial-value of 0px, and the shadow keeps rendering.

Animation benefits

Typed properties also open up animation for values the browser otherwise can't interpolate, like gradients. A radial-gradient spotlight effect driven by mouse position is a good example. Normally, the gradient is too complex for the browser to infer how to animate it. Register a single property with @property, however, and the browser understands the intent and can interpolate it smoothly:

@property --focal-size {
  syntax: '<length-percentage>';
  initial-value: 100%;
  inherits: false;
}

.focus-effect {
  --focal-size: 100%;
  --mouse-x: center;
  --mouse-y: center;

  mask-image: radial-gradient(
    circle at var(--mouse-x) var(--mouse-y),
    transparent 0%,
    transparent var(--focal-size),
    black 0%
  );

  transition: --focal-size .3s ease;
}
Try the demo

Limiting modification to one typed property reduces the interpolation surface enough that the browser can animate from a large circle to a pinhole and back. The same approach extends to many other stiffnesses in CSS animation.

Media query convenience features

Range syntax

Media queries have traditionally leaned on min-width and max-width prefixes to express over/under conditions. The newer range syntax reads more like plain comparison operators. A query like this:

@media (min-width: 320px) {
  …
}

...becomes:

@media (width >= 320px) {
  …
}

For both bounds together:

@media (min-width: 320px) and (max-width: 1280px) {
  …
}

...becomes:

@media (320px <= width <= 1280px) {
  …
}

Which style you prefer is a matter of taste; the spec supports both.

@custom-media aliases

Media queries also suffer from repetition. @custom-media allows aliasing an entire media query and referencing it wherever needed, just like a custom property. Well-named aliases align intent with syntax and make queries easier to share across a team:

@custom-media --OSdark  (prefers-color-scheme: dark);
@custom-media --OSlight (prefers-color-scheme: light);

@custom-media --pointer (hover) and (pointer: coarse);
@custom-media --mouse   (hover) and (pointer: fine);

@custom-media --xxs-and-above (width >= 240px);
@custom-media --xxs-and-below (width <= 240px);

Usage is then a short reference instead of a repeated expression:

@media (--OSdark) {
  :root {
    …
  }
}

Nesting and scoping

@nest

Nesting has long been a top preprocessor selling point. Native CSS nesting with @nest removes the repetition of repeating the parent selector:

article {
  color: darkgray;
}

article > a {
  color: var(--link-color);
}

/* with @nest becomes */

article {
  color: darkgray;

  & > a {
    color: var(--link-color);
  }
}

Beyond less typing, nesting keeps styling context within a single style block. An article's styles, including its links, live together instead of scattering across a stylesheet. Careful placement also allows a component to style itself differently based on its parent context:

/* parent owns this, adjusting children */
section:focus-within > article {
  border: 1px solid hotpink;
}

/* with @nest becomes */

/* article owns this, adjusting itself when inside a section:focus-within */
article {
  @nest section:focus-within > & {
     border: 1px solid hotpink;
  }
}

The organizational gains can be substantial for readability and ownership of styles.

@scope

@property and nesting cover typed values and selector organization; the cascade still defaults to global scope, which gets messy on larger sites. @scope provides both a boundary for where styles apply and a way to articulate where they end.

Consider the typical BEM pattern for styling a header inside a card. The naming convention — adding a class like .card__header to the element — exists purely to emulate scoping. With @scope, no such markup is needed:

.card__header {
  color: var(--text);
}

/* with @scope becomes */

@scope (.card) {
  header {
    color: var(--text);
  }
}

@scope also solves ordering conflicts, such as light versus dark themes that rely on source order to win. Wrapping each theme in its own scope lets the correct one apply without ordering tricks:

​​@scope (.light-theme) {
  a { color: purple; }
}

@scope (.dark-theme) {
  a { color: plum; }
}

Most uniquely, @scope can limit where styles stop applying. Naming conventions and preprocessors can't do this — it's a browser-native capability. A style can be confined to a .media-block and further restricted to children that are siblings of the .content element:

@scope (.media-block) to (.content) {
  img {
    border-radius: 50%;
  }

  .content {
    padding: 1em;
  }
}

Layout gaps and data savings

Masonry via grid

Masonry layouts previously required JavaScript. Native masonry in CSS grid keeps content order accurate and removes the library dependency:

Screenshot of the masonry layout which shows numbers traveling along the top, then going down.
Image and demo from Smashing Magazine
https://www.smashingmagazine.com/native-css-masonry-layout-css-grid/
.container {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: masonry;
}

The draft feature is already available to try in Firefox behind a flag.

prefers-reduced-data

Data-saver preferences have historically been the domain of JavaScript. The prefers-reduced-data media query lets CSS opt in:

@media (prefers-reduced-data: reduce) {
  picture, video {
    display: none;
  }
}

Applied to a media-scroller component, the savings are measurable. On a medium viewport, an initial load that would normally make 40 requests for about 700 KB of resources drops to 10 requests and 172 KB with the query active. In combination with loading="lazy", images inside a hidden scroller never trigger network requests at all. Users on metered connections see more titles with fewer distractions, since cover images are omitted entirely.

Screenshot of a TV show carousel interface with many thumbnails and titles shown.

Scroll snap improvements

Custom scroll-snap interactions currently rely on JavaScript observers and state management, which can easily normalize natural scrolling feel. Several proposals aim to hand that orchestration back to the browser.

  • snapChanging() fires as soon as the browser releases a snapped child, signaling the scroller is between snap targets.
  • snapChanged() fires once the scroller has settled on a new snapped child.
  • scroll-start is a CSS property for starting a scroller at a specified position rather than the top.
  • :snap-target is a selector that matches the element currently snapped within a scroll-snap container.

Together these remove a layer of orchestration from sliders, carousels, and galleries. These APIs remain in early draft stages; watch for polyfills to arrive.

Emerging interaction primitives

toggle() for custom states

Stylable states today are limited to what the browser already exposes, like :checked on checkboxes. toggle() would allow creating custom states on any element, with support for cycling through groups and directed toggling.

In one example, a strikethrough on task completion can be implemented without <input type="checkbox"> elements at all, relying instead on toggle() style states:

li {
  toggle-root: check self;
}

li:toggle(check) {
  text-decoration: line-through;
}

The syntax shares conceptual ground with state machines, potentially moving more interaction state into CSS itself.

The <selectmenu> element

Elements inside a <select> have resisted meaningful styling, pushing developers toward JavaScript libraries that rebuild native behavior. The proposed <selectmenu> element aims to allow rich HTML for options while keeping semantics and accessibility intact:

<selectmenu>
  <option>Option 1</option>
  <option>Option 2</option>
  <option>Option 3</option>
</selectmenu>

Its internal parts are targetable by CSS:

.my-select-menu::part(button) {
  color: white;
  background-color: red;
  padding: 5px;
  border-radius: 5px;
}

.my-select-menu::part(listbox) {
  padding: 10px;
  margin-top: 5px;
  border: 1px solid red;
  border-radius: 5px;
}
A select looking menu with red accent colors.

Experimental support exists in Chromium Canary behind the web experiments flag, with the form factor continuing to evolve.

anchor() positioning

Absolute and relative positioning only let an element move within its parent. anchor() proposes positioning any element relative to any other element, with control over which edge to align against. The approach targets creating position relationships for features like popovers and tooltips, without being constrained by DOM parentage. The spec is still in explainer form.