Skip to content

apply()

apply() is the SCSS side of NuvoUI. Every utility class has a mixin of the same name, and @include apply(...) takes a list of those mixins, each optionally followed by a state and a breakpoint. It lets you write semantic class names in your own stylesheets with the same scale, tokens and validation as the utility classes.

@use "@nuvoui/core" as *;
.card {
@include apply(p(4), bg(surface), rounded(md), shadow(md) hover, p(6) md);
}
.card {
padding: var(--nu-space-4);
background-color: var(--nu-bg-surface);
border-radius: var(--nu-radius-md);
}
.card:hover {
box-shadow: var(--nu-shadow-md);
}
@media (min-width: 768px) {
.card {
padding: var(--nu-space-6);
}
}
  • One item per utility. Each comma-separated item names a mixin and its arguments: p(4) is the p() mixin with key 4. A mixin without arguments is written bare: flex, transition.
  • Same names as the classes. .p-4 is p(4), .bg-surface is bg(surface), .flex is flex. The utility pages list the classes; the mixins follow the same naming, and the reference lists them all.
  • Same output as the classes. A key resolves to the same token: p(4) outputs var(--nu-space-4), exactly like .p-4. See Spacing for the scale.
  • Items run in order. Each item is included where it is written. An item with a state is nested as &:state; an item with a breakpoint is wrapped in a min-width media query.
@include apply(item, item, ...);
item: mixin(arguments) [state] [breakpoint]
Utility classes and the same styles through apply()
Card

Padding grows from the md breakpoint.

<div class="p-4 p-6@md bg-surface border rounded-md shadow-sm">
<strong>Card</strong>
<p class="mt-2">Padding grows from the md breakpoint.</p>
</div>

Modifiers come after the utility, separated by spaces. Write the state first and the breakpoint second.

.menu-item {
@include apply(bg(gray, 100) hover lg);
}
@media (min-width: 1024px) {
.menu-item:hover {
background-color: var(--nu-gray-100);
}
}

Never put a modifier before the utility, and never use the class syntax inside apply():

Write Not
bg(gray, 100) hover hover:bg(gray, 100)
p(6) md md:p(6), p(6)@md
bg(gray, 100) hover lg lg hover bg(gray, 100)

A modifier that is a key of $breakpoints is a breakpoint. It wraps the item in @media (min-width: ...) through media-up(), so it applies from that width upwards (mobile first). The default keys are xs, sm, md, lg, xl and 2xl; 2xl works with or without quotes. Breakpoints you add to $breakpoints (see Configuration) work as modifiers too.

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 table shows the class suffixes; in apply() the same names are written without @.

A state is one of these words, written into the selector as a pseudo-class (&:{state}): hover, focus, focus-visible, focus-within, active, visited, disabled, enabled, checked, required, invalid, valid, placeholder-shown, read-only, first-child, last-child, only-child, first-of-type, last-of-type, empty, target. group-hover is the one exception: it emits .group:hover &, matching the group-hover: classes.

.button {
@include apply(
px(4),
bg(brand),
bg(brand-hover) hover,
bg(brand-active) active,
shadow(md) focus-visible,
opacity(50) disabled
);
}

Every modifier is checked: a word that is neither a configured breakpoint nor a supported state raises an error (see Errors). For another pseudo-class, nest it in your own SCSS.

An item takes at most one state and one breakpoint. Two states (p(4) hover focus) or two breakpoints (p(4) md lg) on one item raise an error. For several states, write the utility once per state.

.field {
@include apply(border-color(control-hover) hover, border-color(control-hover) focus);
}

Each item produces its own rule or media block in the order written; items at the same breakpoint are not merged into one @media block.

Most mixins take a scale key or a value written in CSS. A unitless number or a word is a key; a value with a unit is used as written.

