The New Default. Your hub for building smart, fast, and sustainable AI software

See now
Practical CSS Guidelines for 2026: Architecture Patterns for Long-Lived Projects

Practical CSS Guidelines for 2026: Architecture Patterns for Long-Lived Projects

Monterail Team
|   Updated Jul 20, 2026

CSS guidelines are the shared rules a team agrees on for variables, specificity, selectors, architecture, and defaults that support accessibility in product design – the decisions that determine whether a stylesheet is still pleasant to work in three years from now, or has turned into a pile of overrides nobody wants to touch. 

Specificity discipline, sensible units, BEM-style naming, and accessibility-first defaults remain the backbone of maintainable CSS.

What's changed is the toolbox: native CSS nesting, cascade layers, the newer relational selectors, container queries, and a set of accessibility-related media features have all moved from "interesting but unsupported" to "just use this."

Teams are also increasingly pairing these native CSS capabilities with AI-assisted development to speed up how quickly a new architecture gets adopted across a codebase.

Each of these removes a workaround that used to shape how teams structured their CSS in the first place.

Executive Summary

CSS itself hasn't changed how it fundamentally works, but several additions over the past few years change how you should organize it. Native nesting, @layer, :is()/:where()/:has(), and container queries each remove a workaround that used to define a CSS architecture decision, so some of the "best practices" from a few years ago are now just defaults. The fundamentals haven't moved: keep specificity low and predictable, pick units deliberately, scope your components, and build accessibility into the defaults rather than bolting it on. The guidelines below are organized around one goal – a stylesheet a new team member can read, extend, and safely change, without archaeology

What's New in CSS (and Why It Matters for Architecture)

Native nesting, cascade layers, :is()/:where()/:has(), and container queries – all four are now safe to use without fallbacks. Each one removes a workaround that used to shape a CSS architecture decision on its own. Here's what each replaces and when to reach for it.

CSS Nesting

CSS Nesting lets you place selectors inside other selectors, just like in Sass, but directly in CSS and without a build step.

.card {

  padding: 1rem;

  border-radius: 0.5rem;

  & .card__title {

    font-size: 1.25rem;

    font-weight: 600;

  }

  &:hover {

    box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);

  }

}

  • What it solves: less repetition of the parent selector, and styles for a component stay visually grouped in the source.

  • When to use it: for a component's own variants, states, and direct children, the same cases where you'd already reach for Sass nesting.

  • What to watch for: nesting depth maps directly to specificity. Two or three levels deep is usually fine; beyond that, you're often looking at a sign that the block should be split into smaller components instead.

  • Maintenance impact: shallow, intentional nesting keeps a component's styles readable as a unit. Deep nesting produces the same specificity headaches Sass nesting always did – native CSS doesn't fix that, it just removes the compiler.

Cascade Layers (@layer)

Cascade layers let you group styles into named layers and control the order those layers are applied in, independent of specificity or source order.

@layer reset, base, components, utilities;

@layer reset {

 *, *::before, *::after {

    margin: 0;

    box-sizing: border-box;

  }

}

@layer components {

  .button {

    padding: 0.5rem 1rem;

    border-radius: 0.25rem;

  }

}

@layer utilities {

  .u-mt-0 {

    margin-top: 0 !important;

  }

}

  • What it solves: a styles from a later layer always beat an earlier layer, even if the earlier layer's selector is more specific. That means a one-class utility in the utilities layer can override a deeply nested component selector without !important and without a specificity arms race.

  • When to use it: define the layer order once, near the top of your stylesheet (or your main entry file), and assign third-party styles, resets, components, and utilities to layers as they're imported. This is also a good way to bring in a component library's styles without them fighting your own.

  • What to watch for: anything not in a layer is treated as if it's in its own layer that comes after all named layers, so unlayered styles will beat layered ones regardless of specificity. Be deliberate about what stays unlayered.

  • Maintenance impact: @layer makes override issues easier to debug. Instead of tracing specificity and source order, you can check the layer order in one place.

