EthicsComputerCourses
Join training +91 90532 10052

Responsive Breakpoint Mixins

Sass · Lesson 20 of 26 Advanced 5 min read Updated

Every responsive site repeats the same few media queries: "from tablet width up", "below phone width", "only on tablets". Typing @media (width >= 48rem) in fifty places means fifty places to change when the design team moves the tablet breakpoint. A small set of breakpoint mixins gives those widths names and keeps them in one map.

In this lesson you will build that system step by step: a breakpoint map, a lookup function with a helpful error, up, down and between mixins, and a mixin for container queries. Frameworks such as Bootstrap ship mixins built on the same idea.

Step 1: one map of named widths

Choose breakpoints from your content and design, not from particular phone models. Store them in a map. Using rem means the breakpoints grow if a user increases their browser's default font size, which keeps the layout readable:

SCSS
@use "sass:map";

$breakpoints: (
  sm: 36rem,   // 576px at the default 16px
  md: 48rem,   // 768px
  lg: 64rem,   // 1024px
  xl: 80rem,   // 1280px
);

In a real project this map lives in a settings partial such as abstracts/_breakpoints.scss and is marked !default, so it can be configured with @use ... with.

Step 2: a lookup function that fails loudly

map.get() returns null for an unknown name, which would produce a broken media query. A small function turns that into a clear error (see @debug, @warn & @error):

SCSS
@function bp($name) {
  @if not map.has-key($breakpoints, $name) {
    @error "Unknown breakpoint `#{$name}`. Use: #{map.keys($breakpoints)}.";
  }
  @return map.get($breakpoints, $name);
}

Step 3: up, down and between

Modern CSS has range syntax for media queries, (width >= 48rem), supported in all current browsers. It reads naturally and avoids the old off-by-one problems. Each mixin wraps @content in the right query:

SCSSTry it yourself
@use "sass:map";

$breakpoints: (sm: 36rem, md: 48rem, lg: 64rem, xl: 80rem);

@function bp($name) {
  @if not map.has-key($breakpoints, $name) {
    @error "Unknown breakpoint `#{$name}`. Use: #{map.keys($breakpoints)}.";
  }
  @return map.get($breakpoints, $name);
}

@mixin up($name) {
  @media (width >= bp($name)) { @content; }
}

@mixin down($name) {
  @media (width < bp($name)) { @content; }
}

@mixin between($from, $to) {
  @media (bp($from) <= width < bp($to)) { @content; }
}

.product-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;

  @include up(sm) { grid-template-columns: repeat(2, 1fr); }
  @include up(lg) { grid-template-columns: repeat(4, 1fr); }
}

.promo-banner {
  @include down(sm) { display: none; }
}

