Modern CSS You Can Actually Use Today

CSS changes quickly now.

Every year brings new features, but not every new feature is something I want to use on a real website right away. Some are still experimental, some solve very rare problems, and some need browser support that may not fit the project.

This article is about the other group: modern CSS features that are already useful for normal websites.

These are the ones I reach for most often.

:has()

:has() is often called the parent selector, although it can do more than that.

It lets you style an element based on what is inside it.

<div class="field">
  <label for="email">Email</label>
  <input id="email" type="email" required>
</div>
.field:has(input:invalid) {
  border-color: red;
}

You can also react to focus:

.field:has(input:focus) {
  outline: 2px solid currentColor;
}

Or change a card if it has an image:

.card:has(img) {
  grid-template-columns: 12rem 1fr;
}

This removes many small JavaScript class toggles.

CSS nesting

If you have used Sass, nesting will look familiar.

Instead of:

.card {
  padding: 1rem;
}

.card h2 {
  margin: 0;
}

.card a {
  color: blue;
}

you can write:

.card {
  padding: 1rem;

  h2 {
    margin: 0;
  }

  a {
    color: blue;
  }
}

You can also use &:

.button {
  background: black;
  color: white;

  &:hover {
    opacity: 0.8;
  }
}

For small components, this can make the stylesheet easier to read without needing a preprocessor.

Container queries

Media queries look at the viewport.

Container queries look at the space a component has.

First:

.card-list {
  container-type: inline-size;
}

Then:

@container (min-width: 40rem) {
  .card {
    display: grid;
    grid-template-columns: 12rem 1fr;
  }
}

The container query documentation shows how components can react to their own container rather than the whole browser window.

This is very useful when the same card can appear in several layouts.

Subgrid

CSS Grid is already useful, but subgrid fixes a common problem.

Imagine several cards:

<div class="cards">
  <article class="card">
    <h2>Short title</h2>
    <p>Description.</p>
    <a href="#">Read more</a>
  </article>

  <article class="card">
    <h2>A much longer title that wraps</h2>
    <p>Description.</p>
    <a href="#">Read more</a>
  </article>
</div>

You may want the titles, text, and links to line up.

.cards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: auto auto 1fr;
}

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

The child card can use the row sizes from the parent grid.

That is much cleaner than trying to force every heading and paragraph to the same height.

clamp()

clamp() is great for values that should grow but stay inside limits.

For a heading:

h1 {
  font-size: clamp(2rem, 5vw, 4rem);
}

This means:

Minimum: 2rem
Preferred: 5vw
Maximum: 4rem

The font grows with the viewport but never becomes smaller or larger than the limits.

It is also useful for spacing:

section {
  padding-block: clamp(3rem, 8vw, 8rem);
}

This can replace several small media queries.

min(), max(), and clamp()

These three functions work well together.

Limit a content area:

main {
  width: min(70rem, calc(100% - 2rem));
  margin-inline: auto;
}

Keep something at least a certain size:

.hero {
  min-height: max(30rem, 60vh);
}

They make responsive CSS much easier to write.

aspect-ratio

You no longer need padding hacks to keep a box at a fixed ratio.

.video {
  aspect-ratio: 16 / 9;
}

A square:

.avatar {
  aspect-ratio: 1;
}

A photo:

.card img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

Simple and readable.

object-fit

This is not brand new, but it is still one of the most useful modern layout tools for images.

.photo {
  width: 100%;
  height: 20rem;
  object-fit: cover;
}

The image fills the area without being stretched.

Use object-position to change the crop position:

.photo {
  object-position: center top;
}

Logical properties

Instead of:

.card {
  margin-left: 2rem;
  padding-top: 1rem;
  padding-bottom: 1rem;
}

you can use:

.card {
  margin-inline-start: 2rem;
  padding-block: 1rem;
}

Common logical properties include:

margin-inline
margin-block
padding-inline
padding-block
inset-inline
inset-block
border-inline

