Navigation

Directives

Utilities and mixins

Define on-demand utilities, reusable mixins and layered native styles.

On this page

@utility registers classes generated on demand. @mixin names a reusable CSS body; @apply expands it where it is called. Native selectors remain ordinary CSS. Reusing styles teaches which form to choose; this page defines their grammar and replacement rules.

@utility

Declare the class form explicitly at the top level:

@utility card-layout {  display: grid;  gap: var(--spacing-md);  &:hover {    outline: 1px solid currentColor;  }}@utility col-gap-(--spacing) {  column-gap: var(--spacing);}@utility grid-cols(--cols <integer>) {  display: grid;  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));}

Use card-layout, col-gap-sm, and grid-cols(3)@sm in markup. The header determines whether a definition registers a fixed class, a token pattern, or a function. Each form registers only that form. A token pattern does not also create a function call. Adding declarations or supported nested rules does not change the registration.

A utility name or token prefix cannot contain _, because _ begins a descendant selector in classes; such a definition reports an error at its source. Token keys may contain _: they remain valid variables for var(), but they do not create bare token classes or completions. Mixin names, custom properties and native stylesheet classes may still use _.

Registration and the preset

A theme supplies tokens; utility definitions determine which classes those tokens can produce. Import "@master/css" for the complete preset, or load "@master/css/utilities.css" alongside your own tokens when composing a stylesheet explicitly.

The family index owns the complete token-to-property mappings. Fixed utilities and Direct values list their corresponding registrations. All use the same definition forms described here.

Token patterns

A pattern has one trailing placeholder. Its parameter names the token namespace; the fixed part names the class prefix. An untyped placeholder binds the token value:

@utility p-(--spacing) {  padding: var(--spacing);}@utility m-(--spacing) {  margin: var(--spacing);}@utility fg-(--color) {  color: var(--color);}@utility size-(--spacing) {  width: var(--spacing);  height: var(--spacing);}

p-md binds var(--spacing-md); size-md uses that reference in both declarations. Ordinary tokens retain browser-evaluated references, while inline tokens substitute their declared value once. Unknown tokens do not create classes. Value patterns accept numeric and Unicode keys, including keys containing --.

A <string> placeholder binds the token key, allowing explicit companion references:

@theme {  --text-hero: var(--font-size-5xl);  --text-hero--line-height: 1.15;  --text-hero--letter-spacing: -.03em;}@utility text-(--text <string>) {  font-size: var(ident("--text-" var(--text)));  line-height: var(ident("--text-" var(--text) "--line-height"), normal);  letter-spacing: var(ident("--text-" var(--text) "--letter-spacing"), normal);}

text-hero binds the string "hero". A matching primary token is required. Companion keys containing -- do not create classes, and missing companions use the authored fallback. ident() folds statically into an ordinary CSS identifier. The namespace comes from the parameter, so @utility type-(--text <string>) uses the same tokens with a different class prefix. Token placeholders accept no defaults or other types.

A shared prefix can expose several namespaces explicitly:

@utility font-(--font-family) {  font-family: var(--font-family);}@utility font-(--font-size) {  font-size: var(--font-size);}@utility font-(--font-weight) {  font-weight: var(--font-weight);}

font-sans, font-sm and font-bold each select the one namespace containing that key. A key present in multiple branches reports AMBIGUOUS_TOKEN when used and produces no CSS. Use distinct keys or a native declaration such as (font-size:var(--font-size-brand)). This rule also applies to string-key recipes and bodies with multiple declarations. Negation and opacity are checked only after a unique branch is selected.

Fixed classes take priority over patterns: the complete class spelling is reserved, not the token key, so text-center sets text-align even when --text-center exists, while fg-solid, b-solid/.5 and -mx-auto still reach tokens named solid or auto. Patterns use the longest prefix; an unknown token in the selected family reports an error without retrying a shorter prefix. Token patterns sharing a prefix coexist when their parameter namespaces differ. A later pattern replaces the entire definition with the same decoded prefix and namespace, including its parameter mode, body, source and dependencies. Static and function registrations replace the same form and name independently.

Negative values and opacity apply to the direct-value subset: one untyped placeholder and one native declaration whose entire value is var(--parameter). -m-md negates a numeric spacing token; -p-md is invalid because padding does not support negative values. fg-red/.5 applies opacity to a color token, with opacity between zero and one. These capabilities do not extend to general multi-declaration or string-key recipes.

Two matching namespaces do not produce two declarations

Both token keys exist in this custom theme. Master CSS requires a unique namespace selection before generating the class.

Text description
  1. With --font-family-brand: monospace and --font-size-brand: 2rem, font-brand matches two preset font- families.
  2. The result is AMBIGUOUS_TOKEN and no CSS for that class; the branches are not combined.
  3. Use distinct keys or an explicit native declaration such as (font-size:var(--font-size-brand)).

