Modern CSS & Best Practices
:is(), :where(), and :has(), container queries that respond to parent width instead of viewport, native CSS nesting, and the performance & accessibility habits that separate pros from beginners. By the end you'll write CSS that is leaner, smarter, and future-proof.
Modern Selectors: :is(), :where(), :has()
These three pseudo-class functions arrived in browsers around 2021–2022 and have quickly become essential. They let you write shorter, smarter selectors without the combinatorial explosion of the old way.
:is() — Smart Grouping
:is() lets you group selectors in a list. The specificity of :is() is determined by the most specific selector in its argument list. It collapses dozens of repetitive rules into one.
/* OLD WAY — repetitive */ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; text-decoration: none; } /* MODERN WAY — :is() groups them */ :is(h1, h2, h3, h4, h5, h6) a { color: inherit; text-decoration: none; } /* Combine contexts easily */ :is(.card, .modal, .sidebar) p { font-size: 0.9rem; color: var(--txt2); }
:where() — Zero-Specificity Grouping
:where() works exactly like :is() but with zero specificity. This makes it perfect for base/reset styles that you want any author style to override easily.
/* :where() has 0 specificity — easy to override */ :where(ul, ol) { list-style: none; padding: 0; margin: 0; } /* This single-class rule easily overrides the :where() above */ .my-list { list-style: disc; /* wins because .class > :where() */ } /* Great for design-system base resets */ :where(button, input, select, textarea) { font-family: inherit; font-size: 1rem; }
:has() — The Parent Selector 🎉
For years developers begged for a way to select a parent based on its children. :has() finally delivers. It is the most powerful selector ever added to CSS — it lets you style a parent based on what's inside it.
/* Style a card differently if it contains an image */ .card:has(img) { padding: 0; /* remove padding when image present */ } /* Style a form when any input is invalid */ form:has(:invalid) { border: 2px solid red; } /* Style a label when its checkbox is checked */ .toggle:has(input:checked) { background: var(--acc); color: white; } /* Layout change when sidebar is present */ body:has(.sidebar) .main { margin-left: 280px; /* only when sidebar exists in DOM */ }
:is() and :where() have near-universal support (97%+). :has() is now supported by all major browsers including Chrome 105+, Safari 15.4+, Firefox 121+, and Edge 105+. Safe to use in production.Container Queries
Media queries respond to the viewport width. But what if a component is used in both a full-width page and a narrow sidebar? Container queries let a component respond to its own parent's size — a game-changer for reusable UI.
/* Step 1: declare a container on the PARENT */ .card-wrapper { container-type: inline-size; /* tracks width */ container-name: card; /* optional name */ } /* Step 2: query the container from CHILD styles */ @container card (min-width: 400px) { .product-card { display: grid; grid-template-columns: 1fr 1fr; } .product-card img { height: 100%; object-fit: cover; } } /* Without a name, queries apply to nearest containment ancestor */ .sidebar-widget { container-type: inline-size; } @container (max-width: 250px) { .sidebar-widget .icon-label { display: none; /* hide text in tiny sidebars */ } }
Native CSS Nesting
For years, nesting was only available via Sass or PostCSS. Now it's built into native CSS. You can nest selectors inside each other just like you would in a preprocessor — no build step required.
/* Native CSS nesting — no Sass needed! */ .card { background: white; border-radius: 12px; padding: 1.5rem; /* Nested child selector */ & .card-title { font-size: 1.2rem; font-weight: 700; } /* Nested hover state */ &:hover { box-shadow: 0 8px 32px rgba(0,0,0,.15); transform: translateY(-2px); } /* Nested media query */ @media (max-width: 600px) { padding: 1rem; } /* Nested modifier class */ &.card--featured { border: 2px solid gold; } }
& symbol is required& nesting selector when combining with element selectors or starting with a letter. Omit it only for nested at-rules like @media. Supported in all modern browsers (Chrome 112+, Firefox 117+, Safari 16.5+).Performance & Accessibility Best Practices
Writing great CSS isn't just about visual results — it's about making sure your styles load fast and work for everyone.
🚀 Performance Tips
/* 1. Use will-change SPARINGLY for animated elements */ .animated-card { will-change: transform; /* hints browser to composite this layer */ } /* ⚠️ Don't apply to everything — it uses GPU memory */ /* 2. Prefer transform & opacity for animations (GPU-accelerated) */ .slide-in { transform: translateX(-100%); /* ✅ GPU */ opacity: 0; /* ✅ GPU */ } /* Avoid animating: width, height, top, left, margin (cause reflow) */ /* 3. Use content-visibility for off-screen sections */ .below-fold-section { content-visibility: auto; /* skip rendering until visible */ contain-intrinsic-size: 0 500px; /* estimated height hint */ } /* 4. Limit selector depth — shallow is faster */ .nav-item { } /* ✅ fast */ /* header nav ul li a span { } — ❌ slow, avoid */ /* 5. Use logical properties for international support */ .box { margin-inline-start: 1rem; /* left in LTR, right in RTL */ padding-block: 0.5rem; /* top + bottom padding */ }
♿ Accessibility in CSS
/* 1. Never remove focus outlines — restyle them instead */ :focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; } /* 2. Respect user motion preferences */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } /* 3. Respect user color-scheme preference */ @media (prefers-color-scheme: light) { :root { --bg: #ffffff; --txt: #111111; } } /* 4. Visually hidden but accessible to screen readers */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } /* 5. Minimum touch target sizes */ .btn { min-height: 44px; /* WCAG 2.5.5 recommended */ min-width: 44px; }
What's Next in CSS
CSS is evolving faster than ever. Here's a quick radar of what's coming or recently landed:
anchor-name + position-anchor. Landing in 2024–2025.animation-timeline: scroll().color-mix(in oklch, blue 50%, red) — no preprocessor needed./* @layer — explicit cascade ordering (no !important needed) */ @layer reset, base, components, utilities; @layer reset { *, *::before, *::after { box-sizing: border-box; } } @layer base { body { font-family: var(--ff-b); } } @layer components { .btn { padding: 0.5rem 1rem; border-radius: 6px; } } @layer utilities { /* Utilities always win over components in the same layer order */ .mt-4 { margin-top: 1rem; } .hidden { display: none; } }
:is() and :where()?.card to display in a two-column grid ONLY when its container is wider than 500px (not when the viewport is). Which approach is correct?:has() allow that was previously impossible in pure CSS?transform and opacity over properties like width, top, or margin?& nesting selector required?Create a
.card component using the techniques from this lesson:1. :has() — When the card contains an
img, remove its padding and display the image edge-to-edge (border-radius on the image instead).2. Container Query — When the card's container is wider than 420px, switch the card to a horizontal layout (image left, content right) using
display: grid; grid-template-columns: 200px 1fr.3. Native Nesting — Write all card styles using native CSS nesting (
.card { & img { } &:hover { } }).4. Accessibility — Add
:focus-visible styles and respect prefers-reduced-motion for the hover transition.
💡 Show hints
- Set
container-type: inline-sizeon the wrapper div, not on .card itself - Use
.card:has(img) { padding: 0; }then.card:has(img) img { border-radius: 12px 12px 0 0; } - Inside
@container (min-width: 420px), target.card:has(img)for grid layout change - Wrap your hover transition in
@media (prefers-reduced-motion: no-preference)