They are especially useful for sites that may use right-to-left languages, but I also like them because they describe layout direction more clearly.

gap outside Grid

gap is now useful in Flexbox too.

nav {
  display: flex;
  gap: 1rem;
}

This is cleaner than adding margins to every child and then removing the margin from the last item.

For vertical stacks:

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

color-mix()

You can create a new color from two others:

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

Or create a light tint:

.notice {
  background:
    color-mix(in oklab, var(--brand) 12%, white);
}

The color-mix() function is useful when a color should stay connected to a design variable.

oklch()

oklch() is a newer way to define colors:

:root {
  --brand: oklch(62% 0.18 250);
}

The values control:

Lightness
Chroma
Hue

I like it because changing the lightness tends to produce more natural steps than doing the same thing in HSL.

:root {
  --brand-light: oklch(82% 0.10 250);
  --brand: oklch(62% 0.18 250);
  --brand-dark: oklch(42% 0.14 250);
}

light-dark()

For a simple light and dark theme:

:root {
  color-scheme: light dark;

  --background: light-dark(#fff, #111);
  --text: light-dark(#111, #eee);
}

Then:

body {
  background: var(--background);
  color: var(--text);
}

This can make theme variables much easier to manage.

field-sizing

A textarea can grow with its content:

textarea {
  field-sizing: content;
  min-height: 6rem;
  max-height: 20rem;
}

This is the kind of small feature that can replace a whole little JavaScript script.

Because it is newer, check support if the behaviour is important to your form.

scroll-behavior

Smooth anchor links need almost no code:

html {
  scroll-behavior: smooth;
}

I usually add:

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

That respects users who prefer less motion.

Scroll snapping

A horizontal gallery can snap into place:

.gallery {
  display: flex;
  overflow-x: auto;
  gap: 1rem;

  scroll-snap-type: x mandatory;
}

.gallery > * {
  flex: 0 0 80%;
  scroll-snap-align: start;
}

For a simple swipeable list, this may be enough without a carousel library.

position: sticky

Sticky elements do not need scroll event listeners.

.sidebar {
  position: sticky;
  top: 1rem;
}

Or:

header {
  position: sticky;
  top: 0;
}

The main thing to remember is that sticky positioning depends on the scroll container and can be affected by ancestor overflow settings.

:focus-visible

Normal :focus styles are important, but :focus-visible can make keyboard focus clearer without always showing the same outline after a mouse click.

a:focus-visible,
button:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 3px;
}

Do not remove focus outlines without providing a useful replacement.

@supports

This is one of the easiest ways to use new CSS safely.

.card {
  display: block;
}

@supports (display: grid) {
  .card {
    display: grid;
  }
}

For a newer feature:

@supports (field-sizing: content) {
  textarea {
    field-sizing: content;
  }
}

The older browser keeps the base version.

Use new CSS as an improvement

This is the way I prefer to use modern CSS:

Good basic layout
      ↓
Add newer feature
      ↓
Better experience in supported browsers

Not:

New feature
      ↓
Site breaks without it

A website does not need to look exactly the same in every browser.

It needs to work.

What I would use today

For normal production work, I am comfortable using things such as:

Grid
Flexbox
gap
aspect-ratio
object-fit
clamp()
min()
max()
logical properties
:has()
CSS nesting
container queries
subgrid
color-mix()
oklch()
:focus-visible
position: sticky
scroll snap

For newer features such as field-sizing or scroll-driven animations, I am more likely to use them as an extra improvement with a fallback.

Check support, but do not be afraid of CSS

There was a time when using a new CSS feature meant waiting years for browser support.

That still happens sometimes, but the situation is much better now.

Before using something new, I check a source such as MDN, look at the browsers the project needs, and ask a simple question:

What happens if this feature is not supported?

If the answer is “the site still works, it just looks a little less nice,” I am usually happy to use it.

Modern CSS can replace a surprising amount of old layout code and small JavaScript helpers.

The trick is not to use every new feature.

It is to know which ones make the code simpler.