10 Modern CSS Features You Should Use in Every Project
CSS has evolved more in the past three years than in the previous decade. Features that once required preprocessors, JavaScript libraries, or elaborate workarounds are now available natively in every major browser. Container queries let components respond to their parent's size rather than the viewport. The :has() pseudo-class finally gives us a parent selector. Native nesting eliminates the last major reason to reach for Sass. And that is just the beginning.
This guide covers ten CSS features that have reached baseline browser support between 2023 and 2025 and are ready to use in production today. Each section includes a concise explanation of the feature, a practical code example, and a one-line browser support summary. These are not experimental curiosities — they are practical, everyday tools that will make your stylesheets simpler, more maintainable, and more powerful.
Whether you are starting a new project or gradually modernizing an existing codebase, adopting these features will reduce your reliance on build tools and JavaScript, shrink your CSS bundle size, and give you capabilities that were simply impossible before.
1. CSS Nesting
CSS nesting lets you write selectors inside other selectors, mirroring the structure of your HTML. This was the defining feature of preprocessors like Sass and Less for over a decade, and it is now a native part of the CSS language. Nesting improves readability, reduces repetition, and keeps related styles grouped together.
The syntax is straightforward: place a selector inside a rule block, and the browser treats it as a descendant (or modifier) of the parent selector. You can use the & symbol to explicitly reference the parent selector, which is required when combining with pseudo-classes or pseudo-elements, or when the nested selector does not start with a symbol.
.card {
padding: 1.5rem;
border-radius: 0.5rem;
background: white;
&:hover {
box-shadow: 0 4px 12px oklch(0% 0 0 / 10%);
}
.card-title {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.card-body {
color: #64748b;
}
&.featured {
border: 2px solid var(--primary);
}
@media (max-width: 768px) {
padding: 1rem;
}
}
Nesting also works with media queries and other at-rules. A @media block nested inside a rule automatically inherits the parent selector, so you keep all styles for a component in one place rather than scattering them across separate media query blocks. This co-location of responsive styles is one of the biggest practical benefits of nesting.
Browser support: Chrome 120+, Edge 120+, Firefox 117+, Safari 17.2+.
2. Container Queries
Container queries allow components to adapt their styles based on the size of their parent container rather than the viewport. This is a fundamental shift in responsive design. Media queries answer the question "how wide is the screen?" Container queries answer "how wide is the space I have been given?" — which is the question components actually need answered.
To use container queries, first declare a containment context on the parent element with container-type. Then use the @container at-rule in the child's styles to set conditional rules based on the container's dimensions:
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
.widget {
display: flex;
flex-direction: column;
gap: 0.5rem;
@container sidebar (min-width: 300px) {
flex-direction: row;
align-items: center;
}
@container sidebar (min-width: 500px) {
font-size: 1.125rem;
}
}
The container-type: inline-size declaration enables size containment on the inline axis (width in horizontal writing modes). You can also use size for both axes, though inline-size is more common and less likely to cause layout issues. The container-name is optional but helpful when you have multiple containers and need to target a specific one.
Container queries make truly reusable components possible. A card component can have one layout when placed in a narrow sidebar and a completely different layout when placed in a wide main content area, without the component knowing anything about the page structure.
Browser support: Chrome 105+, Edge 105+, Firefox 110+, Safari 16+.
3. :has() — The Parent Selector
The :has() pseudo-class is arguably the most transformative CSS selector ever introduced. It selects an element based on its descendants, siblings, or subsequent elements. In practice, it functions as a parent selector, a previous-sibling selector, and a conditional selector all in one.
/* Style a card differently when it contains an image */
.card:has(img) {
grid-template-rows: 200px auto auto;
}
/* Style a form group when its input is invalid */
.form-group:has(input:invalid) {
.label {
color: var(--error);
}
}
/* Style a figure when it has a figcaption */
figure:has(figcaption) {
padding-bottom: 0.5rem;
border-bottom: 1px solid #e2e8f0;
}
/* Select a heading followed by a subtitle */
h1:has(+ .subtitle) {
margin-bottom: 0.25rem;
}
The :has() selector is not limited to parent selection. Using combinators like + (adjacent sibling) and ~ (general sibling) inside :has(), you can select elements based on what comes after them in the DOM. This was previously impossible in CSS.
Performance-wise, :has() is fast in all implementations. Browsers optimize it using the same invalidation strategies used for other pseudo-classes. However, avoid overly broad selectors like :has(*) or deeply nested :has() chains in very large DOMs.
Browser support: Chrome 105+, Edge 105+, Firefox 121+, Safari 15.4+.
4. color-mix() — Blend Colors in Any Color Space
The color-mix() function blends two colors in a specified color space and returns the resulting color. This eliminates the need for preprocessor color functions like Sass's darken(), lighten(), and mix(). Better yet, it works at runtime with custom properties, enabling dynamic theming that preprocessors could never achieve.
:root {
--primary: oklch(55% 0.22 260);
}
.button {
background: var(--primary);
}
.button:hover {
/* Darken by mixing with black */
background: color-mix(in oklch, var(--primary) 85%, black);
}
.button:active {
background: color-mix(in oklch, var(--primary) 70%, black);
}
.button-ghost {
color: var(--primary);
/* Tinted background at 10% opacity */
background: color-mix(in oklch, var(--primary) 10%, transparent);
}
The first argument specifies the interpolation color space. Using oklch gives perceptually uniform results. Using srgb can produce muddy mid-tones when mixing complementary colors but is adequate for simple tint/shade operations. For a comprehensive guide to modern CSS color, see our Modern CSS Color guide.
Browser support: Chrome 111+, Edge 111+, Firefox 113+, Safari 16.2+.
5. CSS @layer — Control Cascade Order
The @layer at-rule gives you explicit control over the cascade order of your styles. Instead of relying on source order and specificity wars, you declare an ordered list of layers. Styles in later layers take precedence over styles in earlier layers, regardless of selector specificity.
/* Declare layer order upfront */
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: #1e293b;
}
a {
color: var(--primary);
}
}
@layer components {
.button {
padding: 0.75rem 1.5rem;
border-radius: 0.375rem;
font-weight: 600;
}
}
@layer utilities {
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
}
The killer use case for @layer is managing third-party CSS. Import a library's styles into a lower layer, and your component styles will always win regardless of the library's specificity. This eliminates an entire category of CSS debugging: figuring out why a framework's styles are overriding yours.
Browser support: Chrome 99+, Edge 99+, Firefox 97+, Safari 15.4+.
6. Logical Properties — Writing-Mode Aware Sizing
Logical properties replace physical direction-based properties (margin-left, padding-right, border-top) with writing-mode-aware alternatives. margin-inline-start refers to the left margin in left-to-right languages and the right margin in right-to-left languages. This makes your CSS automatically adaptable to different writing directions.
.card {
/* Instead of padding: 1rem 2rem */
padding-block: 1rem;
padding-inline: 2rem;
/* Instead of margin-bottom: 1.5rem */
margin-block-end: 1.5rem;
/* Instead of border-left: 3px solid var(--primary) */
border-inline-start: 3px solid var(--primary);
/* Instead of width / height */
inline-size: 100%;
max-inline-size: 40rem;
min-block-size: 10rem;
/* Instead of top, right, bottom, left in inset */
inset-block-start: 0;
inset-inline: 1rem;
}
Even if your site only supports English, adopting logical properties is a good practice. They future-proof your code for internationalization, and the inline/block terminology is more semantically meaningful than left/right/top/bottom in a world of flexbox and grid where axis direction can change.
Browser support: Chrome 87+, Edge 87+, Firefox 66+, Safari 14.1+.
7. text-wrap: balance and pretty
The text-wrap property gives you control over how text breaks across lines. The balance value distributes text evenly across lines so that no line is dramatically shorter than the others — perfect for headings. The pretty value avoids orphans (single words on the last line) in body text.
h1, h2, h3 {
/* Balances text across lines for even visual weight */
text-wrap: balance;
}
p {
/* Avoids typographic orphans on the last line */
text-wrap: pretty;
}
/* Example heading without balance:
"Introduction to Modern CSS
Tools"
With balance:
"Introduction to
Modern CSS Tools"
*/
The balance value works best on short text (headings, captions, labels). Browsers limit balancing to blocks of six lines or fewer for performance reasons. The pretty value is designed for longer paragraphs and focuses on the last few lines, preventing single-word orphans that look awkward at the end of a paragraph.
Both values are progressive enhancements — browsers that do not support them simply render text with default wrapping behavior. There is no visual regression, just a missed optimization.
Browser support: balance: Chrome 114+, Edge 114+, Firefox 121+, Safari 17.5+. pretty: Chrome 117+, Edge 117+, Safari 17.5+.
8. @property — Typed Custom Properties
The @property at-rule lets you register custom properties with a specific type, initial value, and inheritance behavior. This unlocks capabilities that are impossible with regular custom properties, most notably the ability to animate custom properties and provide type checking.
@property --hue {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --progress {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
/* Now you can animate these custom properties */
.loader {
--progress: 0%;
background: conic-gradient(
var(--primary) var(--progress),
transparent var(--progress)
);
transition: --progress 0.5s ease-out;
}
.loader.complete {
--progress: 100%;
}
/* Animate a hue rotation */
.rainbow {
--hue: 0deg;
background: oklch(70% 0.2 var(--hue));
animation: rotate-hue 3s linear infinite;
}
@keyframes rotate-hue {
to { --hue: 360deg; }
}
Without @property, custom properties are treated as opaque strings by the browser and cannot be interpolated. transition: --my-color 0.3s does nothing with a regular custom property because the browser does not know it represents a color. With @property and syntax: "<color>", the browser knows the type and can interpolate between values.
Valid syntax values include <number>, <percentage>, <length>, <color>, <angle>, <time>, <image>, <transform-function>, and combinations like <color>+ (space-separated list) or <length> | <percentage> (either type).
Browser support: Chrome 85+, Edge 85+, Firefox 128+, Safari 16.4+.
9. :is() and :where() — Selector Simplification
The :is() and :where() pseudo-classes both accept a selector list and match any element that matches at least one of the listed selectors. They eliminate repetitive compound selectors. The difference between them is specificity: :is() takes the specificity of its most specific argument, while :where() always has zero specificity.
/* Without :is() - repetitive */
article h1,
article h2,
article h3,
article h4 {
line-height: 1.3;
margin-block: 1.5em 0.5em;
}
/* With :is() - concise, inherits highest specificity */
article :is(h1, h2, h3, h4) {
line-height: 1.3;
margin-block: 1.5em 0.5em;
}
/* :where() for zero-specificity defaults (easily overridable) */
:where(ul, ol) {
padding-inline-start: 1.5rem;
}
/* This easily overrides the above because :where() adds
no specificity */
.compact-list {
padding-inline-start: 0.75rem;
}
Use :is() for component styles where you want normal specificity behavior. Use :where() for base styles, resets, and defaults that should be easily overridable without specificity conflicts. The :where() function is particularly valuable in CSS layers and design system defaults.
Both pseudo-classes are also forgiving: if one selector in the list is invalid, the others still work. Without :is(), a single invalid selector in a comma-separated list invalidates the entire rule.
Browser support: Chrome 88+, Edge 88+, Firefox 78+, Safari 14+.
10. Individual Transform Properties
For years, applying multiple transforms required combining them in a single transform property: transform: translateX(10px) rotate(45deg) scale(1.2). This made it difficult to animate individual transforms independently because changing any one required redeclaring the entire list. CSS now provides individual properties: translate, rotate, and scale.
.element {
/* Individual properties instead of combined transform */
translate: 0 -10px;
rotate: 15deg;
scale: 1.1;
}
/* Now you can transition them independently */
.card {
scale: 1;
rotate: 0deg;
transition: scale 0.2s ease, rotate 0.3s ease;
}
.card:hover {
scale: 1.05;
}
.card:active {
scale: 0.98;
rotate: -1deg;
}
/* Animate rotation without affecting other transforms */
@keyframes spin {
to { rotate: 360deg; }
}
.loading-icon {
translate: 0 -2px; /* Stays in place */
animation: spin 1s linear infinite;
}
The individual properties are applied in a fixed order: translate first, then rotate, then scale. This order applies regardless of the order they appear in your CSS. If you need a different application order, use the combined transform property instead.
Individual transform properties coexist with the transform property. Both can be set simultaneously, with individual properties applied after the transform value. However, mixing them can be confusing, so it is best to pick one approach per element.
Browser support: Chrome 104+, Edge 104+, Firefox 72+, Safari 14.1+.
Summary Table
| Feature | What It Replaces | Baseline Support |
|---|---|---|
| CSS Nesting | Sass/Less nesting | Chrome 120+, Firefox 117+, Safari 17.2+ |
| Container Queries | JS-based resize observers for responsive components | Chrome 105+, Firefox 110+, Safari 16+ |
| :has() | JavaScript parent/sibling selection | Chrome 105+, Firefox 121+, Safari 15.4+ |
| color-mix() | Sass darken()/lighten()/mix() | Chrome 111+, Firefox 113+, Safari 16.2+ |
| @layer | Specificity hacks, !important | Chrome 99+, Firefox 97+, Safari 15.4+ |
| Logical Properties | Physical direction properties + RTL overrides | Chrome 87+, Firefox 66+, Safari 14.1+ |
| text-wrap: balance/pretty | JS text balancing libraries | Chrome 114+, Firefox 121+, Safari 17.5+ |
| @property | JS-driven custom property animation | Chrome 85+, Firefox 128+, Safari 16.4+ |
| :is() / :where() | Repetitive compound selectors | Chrome 88+, Firefox 78+, Safari 14+ |
| Individual Transforms | Combined transform property for independent animations | Chrome 104+, Firefox 72+, Safari 14.1+ |
Getting Started
You do not need to adopt all ten features at once. Start with the ones that address your current pain points. If you are writing Sass only for nesting, switch to native nesting and drop the preprocessor. If you have media queries on every component, try container queries on one component and see how it simplifies your code. If your dark mode uses completely separate color tokens, try color-mix() and relative color syntax to derive them automatically.
The key insight is that these features are not independent islands — they compose beautifully. Container queries plus :has() let you build components that respond to both their container size and their content. CSS nesting plus @layer gives you organized, low-specificity stylesheets. @property plus color-mix() enables animatable, dynamic color themes. The whole is greater than the sum of its parts.
Further Reading
- All CSS Guides — browse the full collection of in-depth CSS tutorials
- CSS View Transitions: From Basic to Advanced — add fluid page transitions to your projects
- Modern CSS Color: oklch, color-mix(), and the New Color Spaces — deep dive into the modern color ecosystem
- CSS Grid and Subgrid: A Comprehensive Deep Dive — master two-dimensional layouts with Grid and subgrid
- Color Mix Playground — interactively experiment with color-mix()
- Grid Generator — visually build CSS Grid layouts
- View Transition Builder — configure and preview view transition animations