Selectors Level 4 :is(), :where(), and :has()

Three relational and matching selectors round out this group, each solving a different long-standing annoyance.

/* :is() and :where() group selectors without repeating them */

:is(.card, .panel, .tile) h2 {

  margin-bottom: 0.5rem;

}

/* :where() has zero specificity – ideal for resets and defaults

   you expect other rules to override easily */

:where(h1, h2, h3, h4) {

  font-weight: 600;

  line-height: 1.2;

}

/* :has() lets a parent react to its children's state */

.form-field:has(input:invalid) {

  border-color: var(--color-danger);

}

.card:has(img) {

  grid-template-columns: 200px 1fr;

}

  • What it solves: :is() and :where() collapse repetitive selector lists into one rule. :where() does this while contributing zero specificity, which makes it the right choice for resets and base styles you want to stay easy to override. :has() is the closest CSS has come to a "parent selector", styling an element based on what's inside it.

  • When to use it: :where() for resets and library defaults; :is() when grouping selectors that share specificity and you don't need the zero-specificity behavior; :has() for the small set of cases that used to require a JavaScript class toggle – form validation states, layout changes based on whether a card has an image, and similar conditional styling.

  • What to watch for: :has() is powerful enough to be tempting for things that are really a component-state problem. If you find yourself writing :has() chains to recreate what a data-state attribute and a class would express more clearly, the attribute is usually the better long-term choice.

  • Maintenance impact: :where() in particular is worth adopting in any shared reset or design-system base layer, because it means consuming projects never have to fight your defaults with !important or extra specificity.

Feature

What it does

Reach for it when

Avoid when

Nesting

Write child/state selectors inside a parent rule

Styling a component's own variants and states

Nesting depth exceeds 2–3 levels

@layer

Sets explicit priority order between groups of styles

Combining resets, components, utilities, and third-party CSS

Fine per-rule control is needed, not group-level

:where()

Groups selectors with zero added specificity

Resets, base styles, design-system defaults

Specificity from this selector needs to apply

:is()

Groups selectors, keeps normal specificity

Shortening repetitive selector lists

Mixing very different specificities in the group

:has()

Styles a parent based on its descendants

Form validation states, conditional component layout

The condition is really a component state better expressed with a class

A useful check for older codebases is to look for resets and scattered !important rules across multiple files. That usually signals unclear ordering – the exact problem @layer is designed to solve.

If a project still depends on !important to override earlier styles, @layer is often one of the first improvements worth making.

CSS Custom Properties or Sass Variables – Which To Use for Design Tokens?

Most projects need both, for different purposes: CSS custom properties for values that live in the browser and can change at runtime, Sass variables for anything needed only at compile time. Treating that split as a deliberate choice pays off as a codebase grows.

The values worth extracting into variables in the first place are colors, dimensions, font sizes, breakpoints, and z-index values. Pulling these out makes refactoring easier and keeps the same values consistent everywhere they're used.

On z-index specifically: keeping a single ordered list of every z-index value in the project removes the guesswork about what should sit above what.

Sass Variables vs. CSS Custom Properties: 3 Key Differences

Sass Variables

CSS Custom Properties

When it exists

Compile-time only – the variable name disappears once Sass builds the CSS

Lives in the browser at runtime – inspectable directly in dev tools

Syntax

color: $text-gray;

color: var(--text-gray);

Media query breakpoints

Works – Sass variables can be used inside @media rules

Doesn't work – @media (min-width: var(--breakpoint-sm)) isn't valid CSS, and this isn't expected to change

What CSS custom properties can do that Sass variables can't:

CSS custom properties have a few abilities with no Sass equivalent at all:

  1. They cascade – override one on any selector, and that element's children pick up the new value. This is what makes them useful for application-wide theming.

  2. They work inside calc().

  3. They can power configurable components via fallback values:

