FullStack Course LogoFullStack Course
Module: CSS
CSS·031·10 MIN READ

031: Media Queries and Mobile-First Enhancement

TOPICS COVERED: Media Queries and Mobile-First Enhancement

Learning outcomes

By the end of this lesson, you should be able to write media queries using both modern range syntax and compatible min-width syntax, choose breakpoints from content pressure, build mobile-first enhancements, query user preferences and input capabilities, avoid assumptions about named devices, and debug conditional CSS.

Prerequisites and retrieval

Start by recalling the responsive behavior you achieved yesterday without media queries. Find one place where the portfolio begins to feel awkward, perhaps the point where the main content and aside need to become columns. That is the kind of discrete change a query is for. Let fluid and intrinsic layout do as much work as possible first.

Terminology

  • Media query: A conditional rule that applies styles according to a media type, media feature, or user preference. — Source: MDN: Using media queries
  • Media feature: A characteristic being tested, such as width, orientation, hover capability, or prefers-reduced-motion. — Source: MDN: Using media queries
  • Breakpoint: The boundary at which a query changes the design's behavior (course term).
  • Mobile-first query: A min-width enhancement layered on top of narrow-screen defaults (course term).
  • Range syntax: Level-4 comparison notation such as (width >= 48rem), which expresses the same condition as a min-width form. — Source: MDN: Using media queries — syntax improvements
  • Logical operator: A keyword that combines or negates conditions: and, not, or comma-as-or. — Source: MDN: Using media queries
  • Interaction media feature: A capability such as hover or pointer that describes the available 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

Treat the base stylesheet as a complete, usable narrow layout, not as a half-built desktop design waiting for overrides. A wider query should change only the declarations whose relationships genuinely need to be enhanced. That approach limits overrides and leaves a safe result when a condition does not match or is not supported.

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

Range syntax is readable and works in current browsers. min-width is still familiar and remains a sensible choice when an established support target expects it. Choose one convention for a project rather than mixing styles without a reason. Breakpoint widths in em or rem relate reasonably to text scale, but browser query processing can make exact conversions vary. Test the behavior instead of trusting arithmetic alone.

A breakpoint should describe the pressure your content encounters, not a device label such as “iPad” or “desktop.” In DevTools, resize the page until the narrow layout no longer supports the intended relationship, then choose a nearby boundary that will be easy to maintain.

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, so source order naturally stacks the main content before the aside. Above that boundary, two explicit tracks provide room for the relationship. The sticky aside is introduced only once there is enough horizontal space and less risk of obscuring the main content. If the aside becomes very tall, remove sticky; width by itself cannot guarantee a manageable content height.

Test at 47.99rem, 48rem, and a much wider size. Also test zoom and long content. A breakpoint should survive more than the single screenshot that motivated it.

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 the extra space solves a real layout problem:

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 is still brand followed by navigation. The base navigation wraps naturally, so it remains usable before the enhancement applies. Do not hide the links and display a decorative menu icon unless you also build an accessible disclosure button and its behavior. CSS can change presentation, but it cannot provide the complete interactive state management.

Queries can combine conditions:

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

Use combinations deliberately. They can create untested gaps or overlapping states. Avoid locking the design to one orientation; in general, it should remain usable in both.

User preference and input capability queries

Motion preference is a separate concern from width:

css
html { scroll-behavior: smooth; }

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

Only add hover decoration when the current environment reports that 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 the only way to reveal essential content. A device may have more than one input type, so “touch versus desktop” is too crude a model. Keep focus styles present independently of these enhancements.

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 and case studies. Before hiding URLs or navigation context, inspect the printed result and make sure the document still makes sense. Print preview is the relevant test environment here.

Mistakes, debugging, and DevTools

  • Starting with desktop defaults and then undoing them through many max-width queries.
  • Choosing breakpoints from popular device dimensions instead of from content behavior.
  • Copying complete component rules into every query rather than patching only the differences.
  • Allowing overlapping queries to contradict each other, leaving the result dependent on unclear source order.
  • Omitting units: (min-width: 48) is invalid for a nonzero length.
  • Nesting @media inside a selector in plain CSS as though a preprocessor were involved.
  • Treating width queries as if they identified the input method.
  • Making information available only on hover.
  • Testing only immediately beside breakpoint boundaries and ignoring the sizes between them.

