responsive-sass
v3.0.0
Published
Responsive sass mixins
Maintainers
Readme
responsive-sass
A library of mixins for creating responsive styles in SASS.
Features
- 🎯 Non-contiguous ranges -
(1,0,1,0,1)targets three separate zones in a single media query. Other libraries need three separate calls and emit three blocks. - 👨🎨 Multiple grids - You can create multiple grids and use them in the same project.
- 👩🔬 Smart rendering - breakpoints are parsed smart under the hood, (1,1,1,1,1) will just insert value as is without any media query.
- 📐 No overlapping boundaries - adjacent zones never both match at the exact breakpoint value, so source order can't silently decide the winner.
- 🕰️ No browser floor - plain
min-width/max-widthby default; opt into range syntax when your targets allow. - ♾️ Unlimited breakpoints - You can create your own mixin from constructor
responsivewith any number of breakpoints. - 📦 Container queries - the same zone syntax targets a container's width via
@container, not just the viewport. - 🧊 Nothing in your namespace - the package exports two mixins. You name your own.
Requirements
Dart Sass >= 1.23.0 (the release that introduced the module system). LibSass / node-sass are not supported, since they never implemented @use.
Output is plain min-width/max-width, so there is no browser floor beyond media query support itself. If you only target modern browsers, $ranges switches to the shorter Level 4 syntax.
Setup
npm install -D responsive-sasspnpm add -D responsive-sassyarn add -D responsive-sassbun add -d responsive-sassThe package is stylesheets only. Nothing of it reaches runtime, so it belongs in devDependencies.
Usage
The package exports two constructors and nothing else: responsive for @media, and container for @container. Build your own mixin on top. Breakpoints live in one place, so changing the grid is a one-line edit:
@use "responsive-sass" as *;
$breakpoints: 30rem, 45rem, 75rem, 100rem; // 480px, 720px, 1200px, 1600px
@mixin media ($values...) {
@include responsive ("all", $breakpoints, $values...) {
@content;
}
}div {
@include media (1,0,0,0,0) {
background-color: blue;
}
@include media (0,0,1,0,1) {
background-color: yellow;
}
}N breakpoints create N + 1 zones, so you pass N + 1 flags. 1 means "apply here", 0 means "skip". The flags above map to: <30rem, 30-45rem, 45-75rem, 75-100rem, >=100rem.
Output
@media (max-width: 29.999rem) {
div {
background-color: blue;
}
}
@media (min-width: 45rem) and (max-width: 74.999rem), (min-width: 100rem) {
div {
background-color: yellow;
}
}Two details in that output:
- The
allmedia type is omitted because it is the default. - Upper bounds step just below the breakpoint.
max-widthis inclusive, so an unadjustedmax-width: 30remwould match at the same width as the next zone'smin-width: 30remand source order would silently decide the winner. The step is 0.001 for font-relative units and 0.02 forpx, both below one device pixel.
Media types
The first argument is the media type, so screen and print variants are the same three lines. They can share the same $breakpoints:
@mixin mediaScreen ($values...) {
@include responsive ("screen", $breakpoints, $values...) {
@content;
}
}
@mixin mediaPrint ($values...) {
@include responsive ("print", $breakpoints, $values...) {
@content;
}
}div {
@include mediaScreen (1,1,0,1,1) {
background-color: blue;
}
@include mediaPrint (0,0,1,0,0) {
background-color: yellow;
}
}@media screen and (max-width: 44.999rem), screen and (min-width: 75rem) {
div {
background-color: blue;
}
}
@media print and (min-width: 45rem) and (max-width: 74.999rem) {
div {
background-color: yellow;
}
}Multiple grids
Nothing is global, so independent grids coexist. A component can have its own breakpoints without touching the page grid:
@mixin mediaSidebar ($values...) {
@include responsive ("all", (20rem, 60rem), $values...) {
@content;
}
}
.sidebar {
@include mediaSidebar (0,1,0) {
display: none;
}
}@media (min-width: 20rem) and (max-width: 59.999rem) {
.sidebar {
display: none;
}
}Container queries
container is the same constructor against a container's width instead of the viewport's, so a component can carry its own breakpoints and behave correctly in a wide column and a narrow sidebar alike:
@use "responsive-sass" as *;
.card-wrap { container-type: inline-size; container-name: card; }
@mixin mediaCard ($values...) {
@include container ("card", (20rem, 40rem, 60rem), $values...) {
@content;
}
}
.card {
@include mediaCard (0,1,0,1) {
display: grid;
}
}@container card ((min-width: 20rem) and (max-width: 39.999rem)) or (min-width: 60rem) {
.card {
display: grid;
}
}Pass null as the name to match the nearest ancestor container.
Two differences from responsive, both dictated by the CSS grammar:
- Alternatives are joined with
or, not commas.@containerdoes accept comma-separated condition lists per spec, but browsers only started shipping them recently, sooris the portable choice. - Two-sided clauses get wrapped in parentheses, because
andandorcannot be mixed at the same level.
Container names are <custom-ident>, so none, and, or, not, default and at are rejected by the parser. Passing one of them emits a warning.
Range syntax
If your support target allows it, $ranges: true switches to the Media Queries Level 4 range syntax:
@use "responsive-sass" as * with ($ranges: true);@media (max-width: 29.999rem) /* default */
@media (width < 30rem) /* $ranges */
@media (min-width: 45rem) and (max-width: 74.999rem) /* default */
@media (width >= 45rem) and (width < 75rem) /* $ranges */It is shorter and exact, because the upper bound is genuinely exclusive and needs no epsilon step. The cost is a browser floor: Chrome/Edge 104+, Firefox 63+, Safari 16.4+. A browser that cannot parse the condition discards the entire block, so every rule inside it is lost rather than degraded. Off by default for that reason.