Static arguments

Function utilities and mixins accept positional parameters: untyped, <integer>, <number>, <string>, or <custom-ident>. Static defaults use a colon, such as --cols <integer>: 2. Numbers must be numeric literals; grid counts and spans must be positive integers. String arguments require quotes.

@utility pair(--size) {  width: var(--size);  height: var(--size);}

Use pair(2rem). General function arguments must be statically known. A function or mixin with exactly one untyped, default-free parameter and one native declaration whose complete value is var(--parameter) also accepts symbolic var() expressions. Its caller's reference remains intact for browser evaluation. Other functions reject external var(), attr(), and browser-dependent arguments. A known outer parameter may be forwarded through var(--parameter).

A parameter utility

@utility pair(--size) { width: var(--size); height: var(--size); } @safelist 'pair(2rem)';
Generated CSS
@layer utilities {  .pair\(2rem\) {    width: 2rem;    height: 2rem  }}

A static recipe argument is different from a live CSS value

The shown pair utility has two declarations. The separate one-parameter, one-declaration symbolic-reference exception does not apply to it.

Text description
  1. @utility pair(--size) { width: var(--size); height: var(--size); } accepts the known argument in pair(2rem) and binds it into both declarations.
  2. This recipe rejects pair(var(--size)) because its argument is browser-dependent. Use explicit native declaration classes for values that must remain live.
  3. (width:var(--size)) preserves the variable reference for the browser; it changes width only. Add a separate height declaration when that is also required.

@mixin and @apply

Master compiles a static subset of the experimental CSS Mixins specification. Definitions have dashed names and must be unconditional stylesheet-level rules. A mixin supplies CSS reuse; it does not register a Master class.

@mixin --caption {  font-size: var(--font-size-sm);  line-height: 1.5;}.caption {  color: currentColor;  @apply --caption;  font-weight: 600;}@utility caption {  @apply --caption;}

@apply accepts one mixin call inside a native rule, utility, or another mixin. It does not accept utility names or class lists. Expansion occurs at the authored declaration position, preserving duplicates, importance, selectors, conditions and URL ownership.

Parameters can be used on the applying element, including &:hover and condition wrappers. Cross-element parameter use, reassignment, selector or condition interpolation, recursive definitions, overloads, @private, and unsupported types report compilation errors. The static subset does not implement browser-dependent parameter evaluation, private properties or general-purpose functions.

A mixin expands at the call position

Matching lines stay aligned around the insertion. The selector, declaration order and surrounding layer remain the caller’s.

Text description
  1. The --caption mixin supplies font-size: 14px and line-height: 1.5.
  2. Inside .caption, @apply --caption expands between color: currentColor and font-weight: 600.
  3. Expansion does not register a class or move the native rule to a generated layer. Duplicate declarations and authored order are preserved.

Contents and class wrappers

Use @contents to mark where a caller’s declarations or nested rules enter a reusable wrapper.

@mixin --card-wide() {  @container card (width >= 40rem) {    @contents {      display: grid;    }  }}.card {  @apply --card-wide {    gap: 1rem;  }}

@contents; inserts supplied content. @contents { … } supplies fallback content only when the call omits its block. An explicit empty @apply --card-wide {} suppresses fallback. Each contents position inserts the block once, preserving declarations, duplicates and nested rules. A mixin without @contents ignores the supplied block.

An omitted contents block differs from an empty one

The mixin has @contents { display: grid; }. Braces at the call site distinguish an intentional empty block from an omitted block.

Text description
  1. @apply --panel; omits a contents block and uses the display: grid fallback.
  2. @apply --panel {} supplies an empty block, suppressing the fallback and adding no declaration in this example.
  3. @apply --panel { display: flex; } supplies its own content in place of the fallback. The two display declarations are not merged.

Supplied content retains the caller's parameter bindings, including when forwarded through nested applications. Fallback uses the defining mixin's bindings. Ordinary custom properties retain native CSS cascade behavior. Finite nested calls are allowed; recursive definitions and expansion depth/node limit violations are errors.

@mixin --hocus {  &:is(:hover, :focus-visible) {    @contents;  }}@utility emphasis {  color: red;  display: block;}
<button class="(color:red)@apply(--hocus)">Save</button><div class="grid-cols(3)@apply(--card-wide)"></div><div class="emphasis@apply(--hocus)!"></div>

@apply(--name(...)) accepts static arguments. Multiple @ wrappers nest from left to right: (color:red)@apply(--a)@apply(--b) puts --a outside --b. A utility supplies its declarations as one block, so a wrapper's own declarations are emitted once per class. Selector suffixes retain their existing placement; a final ! marks every expanded declaration important. The class head and its condition suffixes other than @apply order the class: each outer condition branch is one rule that sorts on its own, while the mixins' inner selectors and conditions stay in the emitted text without moving the class to a later tier, and the expansion keeps its authored order. An expansion that adds no declaration, selector or condition sorts with single-declaration utilities, or with native declarations for a (property:value) head; any other expansion sorts as a recipe. Inspection and conflicts use the expanded rules.

