CSS Custom Properties and Media Queries
|
This section documents general HTML5 and CSS concepts — it is not tied to any specific framework or library. This content was generated with the assistance of AI. Verify it against current MDN documentation and browser-support tables (caniuse.com) before relying on it in production, since HTML/CSS features and browser support continue to evolve. |
This page covers two CSS features that make stylesheets more maintainable and adaptable: custom
properties (also called CSS variables), which let a value be named once and reused across many rules, and
media queries, which let styles be conditionally applied based on characteristics of the viewport or the
user’s system preferences. The two combine naturally in responsive design, and the page closes with
prefers-reduced-motion as a worked media-query example that matters for accessibility.
CSS custom properties
A CSS custom property (informally, a CSS variable) stores a value under a name so it can be reused in
multiple declarations instead of being repeated verbatim. A custom property is defined inside a ruleset,
with its name prefixed by a double hyphen (--), and its value can be any valid CSS value:
:root {
--color-primary: #FC9C9C;
}
p {
color: var(--color-primary, #FF0000);
}
The selector on the left of the declaration determines the property’s scope — the part of the document
tree where it is visible. :root (equivalent to the html element, but with higher specificity) is the
conventional place to define properties meant to be available document-wide, such as a theme’s palette or
spacing scale.
To read a custom property’s value, use the var() function. var() takes the property name as its first
argument and an optional second argument, the fallback value used if the property has not been set (for
example, in an older browser that does not support custom properties, or simply because the property name
was mistyped):
/* Falls back to #FF0000 if --color-primary is not defined in scope */
color: var(--color-primary, #FF0000);
A worked example
A page’s structural elements can share a consistent look by defining a small set of custom properties once and referencing them from every rule that needs them:
:root {
--bg-color: #659494;
--text-color: #fff;
--size-large: 30px;
--size-medium: 20px;
}
header,
nav,
section,
footer {
background: var(--bg-color);
color: var(--text-color);
font-size: var(--size-large);
padding: var(--size-large);
margin-bottom: var(--size-medium);
}
Changing the theme’s background color, or its spacing scale, now means editing a single declaration in
:root instead of hunting down every rule that used the old value. This is the main benefit of custom
properties: a semantic name (--bg-color) can be reused everywhere the raw value would otherwise have been
repeated, which keeps the value both more maintainable and easier to update consistently later.
Scoping
Because a custom property’s visibility follows the normal CSS cascade and inheritance rules, it can be
overridden for a specific part of the document simply by redeclaring it on a more specific selector. Any
descendant of that selector picks up the new value, while the rest of the document keeps the :root
default:
:root {
--bg-color: #659494;
}
.dark-section {
/* Overrides --bg-color only within .dark-section and its descendants */
--bg-color: #1a1a1a;
}
This scoping behavior is what makes custom properties useful for theming (see Light and Dark Theming for a dedicated treatment of light/dark palettes built on this pattern) and for component-level overrides, without needing a preprocessor.
Media queries
Media queries, introduced with CSS3, let a stylesheet apply a block of rules only when the browser or device matches a given condition — most commonly the viewport’s width, but also its height, orientation, or resolution. They are the mechanism behind responsive web design (RWD): a single page adapts its layout to phones, tablets, and desktops instead of shipping a separate page per device class.
A @media rule takes a media type (usually screen) and one or more features, combined with and:
@media screen and (max-width: 600px) {
body {
font-size: 14px;
}
.sidebar {
display: none;
}
}
max-width matches viewports up to (and including) the given width; min-width matches viewports at least
that wide. Combining both expresses a range, useful for styles meant only for an intermediate breakpoint
such as tablets:
@media screen and (min-width: 600px) and (max-width: 900px) {
.container {
width: 90%;
}
}
Mobile-first breakpoints
A common, recommended practice is to write the default (unqualified) styles for the smallest viewport
first, then layer on min-width media queries that progressively adjust the layout as the viewport grows — this is the mobile-first approach. It leans on the normal CSS cascade: later rules (inside a matching media
query) override the defaults, rather than every breakpoint having to restate a whole layout from scratch.
| Breakpoint | Typical device | Example query |
|---|---|---|
Default (no query) |
Phones |
Base styles, no |
|
Tablets (up to 900px) |
|
|
Desktops |
|
/* Default: mobile styles */
body {
font-size: 16px;
color: #333;
}
.sidebar {
background-color: #a2cce4;
padding: 20px;
min-width: 150px;
}
/* Tablets */
@media screen and (min-width: 600px) and (max-width: 900px) {
.container {
width: 90%;
}
}
/* Desktops */
@media screen and (min-width: 1200px) {
.container {
width: 1200px;
display: flex;
}
}
Media queries only take effect on mobile browsers once the page also declares a viewport meta tag — without it, mobile browsers render the page at a default desktop-sized viewport and then scale it down,
which defeats the purpose of the breakpoints above. See
HTML5 Structural and Semantic Elements for the
<meta name="viewport" content="width=device-width, initial-scale=1.0"> tag that must accompany a
responsive layout, and CSS Layout: Float, Flexbox, and Grid for how the
layout modes referenced above (flex, grid) typically respond to these breakpoints.
Custom properties inside media queries
Custom properties and media queries combine well: redeclaring a custom property’s value inside a @media
block lets a single set of semantically named "design tokens" (spacing, font sizes, column counts) change
per breakpoint, while every rule that consumes them via var() stays untouched:
:root {
--content-padding: 16px;
}
@media screen and (min-width: 1200px) {
:root {
--content-padding: 32px;
}
}
.main-content {
padding: var(--content-padding);
}
Accessibility example: prefers-reduced-motion
Media queries are not limited to viewport geometry — they can also match user-level accessibility
preferences configured at the operating-system level. prefers-reduced-motion reports whether the user has
asked their system to minimize non-essential motion (for example, macOS’s Reduce motion setting under
System Settings > Accessibility > Display, or Windows’s Settings > Ease of Access > Display > Show
animations). This matters because repetitive motion, parallax, and flicker effects can cause real
discomfort — including nausea for users with vestibular disorders — so animations should degrade
gracefully rather than forcing motion on everyone.
The media feature takes two values: reduce and no-preference. Gating an animation behind
no-preference means it only plays for users who have not asked for reduced motion:
.animation {
position: absolute;
top: 150px;
left: 150px;
}
@media (prefers-reduced-motion: no-preference) {
.animation {
animation: moveAround 1s 0.3s linear infinite both;
}
}
@keyframes moveAround {
from {
transform: translate(-50px, -50px);
}
to {
transform: translate(50px, 100px);
}
}
An equally common alternative is to write the animation unconditionally as the default and instead gate a
reduce rule that turns it off (or replaces it with an instant transition). This is generally preferable
when the animation is set up elsewhere in the stylesheet (for example alongside other rules for the same
component) since it avoids duplicating the whole ruleset inside the media query, and it fails safe: a
browser that does not yet support prefers-reduced-motion still gets the animation rather than silently
never getting it:
@media (prefers-reduced-motion: reduce) {
.animation {
animation: none;
}
}
See CSS Transitions and Animations for the animation and
@keyframes syntax used above in full, and
Forms Accessibility and Validation for other
accessibility-driven CSS techniques.