Responsive Design with SCSS Media Queries: 10 Code-Along Examples

Learn responsive SCSS by building it. Ten copy-and-run examples covering nested media queries, breakpoint variables and maps, a respond-to mixin with @content, range queries, generated utilities, a responsive grid, and dark mode.

The SCSS media queries guide explains the progression: plain media queries work but scatter responsive rules away from the styles they affect, and SCSS fixes that by letting you nest queries, store breakpoints once, and wrap the repetition in mixins. This workbook builds that toolkit step by step. You start by nesting a single query, then construct a reusable respond-to mixin backed by a breakpoint map, and finish with responsive utilities and a real card grid. From Example 4 onward the examples share a small partial called _breakpoints.scss, which becomes your single source of truth.

1. Nest a media query inside the selector

In plain CSS, the responsive version of a component lives in a separate @media block, often far down the file. SCSS lets you write the query right inside the selector, so the base style and its responsive override sit together.

// styles.scss
.card {
padding: 1rem;
background: #f0f4f8;
// the responsive rule lives next to the base style
@media (min-width: 768px) {
padding: 2rem;
background: #d9e2ec;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif">
<div class="card">Resize the window past 768px</div>
</body>
</html>

When this compiles, SCSS produces an ordinary @media block in styles.css; the only thing that changed is where you got to write it. Keeping the query beside the rule it modifies is the single biggest readability win SCSS gives you here.

2. Store breakpoints in variables

Hard-coded pixel values like 768px scattered through a file are magic numbers. Define them once as variables and every query refers to the same source, so a redesign is a one-line change.

// styles.scss
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
.box {
font-size: 1rem;
@media (min-width: $breakpoint-md) { font-size: 1.25rem; }
@media (min-width: $breakpoint-lg) { font-size: 1.5rem; }
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<p class="box">This text grows at two breakpoints.</p>
</body>
</html>

Change $breakpoint-md once and every query that uses it updates together. This is the first step toward treating your breakpoints as data rather than as numbers you retype.

3. Wrap the repetition in a mixin with @content

Typing @media (min-width: ...) everywhere is exactly the repetition mixins exist to remove. The @content directive is what makes a media-query mixin possible: it marks the spot where the caller’s styles get dropped in.

// styles.scss
$breakpoint-md: 768px;
@mixin from-md {
@media (min-width: $breakpoint-md) {
@content; // the caller's style block is inserted here
}
}
.panel {
display: block;
@include from-md {
display: flex;
gap: 1rem;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<div class="panel">
<div style="flex:1;background:#e8eef5;padding:1rem">A</div>
<div style="flex:1;background:#d9e2ec;padding:1rem">B</div>
</div>
</body>
</html>

Whatever you put between the braces of @include from-md { ... } lands where @content sits. Without @content a mixin can only emit fixed declarations; with it, a mixin can wrap arbitrary blocks, which is precisely what a media-query helper needs.

4. Build the respond-to mixin

Now combine variables and @content into the named helper the article describes. Friendly names like md and lg read better than raw pixels, and @if/@else maps each name to its query. Put this in its own partial so every later example can reuse it.

// _breakpoints.scss (the leading underscore marks it a partial)
$breakpoint-sm: 480px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
@mixin respond-to($name) {
@if $name == sm {
@media (min-width: $breakpoint-sm) { @content; }
} @else if $name == md {
@media (min-width: $breakpoint-md) { @content; }
} @else if $name == lg {
@media (min-width: $breakpoint-lg) { @content; }
} @else {
@error "Unknown breakpoint: #{$name}";
}
}
// styles.scss
@use 'breakpoints' as *; // pull in the partial, no prefix
.headline {
font-size: 1.25rem;
@include respond-to(md) { font-size: 2rem; }
@include respond-to(lg) { font-size: 2.5rem; }
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<h1 class="headline">Responsive headline</h1>
</body>
</html>

The @error line is a small but valuable touch: misspell a breakpoint name and the compile fails loudly instead of silently emitting nothing. Note that you @use the partial by its name without the underscore or extension.

5. Go mobile-first

The cleanest way to use respond-to is mobile-first: write the base styles for the smallest screen, then layer enhancements with min-width as the screen grows. The base case needs no query at all.

// styles.scss
@use 'breakpoints' as *;
.nav {
display: flex;
flex-direction: column; // mobile first: stacked
gap: 0.5rem;
@include respond-to(md) {
flex-direction: row; // wider screens: side by side
gap: 1.5rem;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<nav class="nav">
<a href="#">Home</a>
<a href="#">Articles</a>
<a href="#">Shop</a>
</nav>
</body>
</html>

Reading top to bottom, the default is the mobile layout and each respond-to adds capability for larger screens. Mobile-first with min-width tends to produce less code than the reverse, because the simplest layout needs no overrides.

6. Upgrade the partial to a breakpoint map

The @if/@else chain works but grows awkwardly with every new breakpoint. A Sass map holds all breakpoints as data, and map.get looks one up by name. Replace your _breakpoints.scss with this version. The respond-to interface is identical, so nothing in Examples 5, and 7 through 9 has to change.

// _breakpoints.scss (map-driven, the version used from here on)
@use 'sass:map';
$breakpoints: (
sm: 480px,
md: 768px,
lg: 1024px,
xl: 1280px,
);
@mixin respond-to($name) {
@if map.has-key($breakpoints, $name) {
@media (min-width: map.get($breakpoints, $name)) {
@content;
}
} @else {
@error "Unknown breakpoint `#{$name}`. Use one of: #{map.keys($breakpoints)}";
}
}

Adding a breakpoint is now a single new line in the map, and the error message even lists the valid names. This is the single source of truth the guide recommends, expressed as a map rather than a pile of variables. The @use 'sass:map' line imports Sass’s built-in map module so you can call map.get and friends.

7. Generate responsive utilities with @each

Once breakpoints are a map, you can loop over them. @each walks the map and stamps out a set of classes per breakpoint, which is how utility frameworks build their responsive variants automatically.

// styles.scss
@use 'sass:map';
@use 'breakpoints' as *;
// make .text-center plus .sm:text-center, .md:text-center, etc.
.text-center { text-align: center; }
@each $name, $width in $breakpoints {
@media (min-width: $width) {
.#{$name}\:text-center { text-align: center; }
.#{$name}\:text-left { text-align: left; }
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<p class="text-center md:text-left">Centred on mobile, left-aligned from 768px.</p>
</body>
</html>

The #{$name} is interpolation, dropping the breakpoint name into the class, and the \: escapes the colon so md:text-left is a legal class name. One short loop produces a whole responsive utility set, and adding a breakpoint to the map extends every utility for free.

8. Add a range query with a between mixin

Sometimes you want styles that apply only between two breakpoints, not from one upward. Add a between mixin to the partial that combines min-width and max-width.

// add to _breakpoints.scss
@mixin between($lower, $upper) {
@media (min-width: map.get($breakpoints, $lower))
and (max-width: map.get($breakpoints, $upper) - 1px) {
@content;
}
}
// styles.scss
@use 'breakpoints' as *;
.tablet-only {
background: #f0f4f8;
// applies only on mid-size screens, 768px up to just below 1024px
@include between(md, lg) {
background: #1f4e78;
color: white;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<div class="tablet-only" style="padding:2rem">Only highlighted between md and lg.</div>
</body>
</html>

Subtracting 1px from the upper bound stops the range from overlapping the next breakpoint, a common off-by-one source of two queries firing at once. Range queries are the exception rather than the rule, but having a named mixin keeps the awkward syntax in one place.

9. A responsive card grid

Time to use the toolkit on something real. This grid shows one column on mobile, two from the medium breakpoint, and three from the large one, all expressed with respond-to right inside the selector.

// styles.scss
@use 'breakpoints' as *;
.grid {
display: grid;
grid-template-columns: 1fr; // mobile: single column
gap: 1rem;
@include respond-to(md) { grid-template-columns: repeat(2, 1fr); }
@include respond-to(lg) { grid-template-columns: repeat(3, 1fr); }
}
.grid .card {
background: #f0f4f8;
padding: 1.5rem;
border-radius: 8px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif;padding:1rem">
<div class="grid">
<div class="card">One</div>
<div class="card">Two</div>
<div class="card">Three</div>
<div class="card">Four</div>
<div class="card">Five</div>
<div class="card">Six</div>
</div>
</body>
</html>

The whole responsive story for this component lives in three lines, each reading like plain English. This is the payoff of the earlier examples: the complexity is hidden in the mixin, and the component stays declarative.

10. Beyond width: dark mode and orientation

Media queries are not only about width. The same mixin idea wraps any media feature, so you can offer named helpers for a user’s colour-scheme preference and device orientation.

// styles.scss
@mixin dark-mode {
@media (prefers-color-scheme: dark) { @content; }
}
@mixin landscape {
@media (orientation: landscape) { @content; }
}
body {
background: #ffffff;
color: #11243a;
@include dark-mode {
background: #11243a;
color: #f0f4f8;
}
}
.banner {
padding: 1rem;
@include landscape { padding: 2rem 4rem; }
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body style="margin:0;font-family:sans-serif">
<div class="banner">Switch your OS to dark mode to see this respond.</div>
</body>
</html>

Toggle your operating system’s dark mode and the page follows, with no JavaScript. The lesson is that the mixin pattern generalises: anything you can express as a @media condition can become a clean, named helper.

Work through these and you will have built a complete responsive toolkit: nested queries for locality, a breakpoint map as the single source of truth, a respond-to mixin for everyday use, a between mixin for ranges, generated utilities, and helpers for dark mode and orientation. The closing advice from the guide holds up well. Keep your breakpoints in one place, nest queries inside the selectors they affect, comment any dense responsive section, and always test on real devices rather than just dragging the browser window. Do that and your stylesheets stay organised no matter how many screen sizes you support.

See you soon.

View Comments (1)

Leave a Reply

Prev Next

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading