Skip to content

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.

  • Mobile first. media-up(md) applies from the md width 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-between and media-only end 0.02px below the breakpoint, so a range that ends at md and a range that starts at md never match at the same width.
  • No media type. Queries are written as @media (min-width: ...), without screen 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)

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.

SuffixMedia 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() applies styles at and above a breakpoint. The responsive classes use the same query, so the two lines below are equivalent.

p-2 p-6@md: resize the window past 768px
<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>

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() 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() 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() 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.

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.

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.

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;
}
}

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;
}
}
  • Zoom changes the viewport width. At 200% zoom a 1280px window reports 640px, so the sm layout 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-width and max-width measure the viewport, not the inline axis, so the same query applies in left-to-right and right-to-left pages.