modern-css
Modern CSS Patterns
CSS Nesting
Native CSS nesting, no preprocessor required:
.card {
padding: 1rem;
background: white;
& .title {
font-size: 1.25rem;
font-weight: 600;
}
&:hover {
box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);
}
@media (width >= 768px) {
padding: 2rem;
}
}
&references the parent selector (required for pseudo-classes and combinators).- Media queries can be nested directly inside rules.
- Don't nest more than 3 levels deep — it becomes hard to read and produces overly specific selectors.
Cascade Layers
Control specificity ordering explicitly with @layer:
@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.5;
}
a {
color: var(--color-link);
}
}
@layer components {
.button {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
}
}
@layer utilities {
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
}
Layer order determines priority — later layers win over earlier ones regardless of selector specificity. Unlayered styles always beat layered styles.
Container Queries
Style elements based on their container's size, not the viewport:
.card-grid {
container-type: inline-size;
container-name: card-grid;
}
@container card-grid (width >= 600px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
@container card-grid (width >= 900px) {
.card {
grid-template-columns: 300px 1fr;
}
}
container-type: inline-sizeenables width-based queries.- Use container queries for reusable components that must adapt to their context, not just the viewport.
- Pair with Tailwind v4's
@containerand@md:syntax for utility-class container queries.
Container Style Queries
Query a container's custom property values:
.theme-wrapper {
container-type: normal;
--theme: light;
}
@container style(--theme: dark) {
.card {
background: #1a1a1a;
color: white;
}
}
:has() Selector
The parent selector CSS never had — select elements based on their descendants or siblings:
/* Card with an image gets different layout */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
/* Form group with invalid input shows error styling */
.form-group:has(:invalid) {
border-color: var(--color-error);
}
/* Navigation with many items switches to hamburger */
nav:has(> :nth-child(6)) {
.nav-list {
display: none;
}
.hamburger {
display: block;
}
}
/* Style previous siblings */
li:has(+ li:hover) {
opacity: 0.7;
}
:has() is supported in all modern browsers. Use it to replace JavaScript-driven parent styling.
@scope
Scope styles with upper and lower bounds:
@scope (.card) to (.card-footer) {
p {
color: var(--color-muted);
}
a {
color: var(--color-link);
}
}
- Styles apply within
.cardbut stop at.card-footer. - Proximity wins — closer scopes override farther ones (unlike specificity).
- Useful for component-scoped styles without CSS Modules or BEM.
View Transitions
Same-Document (SPA)
function navigate(url: string) {
if (!document.startViewTransition) {
updateDOM(url);
return;
}
document.startViewTransition(() => {
updateDOM(url);
});
}
::view-transition-old(root) {
animation: fade-out 0.2s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in;
}
Cross-Document (MPA)
@view-transition {
navigation: auto;
}
Named Transitions
Animate specific elements between pages:
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero) {
animation: shrink 0.3s ease-out;
}
::view-transition-new(hero) {
animation: grow 0.3s ease-in;
}
Each view-transition-name must be unique on the page.
Anchor Positioning
Position elements relative to an anchor without JavaScript:
.tooltip-trigger {
anchor-name: --trigger;
}
.tooltip {
position: fixed;
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0.5rem;
/* Fallback positioning */
position-try-fallbacks: flip-block;
}
- Replaces JavaScript tooltip/popover positioning libraries.
position-try-fallbackshandles edge cases when the tooltip would go off-screen.- Works with
popoverattribute for layered UI.
Scroll-Driven Animations
Animate based on scroll position, no JavaScript:
.progress-bar {
animation: grow-width linear;
animation-timeline: scroll();
}
@keyframes grow-width {
from {
width: 0%;
}
to {
width: 100%;
}
}
/* Animate when element enters viewport */
.fade-in {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-in {
from {
opacity: 0;
translate: 0 2rem;
}
to {
opacity: 1;
translate: 0;
}
}
scroll()— progress based on scroll container position.view()— progress based on element visibility in viewport.animation-range— define when the animation starts and ends.
@starting-style
Animate from display: none without JavaScript:
dialog[open] {
opacity: 1;
transform: scale(1);
transition:
opacity 0.3s,
transform 0.3s;
@starting-style {
opacity: 0;
transform: scale(0.95);
}
}
Works with popover, dialog, and any element toggling display.
Custom Properties Patterns
Design Tokens
:root {
--color-primary: oklch(0.6 0.2 250);
--color-surface: oklch(0.98 0 0);
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-4: 1rem;
}
[data-theme="dark"] {
--color-primary: oklch(0.7 0.2 250);
--color-surface: oklch(0.15 0 0);
}
Component-Scoped Variables
.button {
--_bg: var(--color-primary);
--_text: white;
--_padding: var(--space-2) var(--space-4);
background: var(--_bg);
color: var(--_text);
padding: var(--_padding);
&.secondary {
--_bg: transparent;
--_text: var(--color-primary);
}
}
Prefix internal variables with _ to distinguish them from public API tokens.
Color Functions
/* OKLCH — perceptually uniform, wide gamut */
color: oklch(0.6 0.2 250);
/* Relative color syntax — derive colors */
--hover: oklch(from var(--color-primary) calc(l - 0.1) c h);
--muted: oklch(from var(--color-primary) l calc(c * 0.5) h);
/* color-mix — blend colors */
border-color: color-mix(in oklch, var(--color-primary) 20%, transparent);
Prefer oklch for design tokens — it produces perceptually consistent lightness and saturation scales.
More from grahamcrackers/skills
bulletproof-react-patterns
Bulletproof React architecture patterns for scalable, maintainable applications. Covers feature-based project structure, component patterns, state management boundaries, API layer design, error handling, security, and testing strategies. Use when structuring a React project, designing application architecture, organizing features, or when the user asks about React project structure or scalable patterns.
45react-aria-components
React Aria Components patterns for building accessible, unstyled UI with composition-based architecture. Covers component structure, styling with Tailwind and CSS, render props, collections, forms, selections, overlays, and drag-and-drop. Use when building accessible components, using react-aria-components, creating design systems, or when the user asks about React Aria, accessible UI primitives, or headless component libraries.
17clean-code-principles
Clean code principles for readable, maintainable TypeScript and React codebases. Covers naming, functions, abstraction, composition, error handling, comments, and code smells. Use when writing new code, refactoring, reviewing code quality, or when the user asks about clean code, readability, or maintainability.
10typescript-best-practices
Core TypeScript conventions for type safety, inference, and clean code. Use when writing TypeScript, reviewing TypeScript code, creating interfaces/types, or when the user asks about TypeScript patterns, conventions, or best practices.
9tanstack-query
TanStack Query v5 patterns for server state management, caching, mutations, optimistic updates, and query organization. Use when working with TanStack Query, React Query, server state, data fetching hooks, or when the user asks about caching strategies, query invalidation, or mutation patterns.
8zustand
Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions. Use when managing client-side state, creating stores, working with Zustand, or when the user asks about global state management, store patterns, or state persistence.
7