Module: CSS
CSS·031·8 MIN READ

031: Media Queries and Mobile-First Enhancement

TOPICS COVERED: Media Queries and Mobile-First Enhancement

Learning outcomes

By the end, you can write media queries with modern range syntax and compatible min-width syntax; choose content-driven breakpoints; apply mobile-first enhancements; query user preferences; avoid device-specific assumptions; and debug conditional CSS.

Prerequisites and retrieval

Retrieve every responsive behavior achieved yesterday without a query. Identify one point where the portfolio actually becomes awkward, such as a main/aside relationship needing columns. Queries are for discrete conditional changes after fluid and intrinsic behavior has done its work.

Terminology

  • Media query: Conditional rule applying styles based on media type, features, or user preferences. — Source: MDN: Using media queries
  • Media feature: A tested characteristic such as width, orientation, hover, or prefers-reduced-motion. — Source: MDN: Using media queries
  • Breakpoint: Boundary where a query changes design behavior (course term).
  • Mobile-first query: Usually a min-width enhancement over narrow defaults (course term).
  • Range syntax: Level-4 comparison notation like (width >= 48rem), equivalent to min-width forms. — Source: MDN: Using media queries — syntax improvements
  • Logical operator: Keywords combining or negating conditions: and, not, and comma-as-or. — Source: MDN: Using media queries
  • Interaction media feature: Capabilities such as hover and pointer describing input modality. — Source: MDN: Using media queries
  • Container query (@container): "A conditional rule based on a container's size, enabling component-level responsiveness (distinct from @media which queries the viewport)." — Source: CSS Containment Module Level 3: Container Queries — not used as the primary tool in this lesson; container queries are taught in 038
  • Preference media feature (prefers-reduced-motion): "A media feature that indicates whether the user prefers reduced motion." — Source: MDN: prefers-reduced-motion

Mental model: conditional patches over a complete base

The base stylesheet should be a complete, usable narrow layout. A wider query changes only declarations whose relationships need enhancement. This reduces overrides and keeps unsupported or unmatched conditions safe.

css
/* Equivalent width conditions */
@media (min-width: 48rem) { }
@media (width >= 48rem) { }

Range syntax is readable and broadly available in current browsers. min-width remains familiar and safe for established support targets. Pick one project convention. Width in em or rem makes breakpoints relate reasonably to text scale; exact conversion can vary with browser query processing, so test rather than relying on arithmetic alone.

Breakpoints should describe content pressure, not “iPad” or “desktop.” Open DevTools, resize until the narrow base has enough room for the intended relationship, then choose a nearby maintainable boundary.

Beginner example: one-column to two-column case study

Narrow default:

css
.portfolio-layout {
  display: grid;
  gap: 2rem;
}

.portfolio-layout__aside {
  padding: 1rem;
  border-block-start: 4px solid rgb(37 99 235);
  background: rgb(239 246 255);
}

Enhancement:

css
@media (min-width: 48rem) {
  .portfolio-layout {
    grid-template-columns: minmax(0, 2fr) minmax(14rem, 1fr);
    align-items: start;
  }

  .portfolio-layout__aside {
    position: sticky;
    inset-block-start: 1rem;
    border-block-start: 0;
    border-inline-start: 4px solid rgb(37 99 235);
  }
}

Below 48rem, Grid has one implicit column and source order stacks main then aside. Above it, two explicit tracks appear. Sticky behavior is introduced only where the sidebar has room and is less likely to obscure the main content. If it becomes too tall, remove sticky; width alone cannot guarantee content height.

Test at 47.99rem, 48rem, and much wider, but also test zoom and long content. A boundary should not depend on one screenshot.

Intermediate example: header and spacing enhancements

Base:

css
.site-header { padding-block: 1rem; }
.nav-list { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.hero { padding-block: 3rem; }

Enhance only when useful:

css
@media (min-width: 40rem) {
  .site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 2rem;
  }

  .nav-list { gap: 0.75rem; }
}

@media (min-width: 64rem) {
  .hero { padding-block: 6rem; }
}

The HTML order remains brand then nav. The base navigation wraps naturally. Do not hide links and display a fake menu icon without implementing an accessible disclosure button and behavior. CSS cannot provide all interactive state management.

Queries may combine conditions:

css
@media (min-width: 48rem) and (orientation: landscape) { }

