Skip to content

Modern CSS tips and tricks

20 modern CSS features worth using in 2026, from container queries and :has() to cascade layers. Each one has a real before and after, when to reach for it, and the gotcha that trips people up.

20 tips6 topicsfree

Devanshu Verma

Sharing CSS and frontend tips weekly

Follow

Support report

Checking…

–/ 20

Testing each feature in your browser…

  • Layout
  • Selectors
  • Visual Effects
  • Typography
  • Variables
  • Performance

Tested live with CSS.supports(). Nothing is looked up.

Layout: containers, grids and proportions

LayoutBaseline 2023

Container Queries

Respond to a container's own size instead of the viewport. Build truly portable components that adapt wherever they're placed.

Use it when
Components that live in more than one place: a card that sits in a narrow sidebar on one page and a wide grid on another, or a dashboard widget users can resize.
Watch out
Only an ancestor with container-type can be queried; an element can't query itself. container-type: inline-size also stops the container sizing itself from its content, so a shrink-to-fit container (a flex item with no width, say) can collapse to zero.
.wrapper {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 480px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1rem;
  }
}

LayoutBaseline 2023

CSS Grid Subgrid

Let nested grid items snap to the parent's tracks. Solves the classic 'cards with misaligned footers' problem without JavaScript.

Use it when
Rows of cards whose titles, bodies and footers should line up across cards, and forms where every label and input shares the same two columns.
Watch out
The child has to span the parent tracks it inherits (grid-row: span 3, for example), and it inherits the parent's gaps unless you set its own. Subgrid has worked in every major engine since late 2023.
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto 1fr auto;
  gap: 1rem;
}

.card {
  display: grid;
  grid-row: span 3;
  /* Inherit parent row sizes */
  grid-template-rows: subgrid;
}

LayoutWidely available

CSS Grid auto-fit vs auto-fill

auto-fit collapses empty tracks so columns stretch to fill space. auto-fill keeps them. Use auto-fit for responsive grids with zero media queries.

Use it when
Card and gallery grids where the number of columns should follow the space available, with no breakpoints at all.
Watch out
The two only differ when there are fewer items than fit in a row: auto-fill keeps the empty columns, auto-fit collapses them so items stretch. Write minmax(min(16rem, 100%), 1fr) so a single column can't overflow a narrow phone.
/* Columns stretch to fill — no media queries needed */
.responsive-grid {
  display: grid;
  grid-template-columns:
    repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.5rem;
}

/* Empty slots remain (useful for fixed layouts) */
.fixed-slots {
  grid-template-columns:
    repeat(auto-fill, minmax(260px, 1fr));
}

LayoutWidely available

aspect-ratio

Lock element proportions in one line. Replaces the old padding-top percentage hack entirely.

Use it when
Video embeds, image placeholders, avatars and media cards that should reserve their space before content loads, which also prevents layout shift.
Watch out
It's ignored when both width and height are set. It's also a preferred size: content taller than the ratio allows still grows the box, unless you add overflow: hidden or min-height: 0.
.embed   { aspect-ratio: 16 / 9; width: 100%; }
.avatar  { aspect-ratio: 1;       width: 3rem; }
.card-img { aspect-ratio: 4 / 3; }
.golden  { aspect-ratio: 1.618; }

/* Works with object-fit too */
img.cover {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  width: 100%;
}

LayoutWidely available

CSS Logical Properties

Use flow-relative props instead of directional ones. Layouts mirror automatically for RTL languages — no extra CSS needed.

Use it when
Any site that may be translated into Arabic, Hebrew or another right-to-left language, and components that should survive vertical writing modes.
Watch out
Shorthands like margin: 1rem 2rem are still physical; use margin-block and margin-inline instead. Mixing physical and logical properties on the same element makes overrides hard to follow.
/* Logical — works in any writing mode */
.card {
  margin-inline-start: 1rem;
  padding-block-start: 1.5rem;
  border-inline-end: 4px solid;
}

/* No [dir="rtl"] override needed — ever */

/* Shorthand */
.box {
  margin-inline: auto;   /* left & right */
  padding-block: 2rem;   /* top & bottom */
}

LayoutWidely available

grid-template-areas

