Media Queries

Difficulty: Intermediate

Media queries are the foundation of responsive web design. They allow you to apply CSS rules conditionally based on the characteristics of the user's device or viewport. The most common use case is changing layout and sizing based on viewport width, but media queries can also respond to height, orientation, resolution, color scheme preference, reduced motion preference, and more.

The basic syntax is @media (condition) { /* styles */ }. The condition is a media feature expression, most commonly a width comparison: @media (min-width: 768px) applies styles when the viewport is 768px or wider, while @media (max-width: 767px) applies styles when the viewport is 767px or narrower. You can combine conditions with logical operators: and (both must be true), or via comma-separation (either can be true), and not (negation).

Breakpoints are the viewport widths at which your layout changes. Common breakpoints align with device categories: 640px (mobile), 768px (tablet), 1024px (small desktop), 1280px (large desktop), and 1536px (extra-large). However, you should choose breakpoints based on where your content breaks, not based on specific device widths. If your layout looks awkward at 820px, add a breakpoint at 820px - do not force it into a predefined device bucket.

The debate between min-width and max-width breakpoints reflects two design philosophies. min-width is the mobile-first approach: you write base styles for mobile, then layer on complexity for larger screens. max-width is the desktop-first approach: you write base styles for desktop, then override for smaller screens. The industry standard today is mobile-first with min-width, because it results in simpler CSS, better performance on mobile devices (fewer overrides), and a progressive enhancement mindset.

Modern CSS also supports media query range syntax using standard comparison operators: @media (width >= 768px) is equivalent to @media (min-width: 768px), and @media (400px <= width <= 800px) creates a range. This newer syntax is more readable and has good browser support. Media queries can also be used in HTML with the media attribute on <link> tags to conditionally load entire stylesheets.

Code examples

Basic Media Query Syntax

/* Base styles (apply to all viewports) */
.container {
  padding: 16px;
  max-width: 100%;
}

.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

/* Tablet and up (min-width: 768px) */
@media (min-width: 768px) {
  .container {
    padding: 24px;
    max-width: 768px;
    margin: 0 auto;
  }
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop and up (min-width: 1024px) */
@media (min-width: 1024px) {
  .container {
    max-width: 1024px;
  }
  .grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
  }
}

Mobile-first approach: base styles define a single column. At 768px, the grid becomes 2 columns. At 1024px, it becomes 3 columns with larger gaps. Each breakpoint adds complexity rather than removing it.

Logical Operators and Range Syntax

/* AND: both conditions must be true */
@media (min-width: 768px) and (max-width: 1023px) {
  /* Tablet only */
  .sidebar { display: none; }
}

/* OR: comma-separated (either condition) */
@media (max-width: 640px), (orientation: landscape) and (max-height: 500px) {
  /* Small screens OR short landscape devices */
  .hero { min-height: 50vh; }
}

/* NOT: negate the entire query */
@media not print {
  /* Everything except print */
  .no-print { display: block; }
}

/* Modern range syntax (well-supported) */
@media (width >= 768px) {
  /* Same as min-width: 768px */
  .card { flex-direction: row; }
}

@media (400px <= width <= 800px) {
  /* Width between 400px and 800px */
  .banner { font-size: 1.25rem; }
}

The 'and' operator requires both conditions. Commas act as OR. The 'not' operator negates the entire query. Modern range syntax uses >= and <= for more readable conditions.

Non-Width Media Features

/* Dark mode preference */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #f8fafc;
    --border: #334155;
  }
  body {
    background: var(--bg);
    color: var(--text);
  }
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* High-resolution displays */
@media (min-resolution: 2dppx) {
  .logo {
    background-image: url('/logo@2x.png');
  }
}

/* Hover capability (touchscreen vs mouse) */
@media (hover: hover) {
  .button:hover {
    background: #2563eb;
  }
}

/* Print styles */
@media print {
  .navbar, .footer, .sidebar { display: none; }
  body { font-size: 12pt; }
}

Media queries go far beyond viewport width. prefers-color-scheme detects dark mode. prefers-reduced-motion respects accessibility settings. hover: hover targets devices with a mouse. print styles hide non-essential elements.

Conditional Stylesheet Loading

<!-- Load different stylesheets based on conditions -->
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="tablet.css" media="(min-width: 768px)">
<link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)">
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="dark.css" media="(prefers-color-scheme: dark)">

<!-- Note: All stylesheets are downloaded regardless of media match.
     The media attribute only controls when the styles are applied.
     However, non-matching stylesheets are loaded with lower priority,
     so they don't block rendering. -->

Media attributes on link tags control when styles are applied, not when they are downloaded. All files are downloaded, but non-matching ones are low priority and do not block rendering. This is useful for code-splitting CSS.

Key points

Concepts covered

@media, Breakpoints, min-width, max-width, Media Features, Logical Operators