Use combinations sparingly; they can create untested gaps and overlaps. Avoid orientation locking. Design should generally support either orientation.

User preference and input capability queries

Motion preference is not a width issue:

css
html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .project-card { transition: none; }
}

Hover enhancements should apply only when hover is available:

css
@media (hover: hover) and (pointer: fine) {
  .project-card:hover {
    transform: translateY(-0.2rem);
    box-shadow: 0 0.75rem 1.5rem rgb(15 23 42 / 12%);
  }
}

Hover must never be required to reveal essential content. Devices can have multiple input types, and capabilities are more nuanced than “touch versus desktop.” Always preserve focus styles independently.

Optional advanced example: print

css
@media print {
  .site-nav,
  .back-to-top { display: none; }
  body { color: black; background: white; }
  a { color: inherit; text-decoration: underline; }
  .project-card { break-inside: avoid; box-shadow: none; }
}

Print CSS is useful for portfolio resumes/case studies. Do not hide URLs or essential navigation context without checking the printed result. Print preview is the test environment.

Mistakes, debugging, and DevTools

  • Writing desktop defaults then undoing them in many max-width queries.
  • Selecting breakpoints from popular device dimensions.
  • Duplicating complete component rules inside every query instead of patching differences.
  • Overlapping queries with contradictory declarations and unclear source order.
  • Forgetting units: (min-width: 48) is invalid for nonzero length.
  • Putting @media inside a selector in plain CSS as if using a preprocessor.
  • Assuming width queries detect input method.
  • Making information hover-only.
  • Testing only immediately around breakpoints rather than all intermediate sizes.

DevTools shows media-query rules in Styles only when matched, often with clickable source links. Responsive mode can display query bars and emulate reduced motion or print. If a rule is absent, check syntax and condition; if crossed out, ordinary cascade still applies. Use window.matchMedia('(min-width: 48rem)').matches in Console as an optional diagnostic.

Accessibility and performance

Media queries must not remove essential content at narrow widths. Keep DOM order and focus sequence logical across every condition. Support zoom, orientation, reduced motion, contrast, and input variation where relevant. Preference queries enhance accessibility but do not excuse inaccessible defaults.

CSS inside unmatched queries is still downloaded in the same stylesheet, so media queries are not a general code-splitting mechanism. Keep rules concise. Do not load large background images merely because a width query matches; CSS resources may still be discovered, and width does not guarantee bandwidth.

Deep dive: media-query syntax beyond width

Width:

css
@media (width >= 48rem) {
  ...
}

Bounded range:

css
@media (30rem <= width < 60rem) {
  ...
}

Orientation:

css
@media (orientation: landscape) {
  ...
}

Input capability:

css
@media (hover: hover) and (pointer: fine) {
  ...
}

User preferences:

css
@media (prefers-reduced-motion: reduce) {
  ...
}

@media (prefers-color-scheme: dark) {
  ...
}

@media (prefers-contrast: more) {
  ...
}

Do not infer device identity from one feature. A laptop can have touch; a tablet can have a mouse.

Worked example: reduced-motion safe transition system

Base:

css
.button,
.card {
  transition:
    background-color 160ms ease,
    color 160ms ease,
    transform 160ms ease;
}

.card:hover {
  transform: translateY(-3px);
}

Preference override:

css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

For real projects, prefer targeted reductions when possible so essential state changes remain perceptible. The broad pattern is useful as a safety net but should not replace thoughtful motion design.

Deep dive: dark mode is more than swapping backgrounds

css
:root {
  color-scheme: light dark;
  --surface: #ffffff;
  --text: #0f172a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: #0f172a;
    --text: #f8fafc;
  }
}

body {
  color: var(--text);
  background: var(--surface);
}

color-scheme tells the browser which schemes the page supports, helping built-in controls and scrollbars use appropriate system styling. Re-test contrast in both themes; inverting every color mechanically is not enough.

Deep dive: feature queries with @supports

Use a feature query when enhancement depends on CSS support:

css
.card-list {
  display: flex;
  flex-wrap: wrap;
}

@supports (display: grid) {
  .card-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  }
}

Modern browsers support Grid, so this exact fallback is rarely necessary today. The lesson is the pattern: build a usable baseline, then enhance based on capability when your support matrix requires it.

Worked example: mobile-first navigation without device names

Base:

css
.site-header {
  display: grid;
  gap: 1rem;
}