Name your grid regions for visual, readable layout code. Rearranging the layout for different breakpoints is as easy as editing a string.

Use it when
Page shells and complex components with a handful of named regions that rearrange at breakpoints.
Watch out
Every row string needs the same number of cells and every area must be a rectangle, or the whole declaration is ignored. Use a dot (.) for an empty cell.
.page {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main  "
    "footer footer ";
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}

header { grid-area: header; }
aside  { grid-area: sidebar; }
main   { grid-area: main;   }
footer { grid-area: footer; }

Selectors: :has(), nesting and :is()

SelectorsBaseline 2023

:has() — The Parent Selector

Select an element based on its descendants. The most requested CSS feature in history — now universally supported.

Use it when
Styling a parent from what it contains: a field wrapper with an invalid input, a card that has an image, or the page while a dialog is open (body:has(dialog[open])).
Watch out
:has() can't be nested inside another :has(). Broad selectors like body:has(...) are re-checked on many DOM changes, so keep the subject and the argument as specific as you can on large pages.
/* Card layout shifts when it has an image */
.card:has(img) {
  display: grid;
  grid-template-columns: 120px 1fr;
}

/* Disable submit when form has invalid input */
form:has(input:invalid) .btn-submit {
  opacity: 0.5;
  pointer-events: none;
}

/* Dark nav when hero is visible */
:has(.hero:in-viewport) nav {
  color: white;
}

SelectorsBaseline 2024

CSS Nesting

Write nested rules natively — no Sass or PostCSS needed. Use & to reference the parent and keep related styles together.

Use it when
Component styles where states, children and media queries belong together, without reaching for Sass or PostCSS.
Watch out
Nested selectors behave like :is(), so specificity follows the most specific selector in the parent list, which can differ from what Sass compiles to. Early versions needed & before element selectors; current browsers don't.
.button {
  padding: .5rem 1rem;
  background: oklch(55% 0.18 85);

  &:hover {
    background: oklch(45% 0.18 85);
    translate: 0 -2px;
  }

  &.large {
    padding: .75rem 1.5rem;
    font-size: 1.125rem;
  }

  & .icon {
    margin-inline-end: .5rem;
  }
}

SelectorsWidely available

:is() and :where()

:is() groups selectors and keeps the highest specificity of the list. :where() does the same but with zero specificity — ideal for resets.

