Skip to content

Responsive design

WovenUI is mobile first. A breakpoint means “this width and up”: every responsive class, every apply() breakpoint modifier and media-up() compile to a min-width media query. One map, $breakpoints, drives all of them.

A class without a suffix applies at every width. A class with a suffix applies from that breakpoint upward.

<div class="p-4 p-6@md">...</div>
.p-4 {
padding: var(--wu-space-4);
}
@media (min-width: 768px) {
.p-4\@md {
padding: var(--wu-space-4);
}
}

In HTML the class is written p-4@md; the CSS escapes the @ as \@. Start with the small screen layout, then add the classes that change it at larger widths.

A reader who thinks desktop first writes the large layout and then a suffix for “small screens”. The suffix means the opposite:

Written Below the breakpoint From the breakpoint up
flex row col@sm (wrong) Row on phones Column from 640px, so the desktop gets the stacked layout
flex col row@md (right) Column on phones Row from 768px
Column on small screens, row from md
<div class="flex col row@md gap-3">
<div class="p-3 bg-brand-subtle rounded-md">One</div>
<div class="p-3 bg-brand-subtle rounded-md">Two</div>
<div class="p-3 bg-brand-subtle rounded-md">Three</div>
</div>

The preview is a frame with its own window, so its breakpoints read the width of the frame, not of the browser. Pick Tablet or wider to see the three items in a row; pick Phone or drag the handle below 768px to see them stacked.

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)

The defaults come from $breakpoints in config/_breakpoints.scss:

Name Width Class suffix apply() modifier
xs 480px @xs xs
sm 640px @sm sm
md 768px @md md
lg 1024px @lg lg
xl 1280px @xl xl
2xl 1536px @2xl 2xl

Below 480px, only the classes without a suffix apply.

  • Suffix last. The breakpoint is written after the class: p-4@md, cols-3@lg.
  • State prefix first. A class with a state takes the prefix at the start and the breakpoint at the end: hover:opacity-75@md, group-hover:translate-y-1@lg. See States.
  • One breakpoint per class. There is no p-4@md@lg; write p-4@md p-6@lg.
  • Later breakpoints win. The build emits each breakpoint block after the smaller ones, so cols-2@md cols-3@lg gives 3 columns from 1024px.

Every family below has a @{breakpoint} form at each breakpoint, counted in the compiled build:

Family Examples
Display and visibility hide@md, block@md, flex@lg, invisible@sm
Overflow overflow-hidden@md
Flex and grid col@md, between@lg, cols-2@md, col-span-2@lg, gap-4@md
Spacing p-4@md, mx-auto@lg, space-y-2@md
Sizing (keys, percentages, keywords) w-full@md, w-50p@lg, h-full@md (see the gaps below)
Position and z-index relative@md, top-0@lg, z-10@md
Typography text-center@md, text-lg@md, font-bold@lg, uppercase@md, truncate@md
Border width, radius, shadow border@md, border-2@md, rounded-lg@md, shadow-md@lg
Opacity, transforms, transitions opacity-75@md, translate-x-4@md, scale-150@lg, duration-300@md
Backdrop filters, animations backdrop-blur-md@md, animate-spin@md

Families with no responsive classes:

  • Colors: every bg-*, text-* color and border-* color class, with or without a hover: prefix. hover:bg-brand@md does not exist.
  • Border styles and sides: border-dashed, border-t-2, border-s, border-x and the other side classes. Only border and border-{width} are responsive.
  • Side and corner radii: rounded-t-*, rounded-s-*, rounded-tl-* and the rest. Only rounded and rounded-{key} are responsive.
  • Inset shadows and elevation: shadow-inset-*, elevation-*, and the active:shadow-* classes.
  • Sizing by breakpoint name and full-screen keywords: w-lg, min-w-md, max-w-xl, w-screen, h-screen, min-w-full, max-w-full, min-h-full, max-h-full.
  • Others: cursor-*, whitespace-*, break-*, flow-*, auto-fit-*, auto-fill-*, the container classes, sr-only, hover-ready.

For these, use apply() with a breakpoint in your own SCSS: bg(surface) md works even though .bg-surface@md does not exist. The class names are listed on Utility classes.

hide sets display: none everywhere; block@md turns the element back on from 768px.

Hidden below md, shown from md
<div class="space-y-2">
<div class="p-3 bg-surface border rounded-md">Always visible</div>
<div class="hide block@md p-3 bg-brand-subtle rounded-md">Visible from md (768px and up)</div>
<div class="block hide@md p-3 bg-alternate rounded-md">Visible below md only</div>
</div>
1 column, 2 from md, 3 from lg
<div class="grid cols-1 cols-2@md cols-3@lg gap-3">
<div class="p-3 bg-brand-subtle rounded-md">1</div>
<div class="p-3 bg-brand-subtle rounded-md">2</div>
<div class="p-3 bg-brand-subtle rounded-md">3</div>
<div class="p-3 bg-brand-subtle rounded-md">4</div>
<div class="p-3 bg-brand-subtle rounded-md">5</div>
<div class="p-3 bg-brand-subtle rounded-md">6</div>
</div>
Centered on small screens, start aligned from md
<div class="text-center text-start@md p-4 bg-surface border rounded-md">
<strong class="font-semibold">Welcome back</strong>
<p class="text-sm text-muted">Centered below 768px, aligned to the start from 768px.</p>
</div>