.nav-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
}

At the point where the brand and navigation comfortably fit beside each other:

css
@media (width >= 42rem) {
  .site-header {
    grid-template-columns: auto 1fr;
    align-items: center;
  }

  .nav-list {
    justify-content: end;
  }
}

The breakpoint is a content decision. If labels change and wrapping becomes awkward, the breakpoint should be reconsidered.

Bridge: media queries versus container queries

Media queries ask about the environment/viewport:

css
@media (width >= 60rem) {
  .product-card { ... }
}

Container queries ask about the space available to a component:

css
.product-list {
  container-type: inline-size;
}

@container (width >= 30rem) {
  .product-card {
    grid-template-columns: 8rem 1fr;
  }
}

If the same card appears in a sidebar and a main content area at the same viewport width, container queries can adapt it more accurately. They receive a dedicated lesson later.

Query-debugging checklist

  1. Is the condition true right now?
  2. Is another later query overriding it?
  3. Are units interpreted as expected?
  4. Does browser zoom expose a layout assumption?
  5. Is the feature query testing the property/value you actually rely on?
  6. Are you querying viewport width when component width is the real dependency?
  7. Could an intrinsic solution remove the query entirely?

Tiered exercises

Checkpoint: justify and document each boundary

For every breakpoint, write a sentence before writing CSS: “At approximately 48rem, the main article and 14rem aside can coexist with a 2rem gap while preserving readable measure.” If the sentence names only a device, continue testing. If two components fail at different widths, they do not need to share a breakpoint merely to make a tidy list.

Plot the cascade around one query. The base .hero declarations always participate. When the condition matches, only repeated properties are candidates for override; untouched base properties remain. Specificity still applies inside and outside queries, and later source order only breaks ties. A query does not make its declarations inherently stronger.

Use emulation carefully. Reduced-motion emulation verifies the query matches, but also inspect whether any motion remains from libraries, animated images, or JavaScript. Hover emulation cannot represent every hybrid device. Design a usable base for all input types, then let capability queries add optional affordances.

After implementing, sweep all widths rather than jumping between boundaries. Watch for scrollbar flashes, one-word orphans, wrapped controls, sticky collisions, and abrupt spacing. A query can solve one width and create a narrow failure range immediately above it; continuous testing exposes that gap.

Audit query ownership before finishing. Place a component's straightforward enhancements near its base or use a consistent query section, following the project's convention. Avoid repeating the same boundary in scattered files without reason. Comments should explain why a nonobvious boundary exists, not restate min-width.

Finally, print or list every condition and verify there is a usable base below the first, predictable behavior between boundaries, and no accidental overlap from min/max combinations. Preference queries are independent axes: reduced motion can match at any width. Think of conditions as intersecting states, then test important intersections such as narrow plus reduced motion and wide plus keyboard input.

Foundation: Start with a one-column layout. Add one min-width query that creates main/aside tracks only when content has enough room.

Core: Enhance header alignment and hero spacing at independently justified breakpoints. Record the content failure or opportunity that motivated each boundary.

Stretch: Add reduced-motion handling and a hover-only decorative effect guarded by capability queries. Verify keyboard focus remains equally clear.

css
.portfolio-layout { display: grid; gap: 2rem; }
.site-header { padding-block: 1rem; }
@media (min-width: 40rem) {
  .site-header { display: flex; align-items: center; justify-content: space-between; gap: 2rem; }
}
@media (min-width: 48rem) {
  .portfolio-layout { grid-template-columns: minmax(0, 2fr) minmax(14rem, 1fr); align-items: start; }
  .portfolio-layout__aside { position: sticky; inset-block-start: 1rem; }
}
@media (min-width: 64rem) { .hero { padding-block: 6rem; } }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .project-card { transition: none; }
}
@media (hover: hover) and (pointer: fine) {
  .project-card:hover { transform: translateY(-.2rem); }
}

Recap and exit questions

Media queries are targeted conditional enhancements over a resilient base. Choose boundaries from content, keep changes small, and query preference/capability rather than inferring them from screen width.

  1. What makes a stylesheet mobile-first?
  2. Why is “tablet breakpoint” a weak justification?
  3. Are (min-width: 48rem) and (width >= 48rem) equivalent?
  4. Which query should guard nonessential hover effects?
  5. How do you debug a query that does not appear in Styles?

Official references