On this page
Modern CSS in 2026
The state of CSS in 2026
CSS has evolved dramatically in recent years. Features that previously required preprocessors or JavaScript are now native to the browser. This lesson covers the most important additions.
Native CSS Nesting
You no longer need Sass or Less to nest selectors. Native CSS supports nesting:
.nav {
display: flex;
gap: 1rem;
& a {
color: inherit;
text-decoration: none;
&:hover {
color: #b056ff;
}
}
@media (width < 768px) {
flex-direction: column;
}
}The & represents the parent selector. For pseudo-classes and pseudo-elements, the & is optional. For type selectors (p, h1), it is required.
The :has() selector
:has() is the "parent selector" that CSS never had. It lets you style an element based on what it contains:
/* Card that contains an image: different layout */
.card:has(img) {
display: grid;
grid-template-rows: auto 1fr;
}
/* Form with invalid fields: disabled button */
form:has(:invalid) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}
/* Sidebar open: adjust main */
body:has(.sidebar.open) .main {
margin-inline-start: 280px;
}Common use cases for :has()
- Visual form validation without JavaScript
- Conditional layouts based on content
- Global states (sidebar open, modal visible)
- Sibling styles (
.item:has(+ .item-active))
The light-dark() function
Greatly simplifies light/dark theming:
:root {
color-scheme: light dark;
}
body {
background: light-dark(#fff, #0a0a0f);
color: light-dark(#1a1a2e, #e8e8e8);
}You no longer need to duplicate blocks with @media (prefers-color-scheme: dark) for each property.
@scope: style encapsulation
@scope limits the reach of styles to a specific DOM tree, avoiding collisions:
@scope (.panel) {
h2 { font-size: 1.5rem; }
p { color: #555; }
}With the lower boundary (to), you can exclude parts of the tree:
@scope (.panel) to (.panel__slot) {
/* Styles that do NOT affect the slot content */
p { margin: 0; }
}Anchor positioning
Position tooltips, popovers, and dropdown menus relative to an anchor element, without JavaScript:
.trigger {
anchor-name: --my-button;
}
.popover {
position: fixed;
position-anchor: --my-button;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0.5rem;
}Advanced math functions
CSS now includes full math functions:
| Function | Use |
|---|---|
round() |
Round values |
mod() |
Modulo (remainder) |
rem() |
Remainder with dividend sign |
abs() |
Absolute value |
sign() |
Sign of the value (-1, 0, 1) |
pow(), sqrt(), log() |
Advanced operations |
sin(), cos(), tan() |
Trigonometric functions |
.element {
/* Round to the nearest multiple of 4px */
padding: round(nearest, 1.3rem, 4px);
}Scroll-driven animations
Animate elements based on the scroll position, without JavaScript:
@keyframes appear {
from { opacity: 0; scale: 0.9; }
to { opacity: 1; scale: 1; }
}
.section {
animation: appear linear both;
animation-timeline: view();
animation-range: entry 10% entry 40%;
}view() creates a timeline based on when the element enters and leaves the viewport.
View Transitions API
Animated transitions between views in a SPA or between pages:
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: fade-out 300ms ease;
}
::view-transition-new(root) {
animation: fade-in 300ms ease;
}Styling Forms
Forms are one of the hardest parts of CSS to style. New modern pseudo-classes and properties change this entirely.
Validation states: `:valid`, `:invalid`, `:user-invalid`
CSS can react to the validation state of form fields:
/* Applied ONLY after user interaction */
.field:user-invalid {
border-color: #e74c3c;
box-shadow: 0 0 0 3px rgb(231 76 60 / 20%);
}
.field:valid {
border-color: #2ecc71;
}[!TIP] Prefer
:user-invalidover:invalid. The:invalidpseudo-class marks fields as invalid immediately on page load, while:user-invalidwaits until the user has interacted with the field.
Floating labels with `:placeholder-shown`
The "floating label" pattern can be achieved without JavaScript, using :placeholder-shown and the adjacent sibling combinator ~:
.field-group {
position: relative;
}
.field-group .field::placeholder {
color: transparent; /* Hide the real placeholder */
}
.field-group .label {
position: absolute;
top: 50%;
left: 0.75rem;
translate: 0 -50%;
transition: all 200ms ease;
color: #888;
pointer-events: none;
}
/* When the field has text or is focused, the label moves up */
.field-group .field:not(:placeholder-shown) ~ .label,
.field-group .field:focus ~ .label {
top: 0;
translate: 0 -50%;
font-size: 0.75rem;
background: white;
padding-inline: 0.25rem;
color: #b056ff;
}`accent-color` and `caret-color`
Theming native browser controls (checkboxes, radios, range sliders) is now trivial:
input[type="checkbox"],
input[type="radio"],
input[type="range"] {
accent-color: #b056ff;
}
input, textarea {
caret-color: #b056ff; /* Text cursor color */
}accent-color tints the primary color of native controls. The browser handles contrast automatically.
`field-sizing: content` for textareas
A new property that allows <textarea> elements to grow automatically based on their content:
.textarea-auto {
field-sizing: content;
min-height: 3lh; /* Minimum 3 lines */
max-height: 10lh; /* Maximum 10 lines */
}[!INFO] The
lhunit equals the computedline-heightof the element. It is ideal for defining heights based on lines of text.
Customizing `
/* Native CSS Nesting (no preprocessors) */
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #e0e0e0;
transition: border-color 200ms ease;
&:hover {
border-color: #b056ff;
}
& .title {
font-size: 1.25rem;
font-weight: 600;
margin-block-end: 0.5rem;
}
& .description {
color: #666;
line-height: 1.6;
}
/* Nested media queries */
@media (width >= 768px) {
padding: 2rem;
}
}
/* Scope: encapsulate styles */
@scope (.panel) to (.panel__content) {
p { color: #333; font-size: 0.9rem; }
a { color: #b056ff; }
}
/* Anchor positioning */
.tooltip-trigger {
anchor-name: --my-trigger;
}
.tooltip {
position: fixed;
position-anchor: --my-trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
background: #1a1a2e;
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
}
/* :has() - the missing parent selector */
.form:has(:invalid) .submit-button {
opacity: 0.5;
pointer-events: none;
}
/* Card with image vs without image */
.card:has(> img) {
grid-template-rows: 200px 1fr;
}
.card:not(:has(> img)) {
padding-block-start: 2rem;
}
/* Styles based on checkbox state */
.filter:has(input:checked) {
background: oklch(90% 0.1 260);
border-color: #b056ff;
}
/* Advanced math functions */
.grid-items {
--cols: 3;
display: grid;
grid-template-columns: repeat(var(--cols), 1fr);
gap: round(nearest, 1.5rem, 0.25rem);
}
/* light-dark() for theming */
:root {
color-scheme: light dark;
}
.surface {
background: light-dark(#ffffff, #1a1a2e);
color: light-dark(#1a1a2e, #e8e8e8);
border: 1px solid light-dark(
rgb(0 0 0 / 10%),
rgb(255 255 255 / 10%)
);
}
/* Validation states */
.field:user-invalid {
border-color: #e74c3c;
box-shadow: 0 0 0 3px rgb(231 76 60 / 20%);
}
.field:valid {
border-color: #2ecc71;
}
/* Floating labels with :placeholder-shown */
.field-group {
position: relative;
}
.field-group .field::placeholder {
color: transparent;
}
.field-group .label {
position: absolute;
top: 50%;
left: 0.75rem;
translate: 0 -50%;
transition: all 200ms ease;
color: #888;
pointer-events: none;
}
.field-group .field:not(:placeholder-shown) ~ .label,
.field-group .field:focus ~ .label {
top: 0;
translate: 0 -50%;
font-size: 0.75rem;
background: white;
padding-inline: 0.25rem;
color: #b056ff;
}
/* Theme native controls */
.checkbox-field,
.radio-field,
.range-field {
accent-color: #b056ff;
}
.field {
caret-color: #b056ff;
}
/* Auto-sizing textarea */
.textarea-auto {
field-sizing: content;
min-height: 3lh;
max-height: 10lh;
}
/* Custom select */
.custom-select {
appearance: none;
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") no-repeat right 0.75rem center;
padding: 0.5rem 2rem 0.5rem 0.75rem;
border: 1px solid #ccc;
border-radius: 6px;
}
/* :focus-visible vs :focus */
.field:focus-visible {
outline: 2px solid #b056ff;
outline-offset: 2px;
}
.button:focus-visible {
outline: 2px solid #b056ff;
outline-offset: 2px;
}
/* :focus without :focus-visible — mouse only */
.button:focus:not(:focus-visible) {
outline: none;
}
Sign in to track your progress