Skip to main content

Modern CSS Features

CSS has evolved rapidly in recent years. Features that previously required preprocessors (Sass, Less) or JavaScript are now built into the language. This chapter covers the most impactful additions.

CSS nesting

You can now nest selectors inside other selectors, reducing repetition and improving readability:

.card {
padding: 24px;
border: 1px solid #ddd;
border-radius: 8px;

h3 {
margin: 0 0 8px;
font-size: 1.25rem;
}

p {
margin: 0;
color: #666;
}

&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

& .badge {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 4px;
background-color: #4a90d9;
color: white;
}
}

Nesting rules

  • Nested selectors that start with a letter (like h3, p) are automatically interpreted as descendants
  • Use & to reference the parent selector explicitly -- required for pseudo-classes (:hover), pseudo-elements (::before), and class selectors (.badge)
  • Do not nest deeper than two or three levels -- it hurts readability and increases specificity

Before vs after nesting

Without nesting:

.nav { display: flex; gap: 16px; }
.nav a { color: white; text-decoration: none; }
.nav a:hover { text-decoration: underline; }
.nav .logo { font-weight: bold; font-size: 1.25rem; }

With nesting:

.nav {
display: flex;
gap: 16px;

a {
color: white;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

.logo {
font-weight: bold;
font-size: 1.25rem;
}
}

Nested CSS groups related rules together, making it easier to see which styles belong to a component.

Nesting media queries

You can nest @media rules inside selectors:

.sidebar {
display: none;

@media (min-width: 1024px) {
display: block;
width: 250px;
}
}

This keeps the responsive behaviour close to the component it affects.

The :has() selector

:has() is a relational pseudo-class -- it lets you style a parent based on what it contains. This was impossible in CSS until :has() arrived.

.card:has(img) {
padding-top: 0;
}

This targets .card elements that contain an <img>. Cards without images keep their normal padding.

Use cases

Style a form based on its validity

form:has(:invalid) .submit-button {
opacity: 0.5;
pointer-events: none;
}

The submit button looks disabled when any form field is invalid.

Highlight a label when its input is focused

.field:has(input:focus) label {
color: #4a90d9;
}

Change layout based on child count

.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}

When the grid has four or more direct children, switch to a two-column layout.

Style previous siblings

:has() combined with the adjacent sibling selector lets you style a previous element based on the next one:

h2:has(+ .subtitle) {
margin-bottom: 4px;
}

Reduce the margin under an <h2> when it is immediately followed by a .subtitle element.

Tip: Think of :has() as "if this element contains..." or "if this element is followed by...". It is one of the most powerful additions to CSS in years.

Container queries

Media queries respond to the viewport size. Container queries respond to the size of a specific container element. This makes components truly self-contained -- they adapt to wherever they are placed.

Defining a container

.card-wrapper {
container-type: inline-size;
container-name: card;
}
PropertyValueMeaning
container-typeinline-sizeTrack the container's inline (width) size
container-typesizeTrack both width and height
container-nameany nameOptional name for targeted queries

Querying the container

@container card (min-width: 400px) {
.card {
display: flex;
gap: 16px;
}

.card img {
width: 150px;
}
}

@container card (max-width: 399px) {
.card img {
width: 100%;
}
}

When the .card-wrapper is 400px or wider, the card switches to a horizontal layout. When it is narrower, the image takes full width. This happens regardless of the viewport size.

Container query units

UnitRelative to
cqw1% of the container's width
cqh1% of the container's height
cqi1% of the container's inline size
cqb1% of the container's block size
.card-title {
font-size: clamp(1rem, 3cqi, 1.5rem);
}

The title scales with the container's width, not the viewport.

Logical properties

Logical properties replace physical directions (left, right, top, bottom) with flow-relative ones (inline-start, inline-end, block-start, block-end). This makes CSS work correctly in right-to-left (RTL) languages and vertical writing modes.

Physical propertyLogical equivalent
margin-leftmargin-inline-start
margin-rightmargin-inline-end
margin-topmargin-block-start
margin-bottommargin-block-end
padding-leftpadding-inline-start
widthinline-size
heightblock-size
border-leftborder-inline-start

Shorthand properties:

.card {
margin-inline: 16px;
padding-block: 24px;
}

margin-inline sets both margin-inline-start and margin-inline-end. padding-block sets both padding-block-start and padding-block-end.

Tip: Start using logical properties in new code. They make your CSS automatically work with different text directions and are the direction CSS is heading.

accent-color

Style native form controls (checkboxes, radio buttons, range sliders, progress bars) with a single property:

:root {
accent-color: #4a90d9;
}

This changes the default blue colour of checkboxes, radios, and range inputs to your brand colour, without needing custom checkbox implementations.

input[type="checkbox"] {
accent-color: #28a745;
}

input[type="range"] {
accent-color: #e74c3c;
}

color-mix()

Blend two colours together in CSS without a preprocessor:

.button:hover {
background-color: color-mix(in srgb, var(--color-primary) 80%, black);
}

This mixes 80% of the primary colour with 20% black -- creating a darker hover shade dynamically.

.muted {
color: color-mix(in srgb, var(--color-text) 60%, transparent);
}

Mix with transparent to create semi-transparent versions of any colour.

Subgrid

When you have nested grids, subgrid lets a child grid inherit the track definitions of its parent:

.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}

.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}

Without subgrid, the titles, descriptions, and buttons inside each card would not align across cards. With subgrid, the child grid inherits the parent's row tracks, and content aligns perfectly.

Other modern features

FeatureWhat it does
text-wrap: balanceBalances text across lines for headings (avoids orphans)
text-wrap: prettyOptimises line breaking for paragraphs
@scopeScopes styles to a specific DOM subtree
view-transitionAnimates between page states (page transitions)
@starting-styleDefines the starting style for entry animations
popoverNative popover behaviour with CSS styling
anchor positioningPosition elements relative to other elements without JS

These features are at various stages of browser support. Check caniuse.com before using them in production.

Browser support strategy

Not all browsers support every modern feature. Follow this approach:

  1. Check support: Use caniuse.com or MDN browser compatibility tables
  2. Use progressive enhancement: Start with styles that work everywhere, layer on modern features
  3. Use @supports: Test for feature support in CSS:
.card {
display: flex;
}

@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
}
}
  1. Set acceptable fallbacks: If nesting is not supported, the browser ignores it and uses the flat rules

What you learned

  • CSS nesting groups related rules and reduces repetition
  • :has() styles parents based on their children -- one of the most powerful modern selectors
  • Container queries make components respond to their container's size, not the viewport
  • Logical properties replace left/right/top/bottom with flow-relative directions
  • accent-color styles native form controls in one line
  • color-mix() blends colours dynamically without preprocessors
  • Subgrid aligns nested grid children to the parent grid's tracks
  • Use @supports and progressive enhancement for features with limited browser support

Next step

You now know all the CSS tools available. The next chapter covers architecture and best practices -- how to organise your CSS files, name your classes, and structure your code for long-term maintainability.