css

.button {

  /* Use --color-primary, or fall back to a default if it's not defined */

  background-color: var(--color-primary, #c0ffee);

}

  1. Their values can be read and changed directly from JavaScript:

css

#grid {

  display: grid;

  grid-template-columns: repeat(var(--columns), 1fr);

  --columns: 5;

}

js

const grid = document.querySelector("#grid");

// Set the number of columns to 10

grid.style.setProperty("--columns", 10);

How Do Build-Time Tokens and Runtime CSS Variables Work Together?

Build-time tools like Sass (or a JS/JSON config) own breakpoints and other static values; CSS custom properties take everything that changes at runtime – theming, spacing, component state, anything JavaScript needs to read or write. This hybrid split is standard in most long-lived projects, and it exists because of one hard limit: CSS custom properties can't define media query breakpoints, so that piece has to live somewhere else.

Instead of duplicating whole rule blocks inside media queries, let the media query change only the value of a custom property, and let the rest of the CSS reference that property unconditionally.

:root {

  --container-padding: 1rem;

}

@media (min-width: 768px) {

  :root {

    --container-padding: 2rem;

  }

}

.container {

  padding: var(--container-padding);

}

This keeps the responsive logic in one place (the media query) and the application of that logic in the component, rather than repeating the component's rules at every breakpoint.

Maintenance impact: this split is what lets a design system define tokens once and have both the build pipeline (breakpoints, static config) and the running application (themes, dynamic spacing, JS-driven state) consume the same source of truth without fighting CSS's actual limitations.

What Is CSS Specificity, and Why You Should Keep It Low

Specificity is the set of rules browsers use to decide which of several conflicting style declarations wins. Best practice is to keep yours as low as possible, so overriding it stays easy later on.

A high-specificity selector can only be overridden by one that's equal or higher and appears later in the source, which gets messy fast. Cascade layers (@layer) sidestep that entirely, giving you an explicit ordering axis instead of relying on the cascade's default rules.

This comes up constantly when adapting a third-party library or component to fit your application's styles, where you need to override some of its rules while keeping others as a base.

Five CSS specificity best practices to keep it low:

  1. Within the same specificity level, browsers prioritize whatever appears later in the source. The less specific a rule, the earlier in the stylesheet it should generally appear.

  2. Avoid nesting selectors for your own styles. If you do nest, pick a reasonable maximum depth (one or two levels) and stick to it. 

  3. Avoid !important unless there is no practical alternative. In many cases, @layer provides a clearer way to control which styles take priority.

  4. Don't style id selectors. Stick to classes or elements.

  5. Be cautious with bare element selectors. They have low specificity, but they're also very broad. It's easy to style more than intended, which then needs overriding again on more specific selectors.

What Are the Main Types of CSS Selectors?