Use it when
:where() for resets and library defaults that should be trivial to override; :is() to shorten long, repetitive selector lists.
Watch out
:is() takes the specificity of its most specific argument, so :is(#nav, .link) a is as strong as an ID selector. Both are forgiving: one invalid selector in the list no longer drops the whole rule.
/* Clean with :is() */
h1 a, h2 a, h3 a, h4 a { color: inherit; }

/* Clean with :is() */
:is(h1, h2, h3, h4) a { color: inherit; }

/* :where() = zero specificity, easy to override */
:where(h1, h2, h3, h4) {
  line-height: 1.2;
  margin-block-end: .5em;
}

/* Combine both */
:is(article, section) :where(h2, h3) {
  font-size: clamp(1.25rem, 3vw, 2rem);
}

Visual effects: blur, colour and form controls

Visual EffectsWidely available

backdrop-filter

Apply blur, brightness, and saturation to whatever is behind an element. The foundation of modern glassmorphism UI.

Use it when
Frosted headers, menus and overlays that sit on top of photos or scrolling content.
Watch out
It's costly to repaint while scrolling on low-end phones. It also makes the element a containing block, so position: fixed children are placed relative to it, not the viewport. Keep a solid fallback background for contrast.
.glass {
  background: rgb(255 255 255 / 0.1);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgb(255 255 255 / 0.2);
  border-radius: 1rem;
}

.frosted-nav {
  backdrop-filter: blur(24px) brightness(0.85);
  background: rgb(0 0 0 / 0.25);
}

Visual EffectsBaseline 2023

color-mix()

Mix two colors in any color space directly in CSS. Generate tints, shades, and transparent variants from a single token.

Use it when
Hover, active and disabled states, tints and translucent variants generated from one brand colour token.
Watch out
The colour space changes the result: mixing in srgb can look muddy, while oklch or oklab keep lightness more even. Percentages that add up to less than 100% also make the result partly transparent.
:root {
  --brand: #ca8a04;

  /* 20% white = tint */
  --brand-light:
    color-mix(in oklch, var(--brand) 80%, white);

  /* 30% black = shade */
  --brand-dark:
    color-mix(in oklch, var(--brand) 70%, black);

  /* Transparent variant */
  --brand-alpha:
    color-mix(in srgb, var(--brand) 15%, transparent);
}

Visual EffectsWidely available

accent-color

Theme all native form controls — checkboxes, radios, range sliders, progress bars — with one line. No custom component needed.

Use it when
Matching native checkboxes, radios, range sliders and progress bars to your brand without rebuilding them as custom components.
Watch out
It only tints the control; size and shape stay native. Browsers may also adjust the colour to keep enough contrast. For fully custom controls you still need appearance: none and your own styles.
/* Global brand accent on all controls */
:root {
  accent-color: #ca8a04;
}

/* Or target specific elements */
input[type="checkbox"],
input[type="radio"] {
  accent-color: #ca8a04;
  width: 1.2rem;
  height: 1.2rem;
}

input[type="range"],
progress { accent-color: #ca8a04; }

Typography: fluid sizes and better line breaks

TypographyWidely available

clamp() — Fluid Typography

Fluid font sizes and spacing that scale smoothly between a min and max — no breakpoints, no JavaScript.

Use it when
Headings, section padding and grid gaps that should scale smoothly with the viewport between a minimum and a maximum.
Watch out
Keep a rem term in the middle value, as in clamp(1.5rem, 1rem + 2vw, 3rem). Pure vw ignores the reader's font-size setting and can fail WCAG's resize-text check.
/* clamp(minimum, preferred, maximum) */

h1 {
  /* 2rem on mobile → 4.5rem on wide screens */
  font-size: clamp(2rem, 5vw + 1rem, 4.5rem);
  line-height: clamp(1.1, 1.2 + 0.3vw, 1.35);
}

section {
  padding: clamp(2rem, 8vw, 6rem);
}

.container {
  width: min(90%, 1280px);
  margin-inline: auto;
}

TypographyBaseline 2024

text-wrap: balance & pretty

balance distributes words evenly across lines for headings. pretty prevents orphaned last words in paragraphs. Both with one property.

Use it when
balance for headings, captions and short pull quotes; pretty for body paragraphs where a single orphaned last word looks off.
Watch out
balance only works on short blocks (Chromium stops at 6 lines, Firefox at 10), so it does nothing on long text. pretty isn't Baseline yet; browsers without it just wrap normally, so it's safe to add today.
/* Even line lengths for headings */
h1, h2, h3 {
  text-wrap: balance;
  /* Browser limit: max 4 lines */
}

/* No orphaned words at end of paragraphs */
p, li {
  text-wrap: pretty;
}

/* Before: needed ugly hacks like this */
/* h1 { max-width: 28ch; } */

Cascade layers and custom properties

VariablesWidely available

Cascade Layers (@layer)

Control specificity at the layer level. Layers declared earlier always lose — no more specificity wars with third-party CSS.

Use it when
Keeping third-party CSS in a low layer so your own styles always win, and structuring a design system as reset, base, components and utilities.
Watch out
Styles outside any layer beat every layer. For !important declarations the order flips, so earlier layers win. Declare the order once at the top of your CSS: @layer reset, base, components;.
/* Declare order upfront — first = lowest priority */
@layer reset, base, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
}

@layer components {
  .btn { padding: .5rem 1rem; background: blue; }
}

/* Utilities always win regardless of specificity */
@layer utilities {
  .mt-4 { margin-top: 1rem !important; }
}

VariablesBaseline 2024

@property — Typed Custom Properties

Register CSS variables with a type and initial value. Unlocks smooth animation of properties that were previously unanimatable.

Use it when
Animating gradients, angles and colours held in custom properties, and giving design tokens a type the browser checks.
Watch out
The rule needs syntax and inherits, plus initial-value for any syntax except '*', or it's ignored. It only became Baseline in mid-2024, so treat the animation as an enhancement: older browsers jump straight to the end state.
@property --hue {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

.swatch {
  background: hsl(var(--hue), 70%, 55%);
  transition: --hue 0.6s ease;
}
.swatch:hover { --hue: 220; }

/* Also works for gradients! */
@property --stop {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

VariablesWidely available

CSS Custom Properties + calc()

Custom properties are more than static variables. Combine them with calc() to build a single-source-of-truth design system.

Use it when
Spacing and type scales, theme switching, and per-component overrides that all flow from one variable.
Watch out
An invalid value doesn't fall back to the previous declaration: the property resets to its inherited or initial value. var(--gap, 1rem) only helps when the variable isn't defined at all.
:root {
  --space: 0.25rem;
  --cols: 12;
  --gap: calc(var(--space) * 4);
  --radius-base: 0.5rem;
  --radius-lg: calc(var(--radius-base) * 2);
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: var(--gap);
}

@media (max-width: 768px) {
  :root { --cols: 4; }
}

Scrolling and rendering performance

PerformanceWidely available

Scroll Snap

Smooth scroll-stop points with zero JavaScript. Perfect for carousels, image galleries, and full-screen sections.

Use it when
Carousels, image galleries and horizontally scrolling card rows, especially on touch screens.
Watch out
mandatory can trap content that's taller than the viewport, so prefer proximity for vertical pages. Add scroll-padding-top when a fixed header would cover the snapped item.
/* Horizontal carousel */
.carousel {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  display: flex;
  gap: 1rem;
  scrollbar-width: none;
}
.slide {
  scroll-snap-align: start;
  flex: 0 0 300px;
}

/* Full-screen vertical sections */
.page {
  scroll-snap-type: y mandatory;
  height: 100vh;
  overflow-y: scroll;
}

PerformanceWidely available

overscroll-behavior

Stop scroll chaining — the UX bug where scrolling inside a modal also scrolls the page. One line, no JS needed.

Use it when
Modals, side drawers, chat panes and any nested scroll area that shouldn't drag the page along when it reaches its end.
Watch out
It only acts on an element that actually scrolls. On the root element it also turns off pull-to-refresh in Chrome on Android. contain keeps the element's own bounce or glow; none removes that too.
/* Modal: don't scroll the page when it ends */
.modal {
  overflow-y: auto;
  overscroll-behavior-y: contain;
}

/* PWA: disable pull-to-refresh */
body {
  overscroll-behavior-y: none;
}

/* Carousel: no horizontal bounce */
.carousel {
  overscroll-behavior-x: contain;
}

PerformanceBaseline 2024

content-visibility: auto

Tell the browser to skip rendering off-screen content entirely. Can cut initial render time by 50%+ on content-heavy pages.

Use it when
Long pages with many sections below the fold: documentation, feeds, comment threads and long-form articles.
Watch out
Pair it with contain-intrinsic-size (auto 500px, say), or skipped sections count as zero height and the scrollbar jumps as you scroll. Skipped content stays in the DOM and the accessibility tree, and find-in-page still reaches it.
.article-section {
  content-visibility: auto;

  /* Hint at rendered height to
     prevent layout shifts */
  contain-intrinsic-size: 0 500px;
}

/* Works great for long lists too */
.list-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 80px;
}

Keep all 20 tips offline

One PDF with every example, for your next code review.

Questions

What people ask before using modern CSS in production.

Are these CSS features safe to use in production in 2026?

Yes. Of the 20, 12 are widely available, 4 became Baseline in 2023 and 4 in 2024, so they work in current Chrome, Edge, Firefox and Safari. The one exception is text-wrap: pretty, which isn't in every browser yet. For it and the newest features, such as @property animations, write the CSS so the page still works without them.

Do I still need Sass?

Often not. Native nesting, custom properties, color-mix() and cascade layers cover most of what teams used Sass for. Sass still helps with loops, mixins and splitting styles into partials at build time.

Should I use container queries or media queries?

Both. Media queries suit page-level layout such as the site shell and navigation. Container queries suit components that get reused at different widths, because they respond to the space the component actually has.

How do I check whether a browser supports a CSS feature?

Use @supports in CSS, for example @supports (container-type: inline-size), or CSS.supports() in JavaScript. The support report on this page runs CSS.supports() in your browser. For your whole audience, check the Baseline status on MDN or caniuse.com.