In apply(), the breakpoint is a modifier written after the utility, without @:

@use "@wovenui/core" as *;
.card {
@include apply(p(4), p(6) md);
}
.card {
padding: var(--wu-space-4);
}
@media (min-width: 768px) {
.card {
padding: var(--wu-space-6);
}
}
  • After the utility. p(6) md, never md:p(6) or p(6)@md.
  • Either order with a state. shadow(md) hover lg and shadow(md) lg hover give the same rule.
  • One breakpoint per item. p(4) md lg raises an error (see Errors). Write two items: p(4) md, p(6) lg.
  • Smallest breakpoint first. Items without a modifier come first, then each breakpoint from the smallest up, whatever the written order.
.menu {
@include apply(shadow(md) hover lg);
}
@media (min-width: 1024px) {
.menu:hover {
box-shadow: var(--wu-shadow-md);
}
}

The written order does not change the cascade. flex md is written before hide here:

.panel {
@include apply(flex md, hide);
}
.panel {
display: none;
}
@media (min-width: 768px) {
.panel {
display: flex;
}
}

The panel is hidden below md and a flex container from md up. Each item produces its own @media block; items at the same breakpoint are not merged.

For your own CSS, the media query mixins take a breakpoint name or a length. Ranges end 0.02px below the upper breakpoint, so two ranges never overlap.

Mixin Media query Applies
media-up(md) (min-width: 768px) 768px and up
media-down(md) (max-width: 767.98px) Below 768px
media-between(sm, lg) (min-width: 640px) and (max-width: 1023.98px) 640px up to 1024px
media-only(md) (min-width: 768px) and (max-width: 1023.98px) From md to the next breakpoint
@use "@wovenui/core" as *;
.a {
@include media-up(md) { display: flex; }
}
.b {
@include media-down(md) { display: none; }
}
.c {
@include media-between(sm, lg) { padding: 1rem; }
}
.d {
@include media-only(md) { padding: 2rem; }
}
.e {
@include media-up(900px) { gap: 2rem; }
}
.f {
@include media-only(2xl) { padding: 3rem; }
}
@media (min-width: 768px) {
.a {
display: flex;
}
}
@media (max-width: 767.98px) {
.b {
display: none;
}
}
@media (min-width: 640px) and (max-width: 1023.98px) {
.c {
padding: 1rem;
}
}
@media (min-width: 768px) and (max-width: 1023.98px) {
.d {
padding: 2rem;
}
}
@media (min-width: 900px) {
.e {
gap: 2rem;
}
}
@media (min-width: 1536px) {
.f {
padding: 3rem;
}
}
  • Lengths work too. media-up(900px) uses the length as written.
  • The largest breakpoint is open-ended. media-only(2xl) has no upper limit.
  • media-only() takes the breakpoint unit. A length in another unit raises an error.

The breakpoint names also work as widths:

Input Output
width(lg), .w-lg width: 1024px
max-width(md), .max-w-md max-width: 768px
container, .container max-width of each breakpoint minus 100px: 380px, 540px, 668px, 924px, 1180px, 1436px
get-breakpoint-value(md) 768px
@use "@wovenui/core" as *;
.narrow {
@include apply(width(lg), max-width(md));
}
.x {
--md: #{get-breakpoint-value(md)};
}
.narrow {
width: 1024px;
max-width: 768px;
}
.x {
--md: 768px;
}

See Container for the full container() output.

Media queries follow the window. When a component must adapt to the space its parent gives it (a card in a sidebar and the same card in the main column), use container queries instead: container-type on the parent and container-up(), container-down() and the other container mixins on the children. They take the same breakpoint names or a length. See Container queries and Container.

Set $breakpoints in with(). The map replaces the defaults, so list every breakpoint you keep. A new key works as a class suffix, an apply() modifier and a media query mixin argument.

@use "@wovenui/core" as * with (
$generate-utility-classes: true,
$breakpoints: (
"xs": 480px,
"sm": 640px,
"md": 768px,
"tablet": 900px,
"lg": 1024px,
"xl": 1280px,
"2xl": 1536px,
)
);
.sidebar {
@include apply(hide, block tablet);
}
@media (min-width: 900px) {
.p-4\@tablet {
padding: var(--wu-space-4);
}
}
.sidebar {
display: none;
}
@media (min-width: 900px) {
.sidebar {
display: block;
}
}

In HTML, p-4@tablet applies from 900px. The @media block above is shortened to one class; the build emits every responsive class for the new key.

See Configuration for the other maps.

The responsive truncate@{bp} and overflow-label@{bp} classes come after the responsive display, overflow and sizing classes of every breakpoint in the build. truncate@sm overflow-visible@lg stays truncated at lg, because .truncate\@sm is emitted later than .overflow-visible\@lg. Undo it in your own styles.

The messages below are printed by the Sass compiler for the input shown.

Problem Input Error
A number without a unit media-up(800) Invalid breakpoint: 800 is not a breakpoint name (xs, sm, md, lg, xl, 2xl). Write a unit for a custom width (800px).
Two breakpoints on one item apply(p(4) md lg) apply(p(4) md lg): more than one breakpoint (md, lg). Use one breakpoint per item.

The names in the messages come from your $breakpoints map.