Element (div), class (.footer), and ID (#footer) selectors are the basic building blocks – and mixing them well means writing less code overall. They differ in specificity, covered in the section above; here's what each is actually for.

The html and body selectors are the right place for base fonts, colors, and backgrounds. :root usually targets the same element as html, but has higher specificity, which can matter when debugging.

:is(), :where(), and :has() build on these basics by making grouping and more advanced targeting easier.

Pseudo-classes

Pseudo-classes (:hover, :first-child, :last-child, and many others) carry the specificity of a class. They're commonly used to style elements based on state – hovered, focused, an input being invalid or disabled, a checkbox being checked, and so on. 

The full list of pseudo classes is longer than most people expect, and worth browsing. Custom checkbox and form styling in particular gets much easier once you know what's available.

Pseudo-elements

Pseudo-elements (::first-line, ::first-letter, an input's ::placeholder) let you style a specific part of an element, and carry the specificity of an element selector. 

The list of pseudo-elements is shorter, but ::before and ::after alone cover a lot of ground – custom list markers, icons before or after text, notification dots, and similar small additions.

::before and :before (single colon) both work for historical reasons, but the double-colon syntax is the one to use going forward

Sibling Selectors

Sibling selectors define relationships between elements at the same level, which comes up often when styling lists.

.a + .b styles a .b element that immediately follows an .a element in the DOM. For list items, .a + .a targets every item except the first – useful for spacing between items without an extra wrapper:

.list-item + .list-item {

  margin-top: 20px;

}

/* or, with Sass nesting */

.list-item {

  & + & {

    margin-top: 20px;

  }

}

The less common .a ~ .b styles any .b that comes after an .a, regardless of how many siblings sit between them; useful when one element's state needs to affect any number of elements after it.

What's the Right Cross-Browser Support Strategy in 2026?

The right cross-browser support strategy in 2026 looks less like "write extra code for older browsers" and more like "decide deliberately what your baseline is, and let tooling handle the rest."

A practical workflow:

  1. Check support first – Caniuse tells you how broadly supported a feature is, in which browser versions, and with what caveats – check before you build, not after something breaks in QA.

  2. Default to progressive enhancement. Build the core experience to work with broad support, then layer on enhancements for browsers that support them, using @supports feature queries where the difference matters:

.gallery {

  display: flex;

  flex-wrap: wrap;

}

@supports (display: grid) {

  .gallery {

    display: grid;

    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

  }

}

  1. Let Autoprefixer (via PostCSS) handle vendor prefixes. This is a build-step concern, not something to think about while writing styles – configure your target browsers once and let the tooling add -webkit-, -moz-, and similar prefixes only where they're still needed.

  2. Use a modern CSS reset (or a tool like normalize.css) as a common starting point across browsers, ideally placed in its own @layer reset so it's easy to override.

  3. Treat very old browsers as out of scope by default. For most projects, support for browsers like Internet Explorer is no longer a realistic requirement, and designing around it costs more than it returns. The exception is specific contractual or institutional requirements.

  4. Test in more than one browser. Differences in forms, flexbox, and font rendering can still appear even when automated checks pass.

Maintenance impact: a project that defines its browser baseline explicitly, and pushes prefixing and fallbacks into tooling, spends far less time on cross-browser bugs than one that handles them ad hoc as they're reported.

Which CSS Units Should You Use and When?

You'll reach for a handful of these regularly – px, %, em/rem, fr for grids, and vw/vh  for viewport-relative sizing – while most other absolute units (mm, in, cm, pt, pc) stay print-oriented, and narrow-purpose ones like ex and ch rarely come up at all.

Absolute Units

Absolute units have a fixed length: cm, mm, in, pt, pc, and px. The px unit is a slight exception – on low-DPI screens it maps to one device pixel, but on high-DPI screens or in print, one px can correspond to multiple device pixels. Fractional pixel values (5.5px) technically work but should generally be avoided, since browsers may round them differently.

Relative Units

Relative units are sized based on something else: em, ex, ch, rem, fr, vw, vh, vmin, vmax, and %.

The Ones That Matter Most

In practice, you'll mostly use:

  • px

  • % (relative to the parent)

  • em and rem

  • fr (a fraction of a grid)

  • vw, vh (less often vmin, vmax)

px is the right choice for things that shouldn't scale with font-size preferences: border widths, box-shadow offsets, and similar fine details.

% looks intuitive but is relative to a different property depending on context – for width or height, it's a percentage of the parent's width; for font-size, it's a percentage of the parent's font-size.

em is relative to the parent's font-size, which makes it powerful but also easy to lose track of as it compounds down the tree. Use it where the cascading relationship is actually useful, not as a default.

rem is relative to the root (html) element's font-size, which makes it the right choice for anything that should scale with a user's font-size preference.

fr sizes grid tracks as a fraction of the available space (2fr = 2 / total fractions of the grid's width).

vw and vh are relative to viewport width and height, useful for things like a full-viewport hero section, but pair them with min-width/min-height to avoid breaking on unusual aspect ratios. For fluid typography and spacing specifically, clamp(), covered under ‘How Do You Build Responsive Layouts in CSS?’, handles scaling between a minimum and maximum without extra breakpoints.

vmin and vmax refer to the smaller or larger of the two viewport dimensions – 40vmin is 40% of whichever of width/height is smaller.

PX vs. REM

Use rem where you can. If you can't, be aware of what you're giving up.

rem respects the user's font-size preference – if someone increases their browser's default font size, everything sized in rem scales accordingly. px ignores this entirely, which means a user who needs larger text gets none of the benefit unless they zoom the whole page.

Making REM Less Awkward

Because rem is based on the root font-size (16px by default in most browsers, though users can change it), converting design values to rem by hand is tedious – 40px becomes 2.5rem, 1px becomes 0.0625rem, and so on.

A common trick:

html {

  font-size: 62.5%; /* 16px * 62.5% = 10px, so 1rem = 10px */

}

body {

  font-size: 1.6rem; /* 16px */

}

This respects the user's font-size preference (everything still scales from their default) while making the math easier – 1rem now equals 10px at the default size. Don't skip the body rule; it's what makes the default value cascade correctly to everything else.

One caveat: media queries don't pick up this adjustment automatically. rem values inside @media rules are still based on the browser's actual 16px default, not your adjusted root size.

How Do You Build Responsive Layouts in CSS?

Six assumptions cover most of it, plus two newer tools, container queries and clamp(), that reduce how much of this needs to live in media queries at all.

Core Rules for Responsive CSS:

  1. Avoid fixed sizes, especially width or height. Prefer max-width: 250px; width: 100% over width: 250px, so an element can shrink gracefully when space is limited. This matters most for images and video.

  2. Pick a reasonable minimum supported device width (320px or 375px are common choices) and design for that width and everything larger.

  3. When element order changes between layouts, use the order property on flex/grid children, or named grid areas with a different grid-template per breakpoint, rather than duplicating HTML or toggling visibility.

  4. Settle on a small, fixed set of breakpoints (three or four is usually enough) and stick to them.

  5. Make sure interactive elements are easy to tap, responsive and work well on mobile devices – this doesn't require making them visually bigger; padding or an absolutely-positioned hit area works just as well.

  6. @media (hover: hover) lets you check whether the current device supports hover at all, which is useful for adjusting hover-dependent interactions.

Container Queries

Container queries (@container) let an element respond to the size of its container, rather than the size of the viewport.

.pricing-card-container {

  container-type: inline-size;

  container-name: card;

}

@container card (min-width: 400px) {

  .pricing-card {

    display: grid;

    grid-template-columns: 1fr 1fr;

    gap: 1rem;

  }

}

  • What it solves: a component like a pricing card or product tile often needs to look different depending on how much space it's given – full-width in a single-column layout, narrower inside a sidebar, side-by-side in a grid. Before container queries, getting this right meant either a viewport breakpoint for every possible placement, or JavaScript measuring the container and toggling a class.

  • When to use it: any component that's reused in multiple layout contexts and needs to adapt to its own available width. Container queries don't replace viewport-level layout decisions: page structure, navigation, and overall grid still respond to the viewport.

  • What to watch for: a container needs a container-type set on an ancestor before @container rules can target it, and that ancestor becomes a new containment context (which has some layout implications of its own, similar to how overflow or transform create containing blocks).

  • Maintenance impact: components that carry their own responsive rules via container queries can be dropped into a new layout such as a sidebar, a modal, a different grid column count, and adapt correctly without anyone updating a breakpoint list somewhere else in the codebase.

Fluid Typography and Layout with clamp()

clamp(min, preferred, max) sets a value that scales smoothly between a minimum and maximum, based on the preferred value, typically expressed with viewport units.

h1 {

  font-size: clamp(1.75rem, 1.25rem + 2vw, 3rem);

}

.container {

  padding: clamp(1rem, 2vw, 3rem);

}

  • What it solves: instead of jumping between fixed font sizes or spacing values at each breakpoint, the value scales continuously with the viewport, within bounds you control.

  • When to use it: headings, hero text, and layout spacing that should feel proportional to the viewport without becoming unreadably small or overly large.

  • What to watch for: clamp() reduces how many media queries you need for typography and spacing. Layout structure (column counts, navigation patterns, what's visible at all) still generally needs explicit breakpoints.

  • Maintenance impact: fluid values defined with clamp() mean fewer places in the stylesheet where a "magic number at this breakpoint" needs to be revisited when a design's scale changes.

BEM, Utility-First, or Design Tokens: How To Structure CSS Architecture?

Most mature projects combine all three, drawing on an abundance of frameworks and approaches: BEM for scoping and readability, a utility layer for fast, low-risk styling, and design tokens underneath both to keep values consistent. Each solves a different problem – BEM prevents class names from clashing and styles leaking where they shouldn't; utility classes trade some readability for speed; tokens keep colors, spacing, and type consistent no matter which of the other two you're using.

BEM

BEM (Block-Element-Modifier) names classes to make both scoping and the relationships between elements explicit. It also tends to keep specificity low, at the cost of longer class names. This approach keeps styles scoped and explicit about how they relate – the same instinct that matters when you scope a web app redesign.

A few practical notes on keeping BEM manageable:

  1. With Sass, the & operator saves a lot of typing:

.button {

  &__icon { } // .button__icon

  &--primary { } // .button--primary

  &--primary &__icon { } // .button--primary .button__icon

}

  1. Don't nest BEM names to match the DOM tree. foo__bar__baz being inside foo__bar in the HTML doesn't mean the class name needs that nesting – keep it flat:

// Avoid

.foo {

  &__bar {

    &__baz { }

  }

}

// Prefer

.foo {

  &__bar { }

  &__baz { }

}

If there's a relationship that feels like it needs nesting, that's often a sign the nested part deserves to be its own block.

// GOOD

.foo { }

.bar {

  &__baz { }

}

  1. Consider applying modifiers only at the block level, rather than on individual elements – it keeps the Sass simpler and avoids repeating modifier logic per element:

.foo {

  &__bar { }

  &__bar--active { } // .foo__bar--active

}

// vs.

.foo {

  &__bar { }

  &--active &__bar { } // .foo--active .foo__bar – higher specificity, shorter names

}

Other naming conventions exist (often using prefixes to create namespaces), but BEM is common enough that most of what applies to it applies to those too.

Utility-First / Atomic CSS

The alternative end of the spectrum: small, single-purpose classes (.flex, .mt-4, .text-center) composed directly in markup. 

This trades component-level naming for speed. New UI can be built without writing new CSS at all The cost is more cognitive load when reading the HTML, since the styling logic lives in the markup rather than in named, documented components.

Design Tokens

Design tokens (the colors, spacing, typography scale, and other values defined as CSS custom properties) sit underneath both approaches. 

Whether a project uses BEM, utility classes, or both, consistent tokens are what keep the visual language coherent across them.

Comparing the Approaches

Approach

Solves

Trade-off

Best fit

BEM

Scoping, explicit component relationships

Longer class names, some duplication

Component libraries, design systems, teams that value self-documenting CSS

Utility-first

Speed, consistency of small values

Verbose markup, less self-documenting

Rapid prototyping, projects where the team is fluent in the utility set

Design tokens (custom properties)

Consistent values across the whole system

Needs a defined set of tokens up front

Every project; this isn't really an alternative to the other two

CSS-in-JS and Framework Scoping

Most CSS-in-JS solutions, and framework features like Vue's scoped attribute on single-file components, handle scoping automatically. 

For genuinely complex components, BEM-style naming can still add clarity even within a scoped stylesheet – scoping solves collisions, not readability.

What's the Default CSS Architecture Stack Worth Starting With?

The combination that tends to age best – for a SaaS app, an internal platform, or anything with a growing component library – is BEM-style component naming, a small utility layer for spacing and alignment, and design tokens via custom properties for everything else, with @layer used to make the priority between these three explicit (@layer reset, tokens, components, utilities).

Always consider the project-specific context – a small marketing site, or a project already standardized on a utility framework, may reasonably choose differently. As a default, though, this combination balances the readability of named components with the speed of utilities, without leaving either to fight the other for specificity.

Accessibility / a11y

Accessibility is a big topic, but the general rule of thumb: avoid making an interface inaccessible by accident, or in service of how something looks.

  1. Don't remove focus styles without providing a real alternative. The default outline might not match your design, but making focused elements look good is the team's responsibility.

  2. Respect user preferences by using relative units, so the whole interface scales with a user's preferred font size.

  3. Keep font sizes sensible enough to read comfortably across devices. Browsers default to 16px for a reason (for example, an input with a font size below 16px on iOS will cause Safari to zoom in on focus, which is rarely the intended behavior).

  4. Follow WCAG color contrast guidelines, and keep the number of exceptions low – at minimum, avoid low-contrast text on large blocks of content. Browser dev tools can show you the contrast ratio for any selector directly.

  5. Understand the difference between hiding content visually, hiding it from assistive technology, and removing it entirely – the a11y project's article on the topic covers this well.

Respecting User Preferences via Media Features

Alongside the points above, a small set of media features let your CSS respond directly to preferences a user has set at the OS or browser level. prefers-reduced-motion maps directly to a named WCAG technique (C39, supporting the "Animation from Interactions" criterion), while prefers-color-scheme and prefers-contrast aren't called out by number in WCAG but support its broader contrast and readability guidance. 

In practice, all three are now standard in accessibility audits and procurement checklists, not optional extras.

Media feature

Responds to

Typical use

prefers-reduced-motion

The user’s reduced-motion preference

Disable or shorten animations and transitions

prefers-color-scheme

The user’s light or dark mode preference

Provide light and dark theme tokens

prefers-contrast

The user’s contrast preference

Increase text and border contrast

css@media (prefers-reduced-motion: reduce) {

*, *::before, *::after {

    animation-duration: 0.01ms !important;

    animation-iteration-count: 1 !important;

    transition-duration: 0.01ms !important;

  }

}

@media (prefers-color-scheme: dark) {

  :root {

    --color-bg: #121212;

    --color-text: #f5f5f5;

  }

}

@media (prefers-contrast: more) {

  :root {

    --color-border: #000000;

    --color-text: #000000;

  }

}

These media features work well with custom properties. Components use design tokens, and the media query updates the token values instead of rewriting entire rule blocks.

Other CSS Practices Worth Knowing

CSS Math Functions

min(), max(), clamp(), and calc() are all widely supported and worth using directly – calc() in particular for any value that's "this, plus or minus that." clamp() in particular is worth pairing with fluid typography and spacing – see 'How Do You Build Responsive Layouts in CSS?' for the full pattern.

Comment Your Hacks and Magic Numbers

Every project has a property with a value that doesn't seem to follow from anything – a negative margin, half a pixel, a number that "just works." These values make sense at the time they're written and become a mystery a few months later. Multiply that across a codebase, and you get working with legacy code that's incredibly difficult to untangle.

A short comment explaining the reasoning – what the value is for, and ideally how it was derived – saves whoever encounters it next from re-deriving it from scratch, and makes it much easier to revisit if the surrounding layout changes. 

Where the value comes from a calculation, calc() can often make that reasoning explicit in the code itself.

Avoid Generic Shorthand Properties Where They Cause Surprises

Shorthand properties like background set several sub-properties at once, even when you only meant to change one of them.

.foo {

  background-color: blue;

  background-image: url("https://foo.bar/image.png");

  background-repeat: no-repeat;

  background-size: cover;

}

.foo--bar {

  /* This resets background-image, background-repeat, and

     background-size to their defaults – not just the color */

  background: red;

}

When a higher-specificity rule only needs to change one part of a shorthand property, use the specific longhand property instead, so the override doesn't silently reset everything else.

Global Styles

Some styles genuinely don't belong to a single component – base typography, link styles, and similar. The rule for placing them is the same as for specificity generally: the less specific a rule, the earlier it should appear (or, with @layer, the earlier layer it should belong to). 

In a Vue project, for example, import global styles early in the main entry file so component styles can override them naturally.

Utility Classes

Small utility classes, such as one for text-align: center, reduce repetition for styles used throughout a project.

See the ‘BEM, Utility-First, or Design Tokens – How To Structure CSS Architecture?’ section for how utility classes fit alongside BEM and design tokens. In practice, follow these guidelines:

  1. One class, one purpose.

  2. Name the class after what it does (text-align-center needs no lookup).

  3. Import utility classes last, so they have higher specificity by default – or place them in a dedicated @layer utilities that comes last in your layer order.

  4. !important is one of the few places it's defensible to use it for utility classes specifically, since the point is to guarantee they win.

  5. A prefix (u-text-align-center) helps distinguish utilities from component classes at a glance.

Box Sizing

box-sizing has three possible values, and the choice affects how intuitive your width/height math is:

  1. content-box (the default): width/height apply only to content, excluding padding and border. A 40px-tall element with a 1px border needs height: 38px to render at 40px total, which rarely matches how a design was specified.

  2. padding-box: width/height include padding, but not border.

  3. border-box: width/height include both padding and border, the values you set match what a design tool shows.

Setting border-box as the project-wide default, while still allowing overrides where needed:

html {

  box-sizing: border-box;

}

*, *::before, *::after {

  box-sizing: inherit;

}

Key Takeaways

  • Most of the CSS additions that matter for 2026 remove workarounds, which means specificity discipline and deliberate unit choices still matter.

  • @layer gives you a second axis of control over the cascade, independent of specificity and source order. It's a clean way to formalize the reset/base/components/utilities structure most projects already have informally.

  • Container queries solve the "this component needs its own breakpoints" problem that used to require either viewport hacks or JavaScript measurement, but they don’t replace viewport-level layout decisions.

  • CSS custom properties still can't define media query breakpoints. The practical pattern is build-time variables (Sass, JS config) for breakpoints, and custom properties for theming, spacing, and runtime state.

  • Accessibility media features are now standard in accessibility audits, and once your values are tokens, supporting them is cheap.

How To Put These CSS Guidelines Into Practice

There's still no single guidebook that covers every project equally well – the right call depends on the product, the team, and how long the code is expected to live. What's new in the 2026 guide is that some decisions that used to require trade-offs now have a native, supported answer: nesting instead of a Sass dependency, @layer instead of !important, container queries instead of viewport-breakpoint sprawl for reusable components.

A stylesheet's complexity should track the complexity of the product. Treat this as a baseline for your own project's standard, and revisit it as the parts of CSS that are "new" today settle into being just how things are done.

If anything here doesn't match how your team works, or you've found a pattern that's served you better, we'd be glad to hear about it.

CSS Guidelines FAQ (2026)

Learn from various Monterail experts on our blog. Monterail is a full-service agentic development company with 150+ experts. The company delivers custom web and mobile applications for fintech, proptech, healthtech, and eCommerce. Since 2010, Monterail has completed 900+ projects globally. Monterail works with recognized global brands including Bosch, DocPlanner, EY, Merck, and SharkNinja. Monterail was recognized by Deloitte and the Financial Times.