CSS Responsive Design and Modern Features
- Description: Media queries, fluid typography, custom properties for theming, dark mode, transitions, animations, and modern CSS shipped 2022-2026.
- My Notion Note ID: K2A-F2-3
- Created: 2018-03-23
- Updated: 2026-05-17
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Media Queries
- 2. Fluid Typography and Spacing
- 3. Custom Properties and Theming
- 4. Dark Mode
- 5. Transitions
- 6. Animations
- 7. Transforms and Filters
- 8. Cascade Layers, Nesting, Scope
- 9. References
1. Media Queries
Apply rules conditionally based on viewport, device, or user preference.
@media (min-width: 768px) {
.nav { display: flex; }
}
@media (max-width: 767px) and (orientation: portrait) {
.nav { display: none; }
}
1.1 Common Features
| Feature | Examples |
|---|---|
width, height |
(min-width: 768px), (max-width: 1024px) |
orientation |
portrait, landscape |
prefers-color-scheme |
light, dark |
prefers-reduced-motion |
reduce, no-preference |
prefers-contrast |
more, less, no-preference |
prefers-reduced-transparency |
— |
forced-colors |
active, none (Windows high-contrast mode) |
pointer, hover |
coarse/fine, hover/none — touch vs mouse |
resolution |
(min-resolution: 2dppx) (retina) |
1.2 Modern Range Syntax
Cleaner than min-width/max-width:
@media (768px <= width < 1280px) { ... }
@media (width >= 768px) { ... }
1.3 Breakpoints
There are no canonical breakpoints. Common starting points:
| Tier | Width |
|---|---|
| Mobile | < 640px |
| Tablet | 640-1024px |
| Desktop | 1024-1280px |
| Wide | > 1280px |
Tailwind's defaults (sm, md, lg, xl, 2xl) map to 640 / 768 / 1024 / 1280 / 1536px. Pick the breakpoints your content needs, not an arbitrary list.
Mobile-first is the dominant convention — base styles target small screens, @media (min-width: ...) adds desktop refinements. Simpler CSS, smaller mobile payload.
2. Fluid Typography and Spacing
Discrete breakpoints cause jumps in font size at boundaries. Modern approach: a single fluid expression with clamp().
:root {
/* Scales from 1rem at 320px viewport to 1.25rem at 1280px */
--step-0: clamp(1rem, 0.83rem + 0.83vw, 1.25rem);
/* Body */
--step-1: clamp(1.125rem, 0.93rem + 0.93vw, 1.5rem);
/* Heading 1 */
--step-5: clamp(2rem, 1.43rem + 2.86vw, 4rem);
}
body { font-size: var(--step-1); }
h1 { font-size: var(--step-5); }
Tools: https://utopia.fyi/ generates the full clamp scale from min/max values and breakpoints.
Same trick for spacing:
--space-md: clamp(1rem, 0.5rem + 2vw, 2rem);
Pitfall: clamp() arguments must be exact. The middle value is the fluid value — usually (intercept) + (slope * 100vw). The min/max clamp it.
3. Custom Properties and Theming
Custom properties (CSS variables) cascade and inherit, so theming becomes setting a few root-level tokens.
:root {
--color-bg: oklch(99% 0 0);
--color-fg: oklch(20% 0 0);
--color-muted: oklch(55% 0 0);
--color-accent: oklch(60% 0.18 250);
--radius: 0.5rem;
}
.button {
background: var(--color-accent);
color: var(--color-bg);
border-radius: var(--radius);
}
3.1 Scoped Theming
Override at any level:
.card {
--color-bg: oklch(96% 0 0); /* only inside .card */
}
.card .button { background: var(--color-bg); }
3.2 Reading and Writing from JS
// Read computed value
getComputedStyle(document.documentElement).getPropertyValue('--color-accent');
// Write
document.documentElement.style.setProperty('--color-accent', 'oklch(70% 0.2 30)');
3.3 Typed Custom Properties
@property registers a custom property with a type — enables animation interpolation:
@property --gradient-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.box {
background: linear-gradient(var(--gradient-angle), red, blue);
animation: spin 4s linear infinite;
}
@keyframes spin {
to { --gradient-angle: 360deg; }
}
Without @property, animating a custom property would jump rather than interpolate.
4. Dark Mode
Two ways to wire it up.
4.1 OS Preference Only
:root {
--color-bg: white;
--color-fg: black;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: black;
--color-fg: white;
}
}
Simple, fully declarative, respects user setting.
4.2 Manual Toggle With OS Fallback
Add a data-theme attribute or class controlled by JS, persist preference in localStorage:
:root { color-scheme: light; --color-bg: white; --color-fg: black; }
:root[data-theme="dark"] { color-scheme: dark; --color-bg: black; --color-fg: white; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) { color-scheme: dark; --color-bg: black; --color-fg: white; }
}
<script>
// Apply theme before render to avoid flash of wrong theme
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.dataset.theme = saved;
</script>
color-scheme is the magic line — it tells the UA to use dark-mode variants for form controls, scrollbars, and the default text/background.
4.3 light-dark() (2024+)
One declaration, two values:
:root { color-scheme: light dark; }
body { background: light-dark(white, black); color: light-dark(black, white); }
Browser picks based on color-scheme. Cleaner than maintaining two variable sets.
5. Transitions
Animate property changes between two states.
.button {
background: oklch(60% 0.18 250);
transition: background 200ms ease-out, transform 150ms ease-out;
}
.button:hover {
background: oklch(70% 0.18 250);
transform: translateY(-1px);
}
Shorthand: transition: PROPERTY DURATION TIMING-FUNCTION DELAY. Comma-separate multiple transitions.
| Timing function | Curve |
|---|---|
linear |
Constant velocity. |
ease (default) |
Slow start, fast middle, slow end. |
ease-in |
Slow start. |
ease-out |
Slow end. Best for entering/expanding things. |
ease-in-out |
Slow at both ends. |
cubic-bezier(.4, 0, .2, 1) |
Custom — Material's default. |
linear(...) |
Multi-stop curves (2023+) — bouncing, spring-like motion without keyframes. |
5.1 Not All Properties Transition
Properties must have intermediate values. display: none ↔ block historically didn't transition. As of 2024 with transition-behavior: allow-discrete:
.modal {
display: none;
opacity: 0;
transition: opacity 200ms, display 200ms allow-discrete;
transition-behavior: allow-discrete;
}
.modal.open { display: block; opacity: 1; }
@starting-style defines the starting values when an element first appears:
@starting-style {
.modal.open { opacity: 0; }
}
5.2 Respect Reduced Motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
6. Animations
Reusable, multi-stage motion via @keyframes.
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.toast {
animation: fade-in-up 300ms ease-out;
}
Properties:
| Property | Meaning |
|---|---|
animation-name |
Keyframe name. |
animation-duration |
How long. |
animation-timing-function |
Same vocabulary as transitions. |
animation-delay |
Wait before starting. Negative = start partway through. |
animation-iteration-count |
Number of times or infinite. |
animation-direction |
normal, reverse, alternate, alternate-reverse. |
animation-fill-mode |
none, forwards (keep final state), backwards (apply initial state during delay), both. |
animation-play-state |
running, paused. |
Shorthand: animation: NAME DURATION TIMING DELAY ITER DIR FILL PLAY.
6.1 Scroll-Driven Animations (2024+)
Drive an animation by scroll progress instead of time:
@keyframes reveal {
from { opacity: 0; }
to { opacity: 1; }
}
.card {
animation: reveal linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
Solid in Chromium; partial in Firefox/Safari as of 2026 — feature-detect with @supports (animation-timeline: scroll()).
6.2 View Transitions API
CSS hook for cross-page or cross-state animations:
document.startViewTransition(() => updateDOM());
::view-transition-old(root) { animation: fade-out 200ms; }
::view-transition-new(root) { animation: fade-in 200ms; }
7. Transforms and Filters
7.1 Transforms
.box {
transform: translate(10px, 20px) rotate(15deg) scale(1.1);
transform-origin: center; /* pivot point */
}
/* Individual properties (cleaner, animatable independently) */
.box {
translate: 10px 20px;
rotate: 15deg;
scale: 1.1;
}
3D transforms — add transform-style: preserve-3d on parent, then rotateX, rotateY, translateZ, perspective(...).
7.2 Filters
img { filter: blur(2px) brightness(0.9) saturate(1.1); }
img:hover { filter: none; }
| Function | Effect |
|---|---|
blur(LEN) |
Gaussian blur. |
brightness(N) |
0 = black, 1 = unchanged. |
contrast(N) |
0 = grey, 1 = unchanged. |
grayscale(0-1) |
Desaturate. |
sepia(0-1) |
Brownish tint. |
invert(0-1) |
Colour invert. |
hue-rotate(deg) |
Shift hue. |
drop-shadow(...) |
Like box-shadow but follows alpha (great for icons). |
backdrop-filter |
Apply filter to what's behind the element — frosted glass. |
8. Cascade Layers, Nesting, Scope
8.1 Cascade Layers
Predictable overrides across many sources. Layers later in declaration win, regardless of selector specificity.
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
}
@layer base {
body { font-family: system-ui; }
}
@layer components {
.button { padding: 0.5em 1em; }
}
@layer utilities {
.hidden { display: none; } /* always wins over .button */
}
Unlayered styles win against any layered style by default. Great for incremental adoption — drop a vendor stylesheet into @layer vendor and it sits below your application styles.
8.2 Nesting
Native nesting (no Sass needed) — browser-supported since 2023:
.card {
padding: 1rem;
border-radius: 0.5rem;
& .title {
font-weight: bold;
}
&:hover {
background: oklch(96% 0 0);
}
@media (min-width: 768px) {
padding: 1.5rem;
}
}
& references the outer selector. Differences from Sass:
&is mandatory in front of an element selector (& span, not barespan) in the original spec; relaxed in 2023.- Doesn't flatten — the rule remains nested at compute time.
8.3 Scope (2024+)
Limit a rule's reach to a subtree:
@scope (.card) to (.card-footer) {
p { color: var(--color-muted); }
}
The to clause is the scope ceiling — descendants stop matching past it. Useful for component CSS that shouldn't leak into nested components of the same kind.
9. References
- MDN Media Queries — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries
- Utopia fluid type scale — https://utopia.fyi/
- MDN
@property— https://developer.mozilla.org/en-US/docs/Web/CSS/@property - Scroll-driven animations spec — https://drafts.csswg.org/scroll-animations-1/
- CSS Cascade Layers — https://www.w3.org/TR/css-cascade-5/#layering
- View Transitions API — https://drafts.csswg.org/css-view-transitions-1/