Skip to content

Utility classes

Every WovenUI utility class follows one grammar: a property, a value, an optional state in front and an optional breakpoint at the end. Learn it once and you can read any class on the utility pages, and write the matching mixin in SCSS.

<div class="flex between x-center p-4 p-6@md bg-surface text-regular rounded-lg">...</div>
property-value
property-value@breakpoint
state:property-value
state:property-value@breakpoint
Class Reads as
bg-brand Background in the brand color
p-4@md Padding key 4, from the md breakpoint up
hover:bg-brand-subtle Brand subtle background on hover
hover:opacity-75@sm Opacity key 75 on hover, from the sm breakpoint up
  • The state comes first, with a colon. hover:, group-hover:, active: and focus: are the class prefixes. See States and breakpoints in classes for which families have them.
  • The breakpoint comes last, with @. @md means “this width and up” (mobile first). The default names are xs, sm, md, lg, xl and 2xl.
  • In CSS, : and @ are escaped. You write class="p-4@md"; the stylesheet selects .p-4\@md. Write the class in markup exactly as listed, without the backslashes.

Class names describe the result, not the CSS property. Output from the compiled build:

Class CSS
between (on .flex) justify-content: space-between
x-center (on .flex) align-items: center
col (on .flex) flex-direction: column
z-modal z-index: var(--wu-z-modal)
bg-surface background-color: var(--wu-bg-surface)
text-muted color: var(--wu-text-muted)
ps-4 padding-inline-start: var(--wu-space-4)
rounded-lg border-radius: var(--wu-radius-lg)

In flex classes, no prefix means the main axis and x- means the cross axis. x does not mean horizontal: with col, the axes swap.

A number in a class is a key of a configuration map, never a length. p-4 is spacing key 4, which outputs var(--wu-space-4), which is 1rem by default. p-16 is key 16 (4rem), not 16 pixels.

.p-4 {
padding: var(--wu-space-4);
}
  • Only configured keys have classes. The default spacing keys are 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56 and 64. There is no .p-7; a custom value belongs in SCSS (p(7px)).
  • Decimal keys escape the dot. A key 0.5 added to $spacings gives the class p-0.5, selected as .p-0\.5, and the token --wu-space-0_5.
  • Percent keys end in p. w-50p outputs width: 50%. The keys 33 and 66 are exact thirds (33.333%, 66.667%).

Scales and keys per family: Spacing, Sizing.

Color classes take a word, and the word is the same in the class and the mixin.

Kind Words Classes
Roles brand, success, danger, warning, info bg-brand, text-danger, border-brand, bg-success-subtle
Hues gray, red, blue, green, yellow, purple, pink bg-red, text-red, bg-blue-subtle
Theme words surfaces, text, borders bg-surface, bg-alternate, text-muted, border-regular
Stops any role or hue with 50, 100 to 900, 950 bg-red-300, bg-brand-100
On a fill roles only text-on-brand

Every color class outputs a token, never a hex value:

.bg-surface { background-color: var(--wu-bg-surface); }
.text-danger { color: var(--wu-text-danger); }
.bg-red { background-color: var(--wu-red-500); }
.bg-red-300 { background-color: var(--wu-red-300); }
.text-on-brand { color: var(--wu-text-on-brand); }

Role and theme tokens are redefined in dark mode and under data-theme, so bg-brand, text-danger, text-red and bg-surface follow the theme with no extra class. Palette stops (bg-red-300) and the bare hue background (bg-red, stop 500) stay fixed. text-on-{name} exists for roles only; there is no text-on-red. Full lists: Colors and Tokens.

Layout modifiers only work together with their container class. The compiled selectors show why:

.flex.between { justify-content: space-between; }
.flex.col { flex-direction: column; }
.flex > .grow { flex: 1 1 0%; }
.grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.flex:where(.inline) { display: inline-flex; }
@media (min-width: 768px) {
.flex.row\@md { flex-direction: row; }
}

(Each selector also lists the responsive containers, such as .flex\@md.between.)

  • Flex modifiers sit on a .flex element. Write class="flex between x-center"; between alone does nothing.
  • Item classes sit on direct children. grow, no-grow and fill-6 apply only inside a .flex parent.
  • Grid templates sit on a .grid element. class="grid cols-3". col-span-* and row-span-* are standalone.
  • Inline layouts are two classes. flex inline and grid inline; there is no .inline-flex or .inline-grid class.
  • A parent made flex in SCSS does not enable the child classes. @include apply(flex) on .toolbar gives no .flex class; add the class, or use the item mixins (grow, fill(6)) on the children.
