v6: Range media queries (#41786)
BrowserStack / browserstack (push) Has been cancelled Details

* wip

Co-Authored-By: mdo <98681+mdo@users.noreply.github.com>

* linty linterton

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mdo <98681+mdo@users.noreply.github.com>
This commit is contained in:
Mark Otto 2025-10-02 15:24:19 -07:00 committed by GitHub
parent aaf730a893
commit 7457bd8772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 128 additions and 35 deletions

View File

@ -21,7 +21,7 @@
@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map.keys($breakpoints)) { @function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map.keys($breakpoints)) {
$n: list.index($breakpoint-names, $name); $n: list.index($breakpoint-names, $name);
@if not $n { @if not $n {
@error "breakpoint `#{$name}` not found in `#{$breakpoints}`"; @error "breakpoint `#{$name}` not found in `#{$breakpoint-names}`";
} }
@return if($n < list.length($breakpoint-names), list.nth($breakpoint-names, $n + 1), null); @return if($n < list.length($breakpoint-names), list.nth($breakpoint-names, $n + 1), null);
} }
@ -35,18 +35,19 @@
@return if($min != 0, $min, null); @return if($min != 0, $min, null);
} }
// Maximum breakpoint width. // Maximum breakpoint width for range media queries.
// The maximum value is reduced by 0.02px to work around the limitations of // Returns the breakpoint value to use as an upper bound in range queries.
// `min-` and `max-` prefixes and viewports with fractional widths.
// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
// See https://bugs.webkit.org/show_bug.cgi?id=178261
// //
// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) // >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px))
// 767.98px // 576px
// >> breakpoint-max(xxl, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px))
// null
@function breakpoint-max($name, $breakpoints: $grid-breakpoints) { @function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
@if $name == null {
@return null;
}
$max: map.get($breakpoints, $name); $max: map.get($breakpoints, $name);
@return if($max and $max > 0, $max - .02, null); @return if($max and $max > 0, $max, null);
} }
// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front. // Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.
@ -65,7 +66,7 @@
@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) { @mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($name, $breakpoints); $min: breakpoint-min($name, $breakpoints);
@if $min { @if $min {
@media (min-width: $min) { @media (width >= $min) {
@content; @content;
} }
} @else { } @else {
@ -78,7 +79,7 @@
@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) { @mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
$max: breakpoint-max($name, $breakpoints); $max: breakpoint-max($name, $breakpoints);
@if $max { @if $max {
@media (max-width: $max) { @media (width < $max) {
@content; @content;
} }
} @else { } @else {
@ -93,7 +94,7 @@
$max: breakpoint-max($upper, $breakpoints); $max: breakpoint-max($upper, $breakpoints);
@if $min != null and $max != null { @if $min != null and $max != null {
@media (min-width: $min) and (max-width: $max) { @media (width >= $min) and (width < $max) {
@content; @content;
} }
} @else if $max == null { } @else if $max == null {
@ -116,7 +117,7 @@
$max: breakpoint-max($next, $breakpoints); $max: breakpoint-max($next, $breakpoints);
@if $min != null and $max != null { @if $min != null and $max != null {
@media (min-width: $min) and (max-width: $max) { @media (width >= $min) and (width < $max) {
@content; @content;
} }
} @else if $max == null { } @else if $max == null {

View File

@ -0,0 +1,95 @@
@import "true";
@import "../../mixins/breakpoints";
// Test breakpoint functions and mixins for range media query syntax
@include test-module("Breakpoint Functions") {
$test-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 1024px,
xl: 1280px,
xxl: 1536px
);
@include test("breakpoint-max with range syntax") {
@include assert-equal(breakpoint-max(xs, $test-breakpoints), null);
@include assert-equal(breakpoint-max(sm, $test-breakpoints), 576px);
@include assert-equal(breakpoint-max(md, $test-breakpoints), 768px);
@include assert-equal(breakpoint-max(lg, $test-breakpoints), 1024px);
@include assert-equal(breakpoint-max(xl, $test-breakpoints), 1280px);
@include assert-equal(breakpoint-max(xxl, $test-breakpoints), 1536px);
}
}
@include test-module("Media Query Mixins - Range Syntax") {
$test-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 1024px,
xl: 1280px,
xxl: 1536px
);
@include test("media-breakpoint-up generates range syntax") {
@include assert() {
@include output() {
@include media-breakpoint-up(sm, $test-breakpoints) {
.test { color: #f00; }
}
}
@include expect() {
@media (width >= 576px) {
.test { color: #f00; }
}
}
}
}
@include test("media-breakpoint-down generates range syntax") {
@include assert() {
@include output() {
@include media-breakpoint-down(md, $test-breakpoints) {
.test { color: #00f; }
}
}
@include expect() {
@media (width < 768px) {
.test { color: #00f; }
}
}
}
}
@include test("media-breakpoint-between generates range syntax") {
@include assert() {
@include output() {
@include media-breakpoint-between(sm, lg, $test-breakpoints) {
.test { color: #0f0; }
}
}
@include expect() {
@media (width >= 576px) and (width < 1024px) {
.test { color: #0f0; }
}
}
}
}
@include test("media-breakpoint-only generates range syntax") {
@include assert() {
@include output() {
@include media-breakpoint-only(md, $test-breakpoints) {
.test { color: #ff0; }
}
}
@include expect() {
@media (width >= 768px) and (width < 1024px) {
.test { color: #ff0; }
}
}
}
}
}

View File

@ -53,13 +53,13 @@ $utilities: ();
font-size: 1.25rem !important; font-size: 1.25rem !important;
} }
@media (min-width: 333px) { @media (width >= 333px) {
.padding-sm-1rem { .padding-sm-1rem {
padding: 1rem !important; padding: 1rem !important;
} }
} }
@media (min-width: 666px) { @media (width >= 666px) {
.padding-md-1rem { .padding-md-1rem {
padding: 1rem !important; padding: 1rem !important;
} }

View File

@ -1 +0,0 @@
**Why subtract .02px?** Browsers dont currently support [range context queries](https://www.w3.org/TR/mediaqueries-4/#range-context), so we work around the limitations of [`min-` and `max-` prefixes](https://www.w3.org/TR/mediaqueries-4/#mq-min-max) and viewports with fractional widths (which can occur under certain conditions on high-dpi devices, for instance) by using values with higher precision.

View File

@ -74,19 +74,19 @@ These Sass mixins translate in our compiled CSS using the values declared in our
// No media query for `xs` since this is the default in Bootstrap // No media query for `xs` since this is the default in Bootstrap
// Small devices (landscape phones, 576px and up) // Small devices (landscape phones, 576px and up)
@media (min-width: 576px) { ... } @media (width >= 576px) { ... }
// Medium devices (tablets, 768px and up) // Medium devices (tablets, 768px and up)
@media (min-width: 768px) { ... } @media (width >= 768px) { ... }
// Large devices (desktops, 1024px and up) // Large devices (desktops, 1024px and up)
@media (min-width: 1024px) { ... } @media (width >= 1024px) { ... }
// X-Large devices (large desktops, 1280px and up) // X-Large devices (large desktops, 1280px and up)
@media (min-width: 1280px) { ... } @media (width >= 1280px) { ... }
// XX-Large devices (larger desktops, 1536px and up) // XX-Large devices (larger desktops, 1536px and up)
@media (min-width: 1536px) { ... } @media (width >= 1536px) { ... }
``` ```
### Max-width ### Max-width
@ -109,30 +109,28 @@ We occasionally use media queries that go in the other direction (the given scre
} }
``` ```
These mixins take those declared breakpoints, subtract `.02px` from them, and use them as our `max-width` values. For example: These mixins use the breakpoint values to create `max-width` media queries using modern range syntax. For example:
```scss ```scss
// `xs` returns only a ruleset and no media query // `xs` returns only a ruleset and no media query
// ... { ... } // ... { ... }
// `sm` applies to x-small devices (portrait phones, less than 576px) // `sm` applies to x-small devices (portrait phones, less than 576px)
@media (max-width: 575.98px) { ... } @media (width < 576px) { ... }
// `md` applies to small devices (landscape phones, less than 768px) // `md` applies to small devices (landscape phones, less than 768px)
@media (max-width: 767.98px) { ... } @media (width < 768px) { ... }
// `lg` applies to medium devices (tablets, less than 992px) // `lg` applies to medium devices (tablets, less than 1024px)
@media (max-width: 991.98px) { ... } @media (width < 1024px) { ... }
// `xl` applies to large devices (desktops, less than 1200px) // `xl` applies to large devices (desktops, less than 1280px)
@media (max-width: 1199.98px) { ... } @media (width < 1280px) { ... }
// `2xl` applies to x-large devices (large desktops, less than 1600px) // `2xl` applies to x-large devices (large desktops, less than 1536px)
@media (max-width: 1599.98px) { ... } @media (width < 1536px) { ... }
``` ```
<Callout name="info-mediaqueries-breakpoints" type="warning" />
### Single breakpoint ### Single breakpoint
There are also media queries and mixins for targeting a single segment of screen sizes using the minimum and maximum breakpoint widths. There are also media queries and mixins for targeting a single segment of screen sizes using the minimum and maximum breakpoint widths.
@ -149,7 +147,7 @@ There are also media queries and mixins for targeting a single segment of screen
For example the `@include media-breakpoint-only(md) { ... }` will result in : For example the `@include media-breakpoint-only(md) { ... }` will result in :
```scss ```scss
@media (min-width: 768px) and (max-width: 991.98px) { ... } @media (width >= 768px) and (width < 992px) { ... }
``` ```
### Between breakpoints ### Between breakpoints
@ -165,5 +163,5 @@ Which results in:
```scss ```scss
// Example // Example
// Apply styles starting from medium devices and up to extra large devices // Apply styles starting from medium devices and up to extra large devices
@media (min-width: 768px) and (max-width: 1199.98px) { ... } @media (width >= 768px) and (width < 1200px) { ... }
``` ```