In DevTools, matched media-query rules appear in the Styles pane, often with links back to their source. Responsive mode can show query bars and emulate reduced motion or print. If a rule is missing, inspect its syntax and condition. If it is crossed out, the normal cascade is still competing with it. As an optional Console check, use window.matchMedia('(min-width: 48rem)').matches to ask whether a condition currently matches.

Accessibility and performance

Media queries must not remove essential content at narrow widths. Preserve a logical DOM order and focus sequence in every state. Test zoom, orientation, reduced motion, contrast, and input variation when they affect the design. Preference queries are useful accessibility enhancements, but they do not make an inaccessible default acceptable.

Rules inside an unmatched query are still downloaded as part of the same stylesheet, so media queries are not a general code-splitting mechanism. Keep the rules concise. Do not load large background images merely because a width query matches: CSS resources may still be discovered, and width tells you nothing certain about available 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 use one feature to infer a device's identity. A laptop may have touch, and a tablet may 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;
  }
}

In a real project, targeted reductions are usually preferable because essential state changes should remain perceptible. This broad rule is a useful safety net, not a substitute for making deliberate motion decisions.

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, allowing built-in controls and scrollbars to use suitable system styling. Check contrast in both themes. Mechanically inverting every color is not enough.

Deep dive: feature queries with @supports

Use a feature query when the enhancement depends on whether a CSS property or value is supported:

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

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

Grid is supported by modern browsers, so this exact fallback is uncommon today. The durable lesson is the pattern: establish a usable baseline, then enhance for a capability when the support matrix calls for 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 width where the brand and navigation can sit beside each other comfortably:

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

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

That boundary is a content decision. If the labels change and wrapping becomes awkward, revisit the boundary rather than preserving it because it once represented a particular device.

Bridge: media queries versus container queries

Media queries ask about the environment, usually the 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 in the main content at one viewport width, the two containers may offer very different space. A container query can respond to that difference more accurately. Container queries receive their own dedicated lesson later.

Query-debugging checklist

  1. Is the condition true at this moment?
  2. Is a later query overriding the declaration?
  3. Are the units being interpreted as intended?
  4. Does browser zoom reveal an assumption in the layout?
  5. Does the feature query test the exact property and value the enhancement depends on?
  6. Are you querying viewport width when the component's container width is the real dependency?
  7. Could an intrinsic layout solution remove the query entirely?

Tiered exercises

Checkpoint: justify and document each boundary

Before writing CSS for each breakpoint, write the reason in a sentence: “At approximately 48rem, the main article and 14rem aside can coexist with a 2rem gap while preserving readable measure.” If your sentence names only a device, keep testing. If two components fail at different widths, they do not need to share a breakpoint just to produce a neat list.

Trace the cascade around one query. The base .hero declarations always participate. Once the condition matches, only the repeated properties are candidates for override; untouched base properties remain in effect. Specificity applies both inside and outside queries, and source order breaks ties only after specificity is considered. A query does not give its declarations automatic extra strength.

Use emulation with care. Reduced-motion emulation confirms that the query matches, but you must also check for motion supplied by libraries, animated images, or JavaScript. Hover emulation cannot represent every hybrid device. Make the base usable for every input type, then use capability queries for optional affordances.

After implementation, sweep through all widths instead of hopping only between boundaries. Look for scrollbar flashes, one-word orphans, wrapped controls, sticky collisions, and abrupt spacing changes. A query may fix one width while creating a narrow failure range just above it; continuous testing makes that gap visible.

Before finishing, audit who owns each query. Keep a component's straightforward enhancements near its base or follow the project's consistent query section convention. Do not scatter the same boundary across files without a reason. Comments should explain a nonobvious boundary, not repeat min-width.

Finally, list every condition and verify three things: a usable base exists below the first boundary, behavior between boundaries is predictable, and min/max combinations do not overlap accidentally. Preference queries are independent axes: reduced motion may match at any width. Think of conditions as intersecting states, then test important combinations such as narrow plus reduced motion and wide plus keyboard input.

Foundation: Begin with a one-column layout. Add one min-width query that creates main/aside tracks only when the 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 decorative hover-only effect guarded by capability queries. Confirm that 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 should be focused conditional enhancements layered over a resilient base. Choose boundaries from content behavior, keep each change small, and query preferences or capabilities directly instead of trying to infer 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

Reader page: /css/lesson/031/media-queries-and-mobile-first-enhancement