Modern CSS can handle more of a component's layout, responsive behavior, and state styling without a large utility framework or a JavaScript resize listener. The challenge is knowing which features solve a real problem, what their fallback looks like, and where accessibility still needs explicit attention.
This guide covers practical CSS techniques that are useful in a small website or application: custom properties and cascade layers, fluid sizing with clamp(), container queries, logical properties, native nesting, :has(), and subgrid. The examples use a responsive card grid because it shows how the techniques work together, but each feature can be adopted independently.
Start with a resilient baseline
Modern CSS should enhance a working layout rather than make the layout depend on one new feature. Use ordinary grid or flexbox for the first version, then add a container query or subgrid when it improves the component.
Here is the HTML used in the examples:
<main class="page-shell">
<header class="intro">
<p class="eyebrow">Project resources</p>
<h1>Choose a practical starting point</h1>
<p>Short descriptions help readers decide what to open next.</p>
</header>
<section class="card-list" aria-labelledby="resources-heading">
<h2 id="resources-heading" class="visually-hidden">Resources</h2>
<article class="resource-card">
<p class="resource-card__type">Guide</p>
<h3>Choose a responsive CSS framework</h3>
<p>Compare the tradeoffs before adding a layout dependency to your project.</p>
<a href="/responsive-css-frameworks/">Compare the options</a>
</article>
<article class="resource-card">
<p class="resource-card__type">Tutorial</p>
<h3>Make a layout responsive</h3>
<p>Use flexible tracks and content-based breakpoints instead of device lists.</p>
<a href="/why-responsive-design-website/">Learn the approach</a>
</article>
</section>
</main>
The visually-hidden heading gives the section an accessible name without adding another visible heading. The link text is also meaningful when read out of context.
1. Use custom properties and cascade layers as design primitives
Custom properties let you keep recurring values in one place and change them at a component boundary. Cascade layers let you make the intended order of groups of rules explicit instead of trying to win every conflict with selector specificity.
@layer reset, base, components, utilities;
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
}
}
@layer base {
:root {
--color-surface: #ffffff;
--color-page: #f4f7fb;
--color-text: #172033;
--color-muted: #536078;
--color-accent: #175cd3;
--border-radius: 0.75rem;
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
}
body {
background: var(--color-page);
color: var(--color-text);
font-family: system-ui, sans-serif;
line-height: 1.5;
}
}
@layer components {
.resource-card {
background: var(--color-surface);
border: 1px solid #d9e0ec;
border-radius: var(--border-radius);
padding: var(--space-3);
}
.resource-card a {
color: var(--color-accent);
font-weight: 700;
}
}
@layer utilities {
.visually-hidden {
block-size: 1px;
clip-path: inset(50%);
inline-size: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
}
This is useful for a solo developer because a component can consume shared tokens without copying values through many files. A later layer can override an earlier layer even when its selector is less specific. Do not put every rule in a new layer: a small, documented layer order is easier to maintain than a long list of exceptions.
Custom properties inherit by default. That is useful for themes, but it can also make a value leak into a nested component. Define a property closer to the component when it should not inherit, and test the component in both its normal and exceptional contexts. See the MDN guide to custom properties and the MDN guide to cascade layers.
2. Replace many breakpoint values with fluid sizing
clamp(minimum, preferred, maximum) lets a value grow with the available space while keeping a lower and upper bound. It is useful for type, spacing, and sometimes component dimensions.
.page-shell {
inline-size: min(100% - 2rem, 72rem);
margin-inline: auto;
padding-block: clamp(2rem, 5vw, 5rem);
}
.intro h1 {
font-size: clamp(2rem, 1.25rem + 3vw, 4rem);
line-height: 1.05;
max-inline-size: 12ch;
}
.intro p {
color: var(--color-muted);
font-size: clamp(1rem, 0.95rem + 0.3vw, 1.2rem);
max-inline-size: 60ch;
}
The first value in clamp() is the minimum, the middle value is the preferred calculation, and the last value is the maximum. The min() expression keeps the shell from becoming wider than the viewport after its outer padding is accounted for.
Fluid sizing does not remove the need to check zoom and text resizing. A very large heading can create awkward wrapping, and a preferred value based on viewport width may not respond to a narrow component inside a wide page. Keep line lengths reasonable, test at high text zoom, and use a container query when the component should respond to its parent rather than to the viewport.
The MDN reference for clamp() documents its value behavior and browser compatibility.
3. Use container queries for reusable components
Media queries ask how large the viewport is. Container queries ask how large a component's containing block is. That distinction matters when the same card, toolbar, or form appears in a sidebar and in the main content area.
.card-list {
container: resource-cards / inline-size;
display: grid;
gap: var(--space-2);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.resource-card {
display: grid;
gap: var(--space-1);
}
@container resource-cards (min-width: 42rem) {
.resource-card {
grid-template-columns: 1fr auto;
}
.resource-card h3,
.resource-card > p:not(.resource-card__type) {
grid-column: 1 / -1;
}
}
The container shorthand names the container and establishes an inline-size query container. The grid still works if a browser does not apply the @container block, so the fallback is a single-column card layout within each auto-fit track.
Container queries are a good fit for design-system components and embeddable widgets. They are not a replacement for every media query: page-level navigation, global spacing, and print rules may still depend on the viewport. Read the MDN container queries guide for the containment rules and supported query features.
4. Use logical properties for writing-mode and direction support
Physical properties such as margin-left and padding-top describe one screen orientation. Logical properties describe the flow of the content: inline is the text direction, and block is the direction in which lines stack.
.resource-card {
margin-block: 0;
padding-block: var(--space-3);
padding-inline: var(--space-2);
border-inline-start: 0.25rem solid var(--color-accent);
}
.resource-card__type {
margin-block: 0 var(--space-1);
}
In a left-to-right horizontal writing mode, padding-inline looks like left and right padding and border-inline-start is the left border. In a right-to-left or vertical writing mode, the browser maps those properties to the appropriate physical sides. This avoids duplicating selectors for a second language direction.
Logical properties do not translate the meaning of an icon, image, or manually positioned decoration. Check those elements separately, and set an explicit direction or writing-mode only when the component genuinely requires it. The MDN logical properties guide lists the physical-to-logical mappings.
5. Use native nesting to keep component styles together
Native CSS nesting lets related selectors live inside their component rule. The & represents the parent selector, which is useful for pseudo-classes and for building a descendant selector.
.resource-card {
transition: border-color 160ms ease, box-shadow 160ms ease;
& h3 {
margin-block: 0;
}
& a {
text-decoration-thickness: 0.12em;
text-underline-offset: 0.15em;
}
&:hover,
&:has(a:focus-visible) {
border-color: var(--color-accent);
box-shadow: 0 0.5rem 1.5rem rgb(23 92 211 / 15%);
}
}
Keep nesting shallow. Deeply nested rules still create hard-to-predict selectors, and a component can accidentally become coupled to the markup beneath it. Do not use a hover-only style to communicate an important state: the :has(a:focus-visible) branch makes keyboard focus visible on the whole card as well.
If the project must support browsers without native nesting, put the essential base styles outside a nesting block, use a build step that your project already supports, or provide a separate fallback. The MDN CSS nesting guide explains the nesting syntax and compatibility details.
6. Use :has() for parent-aware state styling
CSS normally selects an element and its descendants. The relational pseudo-class :has() can select an element when a relative selector matches inside it.
.resource-card {
border-color: #d9e0ec;
}
.resource-card:has(a:focus-visible) {
border-color: var(--color-accent);
outline: 0.2rem solid rgb(23 92 211 / 25%);
outline-offset: 0.15rem;
}
.form-row:has(input[aria-invalid="true"]) label {
color: #a12a2a;
}
The first rule makes the focus state visible at the card level without JavaScript. The form example can reinforce an application validation state when the input has aria-invalid="true", but it should not be the only error message: keep a useful text error associated with the input and do not rely on red color alone.
Use :has() for state enhancement rather than for the only way a user can understand or operate a control. If the selector is unsupported, the base rule should remain usable. The MDN :has() reference includes selector restrictions and browser compatibility.
7. Use subgrid when repeated content must align
A grid item normally has its own row sizing. With subgrid, a child can participate in the parent grid's tracks. This is helpful for cards whose titles or actions should line up even when their descriptions have different lengths.
.card-list {
align-items: stretch;
display: grid;
gap: var(--space-2);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.resource-card {
display: grid;
grid-template-rows: auto 1fr auto;
}
@supports (grid-template-rows: subgrid) {
.resource-card {
grid-row: span 3;
grid-template-rows: subgrid;
}
}
The regular auto 1fr auto layout is a useful fallback. In the enhanced version, the card spans three rows from the parent grid and inherits those row tracks, so the card type, content, and action can align across neighboring cards. This works only when the child is actually placed in a parent grid with the rows it is trying to inherit.
Do not add subgrid simply because it is new. Use it when alignment is part of the design requirement; otherwise, independent card rows are simpler and more tolerant of unusual content. See the MDN subgrid guide for how inherited tracks work.
Browser support and progressive enhancement
Support is a project decision, not a vague label such as “modern browser.” Check your actual browser matrix and the MDN compatibility table for each feature before removing a fallback.
| Technique | Practical fallback |
|---|---|
Custom properties and clamp() | Provide a fixed value before the custom or fluid declaration. |
| Container queries | Use a flexible grid or flex layout that remains usable without the query block. |
| Logical properties | Use them for new components; keep a physical-property fallback only when an older target requires it. |
| Native nesting | Keep essential rules un-nested or compile CSS through the project's existing build process. |
:has() | Style the control itself and preserve normal focus and validation behavior. |
subgrid | Use ordinary grid tracks or independent card rows. |
Feature queries can guard an enhancement:
.resource-card {
padding: 1rem;
}
@supports (container-type: inline-size) {
.card-list {
container-type: inline-size;
}
}
@supports checks whether the browser recognizes a declaration; it does not prove that every edge case behaves identically. Continue to test the fallback, the enhanced layout, keyboard navigation, narrow widths, and content that is much longer than the example.
Accessibility and responsive checks
Before shipping a modern CSS component:
-
Test keyboard focus. Every interactive control should have a visible
:focus-visiblestate. Do not replace it with a hover-only effect. -
Respect reduced motion. If a transition or animation communicates more than a small state change, reduce or remove it for users who request less motion:
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto; transition-duration: 0.01ms; animation-duration: 0.01ms; animation-iteration-count: 1; } }The
prefers-reduced-motionmedia feature lets the browser expose that user preference to CSS. -
Check contrast and non-color cues. Borders, icons, text, and focus indicators need enough contrast, and a validation error should have text or another clear cue in addition to color.
-
Zoom and resize the content. Check at 200% text zoom and on a narrow viewport. Ensure text does not disappear, overlap, or require horizontal scrolling without a good reason.
-
Test real content. Long headings, translated strings, empty states, and user-generated text reveal layout problems that short sample copy hides.
-
Keep semantics in HTML. CSS can change presentation, but it does not turn a
divinto a button or fix an incorrect heading hierarchy.
For a broader introduction to responsive decisions, see What Is Responsive Web Design and Why Is It Important?. If a project uses a framework, compare its defaults with the native techniques before adding another dependency; responsive CSS frameworks can be useful, but a small component may need only the platform's built-in layout features.
A practical adoption order
You do not need to rewrite a stylesheet to benefit from modern CSS. A low-risk order is:
- Move repeated colors and spacing into custom properties.
- Replace rigid type and spacing breakpoints with bounded
clamp()values. - Use logical properties in new components and areas that may support RTL.
- Add container queries when the same component appears in different-width regions.
- Add
:has(), native nesting, orsubgridonly where each one removes a real workaround. - Keep a working fallback and test the component with keyboard input, zoom, reduced motion, and long content.
The best modern CSS is not the CSS with the newest syntax. It is the smallest set of native capabilities that makes a component easier to reuse, more responsive to its context, and simpler to maintain without making older or assistive browsing experiences worse.