]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
v6: Range media queries (#41786)
authorMark Otto <markd.otto@gmail.com>
Thu, 2 Oct 2025 22:24:19 +0000 (15:24 -0700)
committerMark Otto <markdotto@gmail.com>
Fri, 10 Oct 2025 17:04:01 +0000 (10:04 -0700)
* 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>
scss/layout/_breakpoints.scss
scss/tests/mixins/_breakpoints.test.scss [new file with mode: 0644]
scss/tests/utilities/_api.test.scss
site/src/content/callouts/info-mediaqueries-breakpoints.md [deleted file]
site/src/content/docs/layout/breakpoints.mdx

index 2320792d13fbf0ce98ddfafbaf128cb96b7af405..c8d33116c2ba1bf65ac5dffcf8f2f1b525c536c1 100644 (file)
@@ -21,7 +21,7 @@
 @function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map.keys($breakpoints)) {
   $n: list.index($breakpoint-names, $name);
   @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($min != 0, $min, null);
 }
 
-// Maximum breakpoint width.
-// The maximum value is reduced by 0.02px to work around the limitations of
-// `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
+// Maximum breakpoint width for range media queries.
+// Returns the breakpoint value to use as an upper bound in range queries.
 //
-//    >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px))
-//    767.98px
+//    >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px))
+//    576px
+//    >> breakpoint-max(xxl, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px))
+//    null
 @function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
+  @if $name == null {
+    @return null;
+  }
   $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.
@@ -65,7 +66,7 @@
 @mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
   $min: breakpoint-min($name, $breakpoints);
   @if $min {
-    @media (min-width: $min) {
+    @media (width >= $min) {
       @content;
     }
   } @else {
@@ -78,7 +79,7 @@
 @mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
   $max: breakpoint-max($name, $breakpoints);
   @if $max {
-    @media (max-width: $max) {
+    @media (width < $max) {
       @content;
     }
   } @else {
@@ -93,7 +94,7 @@
   $max: breakpoint-max($upper, $breakpoints);
 
   @if $min != null and $max != null {
-    @media (min-width: $min) and (max-width: $max) {
+    @media (width >= $min) and (width < $max) {
       @content;
     }
   } @else if $max == null {
   $max:  breakpoint-max($next, $breakpoints);
 
   @if $min != null and $max != null {
-    @media (min-width: $min) and (max-width: $max) {
+    @media (width >= $min) and (width < $max) {
       @content;
     }
   } @else if $max == null {
diff --git a/scss/tests/mixins/_breakpoints.test.scss b/scss/tests/mixins/_breakpoints.test.scss
new file mode 100644 (file)
index 0000000..7136bf1
--- /dev/null
@@ -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; }
+        }
+      }
+    }
+  }
+}
index 2b38bb9964cf4b7ba3cce1eb59a5f39593cd6827..4020b2217b5952a1ce13f9c70fd8bedaa307bf12 100644 (file)
@@ -53,13 +53,13 @@ $utilities: ();
           font-size: 1.25rem !important;
         }
 
-        @media (min-width: 333px) {
+        @media (width >= 333px) {
           .padding-sm-1rem {
             padding: 1rem !important;
           }
         }
 
-        @media (min-width: 666px) {
+        @media (width >= 666px) {
           .padding-md-1rem {
             padding: 1rem !important;
           }
diff --git a/site/src/content/callouts/info-mediaqueries-breakpoints.md b/site/src/content/callouts/info-mediaqueries-breakpoints.md
deleted file mode 100644 (file)
index 52be673..0000000
+++ /dev/null
@@ -1 +0,0 @@
-**Why subtract .02px?** Browsers don’t 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.
index c9c43fa40c230f58bb82d70930b00f60452eb9b3..50c74bf2d8c2e7d84031aa96479937a5361c1a9d 100644 (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
 
 // Small devices (landscape phones, 576px and up)
-@media (min-width: 576px) { ... }
+@media (width >= 576px) { ... }
 
 // Medium devices (tablets, 768px and up)
-@media (min-width: 768px) { ... }
+@media (width >= 768px) { ... }
 
 // Large devices (desktops, 1024px and up)
-@media (min-width: 1024px) { ... }
+@media (width >= 1024px) { ... }
 
 // X-Large devices (large desktops, 1280px and up)
-@media (min-width: 1280px) { ... }
+@media (width >= 1280px) { ... }
 
 // XX-Large devices (larger desktops, 1536px and up)
-@media (min-width: 1536px) { ... }
+@media (width >= 1536px) { ... }
 ```
 
 ### 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
 // `xs` returns only a ruleset and no media query
 // ... { ... }
 
 // `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)
-@media (max-width: 767.98px) { ... }
+@media (width < 768px) { ... }
 
-// `lg` applies to medium devices (tablets, less than 992px)
-@media (max-width: 991.98px) { ... }
+// `lg` applies to medium devices (tablets, less than 1024px)
+@media (width < 1024px) { ... }
 
-// `xl` applies to large devices (desktops, less than 1200px)
-@media (max-width: 1199.98px) { ... }
+// `xl` applies to large devices (desktops, less than 1280px)
+@media (width < 1280px) { ... }
 
-// `2xl` applies to x-large devices (large desktops, less than 1600px)
-@media (max-width: 1599.98px) { ... }
+// `2xl` applies to x-large devices (large desktops, less than 1536px)
+@media (width < 1536px) { ... }
 ```
 
-<Callout name="info-mediaqueries-breakpoints" type="warning" />
-
 ### Single breakpoint
 
 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 :
 
 ```scss
-@media (min-width: 768px) and (max-width: 991.98px) { ... }
+@media (width >= 768px) and (width < 992px) { ... }
 ```
 
 ### Between breakpoints
@@ -165,5 +163,5 @@ Which results in:
 ```scss
 // Example
 // 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) { ... }
 ```