A flex row: between spreads the items, x-center aligns them on the cross axis
Invoices 12 open New
<div class="flex between x-center gap-2 p-3 bg-surface border rounded-md">
<strong>Invoices</strong>
<span class="text-muted">12 open</span>
<span class="bg-brand text-on-brand px-3 py-1 rounded-sm">New</span>
</div>

Details: Flex, Grid.

Logical classes follow the writing direction. Under dir="rtl" the start side is the right side, and these classes mirror with it:

Logical CSS
ps-4, pe-2 padding-inline-start, padding-inline-end
ms-2, me-2, ms-auto margin-inline-start, margin-inline-end
start-0, end-0 inset-inline-start, inset-inline-end
border-s, border-e border-inline-start-*, border-inline-end-*
rounded-s-md border-start-start-radius, border-end-start-radius
text-start, text-end text-align: start, text-align: end

px-*, py-*, mx-*, my-* and space-x-* are logical too. Physical classes keep their side in both directions: pl-4 is padding-left, mr-2 is margin-right, left-0 is left, text-left is text-align: left. Use them only for content that must not mirror.

The same markup in both directions: logical classes mirror, pl-4 stays on the left
LTR
Logical: starts on the reading side
Physical: always padded on the left
RTL
Logical: starts on the reading side
Physical: always padded on the left
<div class="ps-4 pe-2 border-s-4 border-brand rounded-s-md bg-brand-subtle text-start p-2">
Logical: starts on the reading side
</div>
<div class="pl-4 text-left bg-alternate p-2 mt-2">
Physical: always padded on the left
</div>

State prefixes exist only where a state is common. The rest are written in SCSS with apply().

Prefix Families Also with @breakpoint
hover: background, text and border colors, border width and style, width and height, opacity, transforms, shadows width and height, opacity, transforms, shadows
group-hover: background, text and border colors, border width and style, width and height, opacity, transforms width and height, opacity, transforms
active: border colors, border width and style, opacity, shadows opacity
focus: border colors, border width and style, opacity, shadows opacity, shadows
  • group-hover: reacts to a parent. Put group on the parent: .group:hover .group-hover\:bg-blue.
  • Color states have no breakpoint form. hover:bg-brand-subtle exists; hover:bg-brand-subtle@md does not.
  • Spacing, display and layout classes have no state prefix. Use apply(): @include apply(p(6) hover).
  • Most classes take a breakpoint suffix: spacing, sizing, display, flex and grid, typography and more (p-4@md, row@md, hidden@lg). text-on-{role} is one of the exceptions.

Full rules: States and Responsive design.

  • Generated from configuration. Classes are emitted only with $generate-utility-classes: true (the prebuilt dist/wovenui.css sets it). Each class comes from a configuration map: a key added to $spacings gets p-*, m-*, gap-* and the rest; a removed key loses them.
  • $parent-selector scopes them. With $parent-selector: ".app", .p-4 is emitted as .app .p-4.
  • Feature flags switch groups off. $feature-flags: ("animations": false) removes the animation classes; each flag owns its group.
  • Classes are unlayered. Reset, base and component styles live in @layer reset, base, components; utility classes do not. Unlayered CSS beats layered CSS, so a class overrides a base style without !important.
@use "@wovenui/core" as * with (
$generate-utility-classes: true,
$parent-selector: ".app",
$feature-flags: ("animations": false)
);

Every setting: Configuration.

Every class has a mixin of the same name for apply(). The value moves into parentheses, a stop becomes a second argument, and the state and breakpoint move after the item without : or @.

Class Mixin
p-4 p(4)
mt-4@sm mt(4) sm
bg-brand-100 bg(brand, 100)
text-muted text-color(muted)
hover:opacity-75@sm opacity(75) hover sm
group-hover:bg-blue bg(blue) group-hover
@use "@wovenui/core" as *;
.item {
@include apply(p(4), mt(4) sm, bg(brand, 100), text-color(muted), opacity(75) hover sm, bg(blue) group-hover);
}
.item {
padding: var(--wu-space-4);
background-color: var(--wu-brand-100);
color: var(--wu-text-muted);
}
.group:hover .item {
background-color: var(--wu-blue-500);
}
@media (min-width: 640px) {
.item {
margin-top: var(--wu-space-4);
}
}
@media (min-width: 640px) {
.item:hover {
opacity: var(--wu-opacity-75);
}
}

Text colors use text-color(), not text(), and the color word never repeats the family: bg(surface), not bg(bg-surface). More: apply().

A class in markup is plain text to Sass. A misspelled class (p-44, bg-primery, justify-between) matches no rule: the element renders unstyled and nothing reports it. Check the class against the utility page, or move the styles to SCSS.

The SCSS side is checked. @include apply(p(7)), bg(primery) or p(4) hovr stops the compile with a message that names the valid options. See Errors.