Unused definitions, discarded contents and unused fallback blocks do not retain tokens or keyframes. Delivered native @apply remains a resource root through the emitted-globals contract. Declaration locations and relative URLs retain their defining or calling file.

The wrapper places the caller’s declarations

The highlighted slot and inserted declaration share one position inside the selector boundary. The wrapper controls where the declaration applies.

Text description
  1. @mixin --hocus { &:is(:hover, :focus-visible) { @contents; } } defines a selector wrapper with an explicit contents slot.
  2. underline@apply(--hocus) supplies text-decoration-line: underline to that slot. The emitted rule matches the class owner in either state.
  3. The visual result abbreviates the escaped class owner as & to keep the placement readable; the generated CSS retains the full owner.

Whole-definition replacement

A later definition replaces the complete body of the same registration; declarations are not merged. A static utility and a function utility with the same name are separate registrations. Token patterns replace only the branch with the same decoded prefix and namespace.

@utility badge {  color: red;  padding: 1rem;}@utility badge {  color: blue;}

badge now emits only color: blue. The earlier padding and its dependencies are gone.

A later definition replaces the whole registration

Both definitions register the same static utility. The second replaces the first, including its padding. This is definition replacement before generation, not two CSS rules competing in the cascade.

Text description
  1. The first static badge definition contains color: red and padding: 1rem.
  2. The later static badge definition contains only color: blue and replaces the first definition completely.
  3. Using badge generates one rule containing color: blue. The earlier padding and its dependencies do not survive.
  4. Static and functional registrations remain independent. Token patterns replace only the branch with the same prefix and namespace; mixins replace their complete named definition.

A mixin name has one final definition, including its parameter list, body and dependencies. An empty definition remains registered. Forward nested calls resolve against the final registry; there is no parameter overloading.

A static name and a function are separate registrations

The lanes encode registration identity. Equal names do not make the two definitions overloads or a merged body.

Text description
  1. @utility badge { display: inline-block; } registers the static badge class. @utility badge(--inset) { padding: var(--inset); } registers a separate function.
  2. Replacing the static definition with display: block changes badge, while badge(8px) still writes padding: 8px.
  3. Replacement affects the complete body of the same form and name. The function is not a fallback for the static class.

Definition scope and imports

@theme, @utility, @mixin, @custom-media, and @custom-container are top-level definitions. Registering them inside a layer or conditional wrapper is an error. Mixin bodies may contain declarations, nested selectors, supported native conditions, nested @apply, and @contents. Layer placement belongs at the call site.

Classes from source scanning, safelists, engine APIs and runtime DOM changes are usage roots. An @apply in delivered native CSS is also a root; one inside an unused utility or mixin or undelivered @reference stylesheet is not. Dependency collection occurs after parameter binding and ident() folding. Used tokens preserve ordered declarations and transitive dependencies. Static tokens remain roots without consumers. Removing the final root releases its resources; native CSS resources remain pinned through emittedGlobals.

Runtime manifests retain unused definitions for future DOM classes. On-demand generation concerns CSS and resource output. Native CSS pruning remains opt-in; keyframes share their declaring file’s native preservation and pruning policy. Qualified imports must contain native CSS only. Load definitions through an unqualified import or reference.

Native property precedence

A native (property:value) class retains its CSS property meaning independently of utility registration. Under equal conditions, single-declaration utilities and native declaration classes can override part of a recipe. See Declarations and values for complete ordering and override rules.

Native defaults

Write native defaults in @layer defaults.

Native components

Write native component selectors in @layer components.

@layer components {  .button {    display: inline-flex;    padding: var(--spacing-md);    &:focus-visible {      outline: 2px solid currentColor;    }  }}

Native rules are delivered according to the stylesheet import graph. Utility definitions register class names; mixin definitions provide CSS reuse through @apply. Both are consumed during compilation and emit CSS only when used.

Sass interoperability

Place CSS mixin definitions in a .css file when using Sass. Current Dart Sass reserves @mixin --name for future CSS support and rejects it as a Sass mixin; see Sass's compatibility notice. An SCSS file may emit the native at-rule through interpolation before Master CSS compiles it:

@#{'mixin'} --card {  padding: 1rem;}.card { @apply --card; }

This is Sass interpolation, not additional Master CSS syntax. Sass @include and CSS @apply remain separate constructs.

Theme selector wrappers

Native selectors and queries activate theme overrides. A mixin can provide a matching class condition:

@mixin --ocean {  &:where([data-theme="ocean"], [data-theme="ocean"] *) {    @contents;  }}

Use (color:white)@apply(--ocean). Add explicit :host(...) selectors when targeting a shadow host.