.side-note {
  @include between(md, lg) { font-size: 0.875rem; }
}
Output
.product-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}
@media (width >= 36rem) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (width >= 64rem) {
  .product-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

@media (width < 36rem) {
  .promo-banner {
    display: none;
  }
}

@media (48rem <= width < 64rem) {
  .side-note {
    font-size: 0.875rem;
  }
}

This is the mobile-first approach: the base styles are for small screens, and up() adds styles as the screen grows. Use down() and between() for the few cases that only apply to a range.

Here is the compiled CSS on a page. Resize the preview to see the grid change from one column to two to four, and the promo banner disappear below 36rem:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Breakpoint mixins in action</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; }
    .item { background: #fce7f3; border-radius: 8px; padding: 1rem; }
    .promo-banner { background: #cc6699; color: white; padding: 0.75rem 1rem; border-radius: 8px; margin-bottom: 1rem; }
    /* compiled from the SCSS above */
    .product-grid {
      display: grid;
      gap: 1rem;
      grid-template-columns: 1fr;
    }
    @media (width >= 36rem) {
      .product-grid { grid-template-columns: repeat(2, 1fr); }
    }
    @media (width >= 64rem) {
      .product-grid { grid-template-columns: repeat(4, 1fr); }
    }
    @media (width < 36rem) {
      .promo-banner { display: none; }
    }
  </style>
</head>
<body>
  <p class="promo-banner">Diwali sale in Jaipur: 20% off handloom sarees</p>
  <div class="product-grid">
    <div class="item">Bandhani saree, ₹2,499</div>
    <div class="item">Block-print kurta, ₹899</div>
    <div class="item">Leheriya dupatta, ₹649</div>
    <div class="item">Mojari shoes, ₹1,199</div>
  </div>
</body>
</html>

Step 4: container queries

Media queries look at the viewport. Container queries look at the size of a parent element, so a card can change layout depending on the column it sits in, not on the screen. They work in all current browsers. The parent needs container-type: inline-size; a mixin keeps the query short:

SCSSTry it yourself
@mixin container($min, $name: null) {
  @container #{$name} (width >= #{$min}) {
    @content;
  }
}

.sidebar {
  container: sidebar / inline-size;
}

.profile-card {
  display: grid;
  gap: 0.5rem;

  @include container(24rem) {
    grid-template-columns: 6rem 1fr;
  }

  @include container(40rem, sidebar) {
    gap: 1.5rem;
  }
}
Output
.sidebar {
  container: sidebar/inline-size;
}

.profile-card {
  display: grid;
  gap: 0.5rem;
}
@container (width >= 24rem) {
  .profile-card {
    grid-template-columns: 6rem 1fr;
  }
}
@container sidebar (width >= 40rem) {
  .profile-card {
    gap: 1.5rem;
  }
}

Sass prints the container shorthand as sidebar/inline-size, without spaces around the slash, which is still valid CSS. When $name is null, the interpolation outputs nothing and the query applies to the nearest container. With a name, it targets that named container. See Container Queries for the CSS side.

Common mistakes

  • Too many breakpoints. Four or five named sizes are enough. Add a one-off query only when content truly breaks.
  • Breakpoints in px while text is in rem. If users enlarge text, px breakpoints switch too late. Use rem for both.
  • Desktop-first everywhere. Writing large-screen styles first and undoing them with down() produces more CSS. Start small and use up().
  • Typos in names. Without the bp() check, up(medium) silently produces a broken query. Keep the @error.

Key points

  • Keep breakpoints in one map with readable names, in rem, in a settings partial.
  • A bp() function with @error turns unknown names into clear messages.
  • up(), down() and between() mixins wrap @content in range-syntax media queries such as (width >= 48rem).
  • Range syntax avoids the old max-width: 767.98px trick because < and >= never overlap.
  • A container query mixin works the same way for component-level responsiveness.

Exercise

A Kochi tourism site wants a different breakpoint map: phone: 30rem, tablet: 50rem, desktop: 75rem. Write the map, the bp() function and an up() mixin. Use them on .tour-list: one column by default, two from tablet, three from desktop. Add .tour-filters that is position: sticky; top: 0; only from desktop up. Check what happens if you call up(laptop).

Show solution
SCSS
@use "sass:map";

$breakpoints: (phone: 30rem, tablet: 50rem, desktop: 75rem);

@function bp($name) {
  @if not map.has-key($breakpoints, $name) {
    @error "Unknown breakpoint `#{$name}`. Use: #{map.keys($breakpoints)}.";
  }
  @return map.get($breakpoints, $name);
}

@mixin up($name) {
  @media (width >= bp($name)) { @content; }
}

.tour-list {
  display: grid;
  grid-template-columns: 1fr;

  @include up(tablet) { grid-template-columns: repeat(2, 1fr); }
  @include up(desktop) { grid-template-columns: repeat(3, 1fr); }
}

.tour-filters {
  @include up(desktop) {
    position: sticky;
    top: 0;
  }
}

up(laptop) stops the build with "Unknown breakpoint laptop. Use: phone, tablet, desktop." instead of writing a broken media query.

Quick Quiz

1. Why store breakpoints in a map?

2. What does @include up(md) produce with md: 48rem?

3. Why did older mixins write max-width: 767.98px?

4. What does a container query respond to?

Learn Sass with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call