Item Output Why
p(4) padding: var(--nu-space-4) 4 is a key of $spacings
p(1.5rem) padding: 1.5rem A unit means a custom value
font-size(lg) font-size: var(--nu-font-size-lg) lg is a key of the type scale
font-size(12px) font-size: 12px Custom value
z-index(modal) z-index: var(--nu-z-modal) A z-index level
z-index(5) z-index: 5 An integer
duration(250ms) transition-duration: 250ms A time in ms or s
bg(#ff5733) background-color: #ff5733 A colour value
.hero {
@include apply(p(1.5rem), font-size(12px), duration(250ms), z-index(5), rounded(12px));
}

calc(), var() and clamp() pass through where the mixin accepts a length or a colour:

.panel {
@include apply(
p(var(--panel-padding)),
width(calc(100% - 2rem)),
font-size(clamp(1rem, 2vw, 2rem)),
bg(var(--panel-bg))
);
}

A unitless number that is not a key raises an error instead of guessing a unit: write p(7px), not p(7). Which keys and values each mixin accepts is documented on its utility page: Spacing, Sizing, Typography, Colours, Transitions, Z-index.

apply() works with both ways of loading core.

@use "@nuvoui/core" as *;
.card {
@include apply(p(4), bg(surface), font-size(12px) md);
}

With as *, every mixin name is also available as a Sass function. p(4) is evaluated by Sass before apply() sees it and returns the mixin name with its arguments as real Sass values, so 12px arrives as a number and #ff5733 as a colour. apply() reads the result directly, without parsing any text.

A few names collide with Sass or CSS functions (transition, rotate, scale, translate, inset and similar) and are not defined as functions. They still work in apply(): they arrive as text and are parsed the same way as in the namespaced style. opacity() is defined: a colour argument keeps the Sass result, anything else reaches apply() as the item, so apply(opacity(inherit)) works.

@use "@nuvoui/core" as nu;
.card {
@include nu.apply(p(4), bg(surface), font-size(12px) md);
}
.toast {
@include nu.apply(px(4), duration(250ms), z-index(5), bg(gray, 100) hover lg);
}

With a namespace, p(4) inside nu.apply() is not a known Sass function, so Sass passes it on as plain text: "p(4)". apply() parses the text: it splits off the mixin name, the arguments and the modifiers, turns numeric arguments back into real numbers (12px, 250ms, 1.5rem, -4), and keeps words, colours, calc() and var() as written. Raw values therefore work the same in both styles, and the output is identical.

One item needs the namespace inside it: Sass reads a bare opacity(inherit) or opacity("50") as its built-in colour function before nu.apply() sees it, so write nu.apply(nu.opacity(inherit)). opacity(50) works as it is.

You can also call the namespaced functions, nu.p(4) or nu.font-size(12px) md. They return the same result as the global functions, so apply() skips the text parsing for those items.

@use "@nuvoui/core" as nu;
.badge {
@include nu.apply(nu.p(2), nu.font-size(12px) md);
}
as * as nu
Call @include apply(...) @include nu.apply(...)
Items Sass function calls, read directly Plain text, parsed once per item
Raw values (12px, 250ms, calc()) Real Sass values Converted from text to real values
Output Same for items with at most one state and one breakpoint Same for items with at most one state and one breakpoint

apply() and the mixins it calls fail at compile time instead of emitting broken CSS. The messages below are printed by the Sass compiler for the input shown.

Problem Input Error
Unknown utility name paddin(4) Mixin not found: paddin
Removed v1 name text(white) Mixin not found: text
Modifier before the utility (class syntax) hover:bg(gray, 100) expected ")".
Modifiers before the utility, as * lg hover bg(gray, 100) (_n: "bg", _p: (gray, 100)) isn't a valid CSS value.
Modifiers before the utility, as nu lg hover bg(gray, 100) Mixin not found: lg hover bg
Unknown scale key p(7) p: 7 is not a configured key. Valid keys: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64. Write a unit for a custom value (7px, 7rem).
Unknown colour bg(brandd) bg(brandd): unknown colour. Use a role (brand, success, danger, warning, info), a palette name (gray, red, blue, green, yellow, purple, pink), a theme word (base, surface, alternate, raised, hover, selected, disabled), a hex colour, a CSS colour name, a colour function (...), a keyword (...) or var().
Unknown level z-index(top) Unknown z-index: top. Use a key (auto, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 500, 1000), a level (base, hover, dropdown, sticky, fixed, overlay, drawer, modal, popover, tooltip, toast, max), an integer, auto, calc() or var().
Unknown modifier p(4) hovr apply(p(4) hovr): unknown modifier hovr. Use a breakpoint (xs, sm, md, lg, xl, 2xl) or a state (hover, focus, focus-visible, focus-within, active, visited, disabled, enabled, checked, required, invalid, valid, placeholder-shown, read-only, first-child, last-child, only-child, first-of-type, last-of-type, empty, target, group-hover).
Two states p(4) hover focus apply(p(4) hover focus): more than one state (hover, focus). Use one state per item.
Two breakpoints p(4) md lg apply(p(4) md lg): more than one breakpoint (md, lg). Use one breakpoint per item.

The key lists in the messages come from your configuration, so they change when you pass your own maps.

// error
.card {
@include apply(hover:bg(gray, 100));
}
// error
.card {
@include apply(p(7));
}

Large applications call apply() thousands of times and recompile on every save, so the cost of one call matters. Core keeps that cost low: a 2.0 call is not meaningfully slower than a 1.x call, and the performance test in the core repository measures it against a recorded baseline and fails when it regresses.

Why a call is cheap:

  • No parsing in the global style. With as *, each item is already the mixin name and its arguments; apply() reads them and includes the mixin directly through meta.get-mixin() and meta.apply().
  • Parsed text is cached. In the namespaced style, and for the collision names such as transition, the parsed form of an item is stored in an internal cache ($ac) keyed on the item text. The next p(4) md anywhere in the compile is a lookup, not a parse.
  • Flat, compact internals. The parsing functions are small, flat and minified in the generated mixins-map.scss.

Bounded inputs (scale keys, theme words, role and hue names) are cached without limit. Items with raw values (a number with a unit, calc(), var(), a hex colour) go in size-capped caches: 128 entries per generation, two generations. A repeated raw value such as p(13px) is resolved once; a stylesheet of unique values still compiles in linear time.

The reason for the cap is that a Sass map is copied on every insert. An unbounded cache keyed on raw values grows with every unique value, so the cost of each new entry grows with the size of the cache, and a stylesheet with many one-off values becomes quadratic to compile. A capped cache keeps each insert bounded. The performance test guards this too: it compiles a fixture of namespaced apply() calls that each hold values no other rule repeats, and fails when the time per call grows with the number of rules.

In practice:

  • Prefer scale keys (p(4), font-size(lg)) for repeated values. They are cached, and they follow the tokens and themes.
  • Raw values are fine for one-off values. They are not slower per call as the stylesheet grows, and repeated ones hit the capped cache.
  • as * and nu.p(4) items skip text parsing entirely.
  • Spacing, Typography, Colours and the other utility pages document each mixin with its keys, custom values and errors.
  • Configuration explains how to load core with your own scales and breakpoints.
  • Tokens explains the --nu-* custom properties that apply() outputs.
  • The reference lists every mixin.