Media queries
Media query mixins wrap styles in a viewport width query. They read the same $breakpoints map as the responsive class suffixes (p-4@md) and the breakpoint modifiers in apply() (p(4) md), so a query written by hand and a responsive class change at the same width.
How media queries work
Section titled “How media queries work”- Mobile first.
media-up(md)applies from themdwidth upwards. This is the same rule the responsive suffixes use, so prefer it and add larger screens on top. - Names or lengths. Every mixin accepts a breakpoint name (
md) or a length (800px,50em). A unitless number that is not a name raises an error. - Upper bounds stop just below a breakpoint.
media-down,media-betweenandmedia-onlyend 0.02px below the breakpoint, so a range that ends atmdand a range that starts atmdnever match at the same width. - No media type. Queries are written as
@media (min-width: ...), withoutscreen and, so they also apply when printing, like the rest of the framework.
| Mixin | Applies | Output for md |
|---|---|---|
media-up($breakpoint) |
At and above the breakpoint | @media (min-width: 768px) |
media-down($breakpoint) |
Below the breakpoint | @media (max-width: 767.98px) |
media-between($lower, $upper) |
From $lower up to just below $upper |
media-between(sm, lg): @media (min-width: 640px) and (max-width: 1023.98px) |
media-only($breakpoint) |
From the breakpoint up to just below the next larger one | @media (min-width: 768px) and (max-width: 1023.98px) |
Breakpoints
Section titled “Breakpoints”The table below is generated from the compiled core build. Each suffix is the minimum width at which a responsive class, a breakpoint modifier and media-up() start to apply.
| Suffix | Media query |
|---|---|
@xs | @media (min-width: 480px) |
@sm | @media (min-width: 640px) |
@md | @media (min-width: 768px) |
@lg | @media (min-width: 1024px) |
@xl | @media (min-width: 1280px) |
@2xl | @media (min-width: 1536px) |
2xl can be written with or without quotes: media-up(2xl) and media-up("2xl") give the same query.
media-up
Section titled “media-up”media-up() applies styles at and above a breakpoint. The responsive classes use the same query, so the two lines below are equivalent.
<div class="p-2 p-6@md bg-brand-subtle rounded"> <div class="p-2 bg-surface rounded">p-2 p-6@md: resize the window past 768px</div></div>.panel { padding: token(space-2);
@include media-up(md) { padding: token(space-6); }}Inside apply(), a breakpoint modifier after a mixin gives the same result with less code. Use media-up() when the styles inside are not NuvoUI mixins, or when several declarations share one query.
.sidebar { display: none;
@include media-up(lg) { display: block; inline-size: 16rem; }}.sidebar { display: none;}@media (min-width: 1024px) { .sidebar { display: block; inline-size: 16rem; }}media-down
Section titled “media-down”media-down() applies styles below a breakpoint. Use it for an exception on small screens that would be awkward to undo in a mobile-first rule.
.toolbar { @include media-down(sm) { flex-direction: column; }}@media (max-width: 639.98px) { .toolbar { flex-direction: column; }}media-between
Section titled “media-between”media-between() applies styles from the lower breakpoint (inclusive) up to just below the upper one (exclusive).
.gallery { @include media-between(sm, lg) { grid-template-columns: repeat(2, 1fr); }}@media (min-width: 640px) and (max-width: 1023.98px) { .gallery { grid-template-columns: repeat(2, 1fr); }}media-only
Section titled “media-only”media-only() applies styles from a breakpoint up to just below the next larger breakpoint in $breakpoints. The largest breakpoint has no upper limit.
.hero { @include media-only(md) { padding-inline: 2rem; }}
.banner { @include media-only(2xl) { font-size: 1.5rem; }}@media (min-width: 768px) and (max-width: 1023.98px) { .hero { padding-inline: 2rem; }}@media (min-width: 1536px) { .banner { font-size: 1.5rem; }}A length works too: the next larger breakpoint is looked up from the map, so media-only(800px) ends at 1023.98px. Give a length in the same unit as the breakpoints (px by default), because the comparison with the map values is numeric.
Why upper bounds end 0.02px below
Section titled “Why upper bounds end 0.02px below”A range that ends at a breakpoint must not match at the breakpoint itself, or a max-width: 768px rule and a min-width: 768px rule would both apply at exactly 768px. Subtracting a whole pixel (767px) leaves a gap instead: browsers can report fractional viewport widths, for example on high-density screens or when the page is zoomed, and a width of 767.5px would then match neither rule.
Core subtracts 0.02px, the same offset Bootstrap uses. It is small enough that no real viewport width falls into the gap, and large enough to survive rounding: 0.01px is rounded incorrectly in Safari. For breakpoints in em or rem the offset is the same amount in that unit, 0.02 / 16 (0.00125em or 0.00125rem).
.note { @include media-down(50em) { font-size: 0.875rem; }}@media (max-width: 49.99875em) { .note { font-size: 0.875rem; }}media-up() never subtracts anything: lower bounds are always the breakpoint itself.
Configuration
Section titled “Configuration”Pass your own $breakpoints map when loading core. The map replaces the default map; it is not merged, so include every breakpoint you use. The keys become the responsive suffixes (@md) and the modifier names in apply(), and the container widths follow the same map.
@use "@nuvoui/core" with ( $generate-utility-classes: true, $breakpoints: ("sm": 600px, "md": 900px, "lg": 1200px));With this map, media-only(md) outputs @media (min-width: 900px) and (max-width: 1199.98px), and p-4@xl no longer exists.
Breakpoint values should be px, em or rem. The ranges that end below a breakpoint (media-down, media-between, media-only) need one of those units to compute the offset.
Errors
Section titled “Errors”The mixins fail at compile time instead of emitting a broken query. The messages below come from the current compiler output.
| Input | Error |
|---|---|
media-up(7) |
Invalid breakpoint: 7 is not a breakpoint name (xs, sm, md, lg, xl, 2xl). Write a unit for a custom width (7px). |
media-up(huge) |
Invalid breakpoint: huge. Use a breakpoint name (xs, sm, md, lg, xl, 2xl) or a length such as 800px. |
media-down(50vw) |
Invalid breakpoint: 50vw. Ranges that end below a breakpoint need a px, em or rem width. |
// error.panel { @include media-up(7) { padding: 0; }}Preference queries
Section titled “Preference queries”The media query mixins only query width. User preference queries live in a separate module (tools/_accessibility.scss) and are loaded with the rest of core:
| Mixin | Query |
|---|---|
prefers-dark |
@media (prefers-color-scheme: dark), the operating system setting only |
dark-mode |
[data-theme="dark"] on the element or an ancestor, or a dark operating system when :root has no data-theme. It does not match inside data-theme="inverse" sections, which also use the dark token values on a light page. See Theming. |
reduced-motion |
@media (prefers-reduced-motion: reduce) |
high-contrast |
@media (forced-colors: active) |
.spinner { @include reduced-motion { animation: none; }}For print styles, the print mixin wraps its content in @media print. It lives with the layout helpers; see the reference.
.nav { @include print { display: none; }}Accessibility and RTL
Section titled “Accessibility and RTL”- Zoom changes the viewport width. At 200% zoom a 1280px window reports 640px, so the
smlayout applies. Make sure every breakpoint range works on its own, because users who zoom see the narrow layouts on wide screens (WCAG 1.4.10 Reflow). - Do not hide content by width alone. Content hidden with
media-down()is hidden from every user at that width, including screen reader users. Rearrange content instead of removing it. - Width queries are direction independent.
min-widthandmax-widthmeasure the viewport, not the inline axis, so the same query applies in left-to-right and right-to-left pages.