基礎原理

重用設計

Decide when to keep utilities in markup, move values into theme tokens, or create reusable component classes.

Overview

Reusable styling starts with a decision, not a file. Keep local choices in markup, move shared values into theme tokens, and define component classes only when a repeated pattern has product meaning.

Writing .btn at the top level of a stylesheet creates a normal CSS rule. Writing btn inside @components defines an on-demand component class. Writing flow inside @utilities defines an on-demand utility class. All three can use @compose and @variant, but they serve different ownership needs.


Reuse decisions

Start with utilities

Use utility classes when the style belongs to one element, one state, or one local layout decision. This keeps the relationship between markup and style explicit while the pattern is still changing.

<button class="inline-flex align-items:center justify-content:center gap:xs px:md py:xs r:lg font:sm font:medium bg:blue fg:white">    Save changes</button>

Do not abstract just to shorten a class list. Abstract when the name improves the product vocabulary: btn, card, field, toolbar, empty-state, or another role that appears across the interface.

Move shared values into the theme

When the repeated part is a value, make it a token instead of a class. Tokens keep markup semantic while still letting each component choose its own structure.

@theme {    --color-brand: $color-blue-60;    --spacing-action-x: 1rem;}
<button class="px:action-x r:lg bg:brand fg:white">    Submit</button>

Use Customizing your theme when the reusable part is color, spacing, typography, radius, shadow, motion, breakpoints, container sizes, or mode-specific values.

Create custom utilities

Use @utilities when your design system needs a reusable primitive that is still lower-level than a product component. Good utility names describe a styling capability, not a UI role.

@utilities {    flow {        @compose grid gap:md;    }    content-auto {        content-visibility: auto;        contain-intrinsic-size: auto 32rem;    }    print-hidden {        @variant @print {            display: none;        }    }}
<section class="flow content-auto print-hidden">...</section>

Custom utility definitions are compiled into the project manifest and generated only when the class appears in markup, is scanned from source, or is composed by another CSS-defined style. Use custom utilities for reusable primitives such as layout helpers, rendering hints, or print behavior. Use component classes when the name describes a product UI role such as .btn, .card, or .field.


Component classes

Define a component class

Use a component class when a repeated class list becomes a reusable UI pattern. Define components in @components, put Master CSS classes in @compose, and write native declarations directly in the named block.

@components {    btn {        @compose inline-flex align-items:center justify-content:center gap:xs px:md py:xs r:lg font:sm font:medium;        @compose bg:blue fg:white;    }}
<button class="btn">Save changes</button>

The generated CSS is emitted in the components layer:

@layer components {    .btn {        display: inline-flex;        gap: 0.5rem;        border-radius: 0.5rem;        padding-left: 1rem;        padding-right: 1rem;        padding-top: 0.5rem;        padding-bottom: 0.5rem;        align-items: center;        font-size: 0.875rem;        font-weight: 500;        justify-content: center;        background-color: var(--color-blue);        color: #fff;    }}

Component definitions are manifest inputs, not immediate CSS output. Defining .btn does not generate CSS until btn appears in scanned classes or is composed by another CSS-defined style.

You can still apply component classes alongside conditional utility classes when a variation belongs to one usage site:

<button class="btn btn-md btn-sm@<sm …">Submit</button>
Generated CSS
@layer components {    .btn {        display: inline-flex;        font-weight: 600    }    .btn-md {        border-radius: .375rem;        padding-left: 1rem;        padding-right: 1rem;        font-size: .875rem;        height: 2.5rem    }    @media (width<52.125rem) {        .btn-sm\@\<sm {            border-radius: .375rem;            padding-left: .75rem;            padding-right: .75rem;            font-size: .75rem;            height: 2rem        }    }}

Add states and conditions

Put selector states in nested selectors and conditional variants in @variant blocks. This keeps the component definition readable and keeps the markup semantic.

@components {    btn {        @compose inline-flex align-items:center justify-content:center px:md py:xs r:lg;        transition: background-color .15s ease, box-shadow .15s ease;        &:hover {            @compose bg:blue/.9;        }        &:focus-visible {            @compose outline:2px|solid|blue outline-offset:0.125rem;        }        @variant @<sm {            @compose block;        }    }}

Equivalent markup stays short:

<button class="btn">Continue</button>

Try clicking the button to see the outline effect


Keep variants composable

Avoid hiding every option behind a new component. Give each component class one responsibility, then combine it with tokens, utilities, or smaller component classes.

@components {    btn {        @compose inline-flex align-items:center justify-content:center;        font-weight: 600;        outline-offset: -0.0625rem;    }    btn-xs {        @compose r:sm px:xs font:xs h:6x;    }    btn-sm {        @compose r:md px:sm font:xs h:8x;    }    btn-md {        @compose r:md px:md font:sm h:10x;    }    btn-lg {        @compose r:lg px:lg font:md h:12x;    }    btn-xl {        @compose r:xl px:lg font:md h:14x;    }}
<button class="btn btn-xs">Submit</button><button class="btn btn-sm">Submit</button><button class="btn btn-md">Submit</button><button class="btn btn-lg">Submit</button><button class="btn btn-xl">Submit</button>

Round the same button with a separate class instead of duplicating every size:

<button class="btn btn-md rounded">Submit</button>

If one class tries to own size, color, shape, layout, and interaction variants, overrides become fragile. Smaller classes compose better with utilities and rarely need !important.


Build shared structures

Create explicit classes for each meaningful part of a repeated structure. Avoid relying on descendant selectors when separate class names make the output easier to trace.

@components {    card {        border-radius: 0.5rem;    }    card-header {        border-bottom: 0.0625rem solid oklch(0% 0 none);    }    card-content {        padding: 1.25rem;    }    card-footer {        border-top: 0.0625rem solid oklch(0% 0 none);    }}
<article class="card">    <header class="card-header">Title</header>    <div class="card-content">Content</div>    <footer class="card-footer">Actions</footer></article>

Use nested selectors when the selector is truly part of the component behavior:

@components {    empty-state {        @compose grid align-items:center justify-content:center min-h:64x p:lg text-center;        &::before {            @compose block size:12x r:full;            content: '';            background: color-mix(in oklab, currentColor 12%, transparent);        }    }}

In this example, size:12x is generated for .empty-state::before, not for .empty-state.


Native CSS and files

Use native CSS when output should be native

@compose is not limited to component definitions. It can also be used in native style rules. The difference is output ownership:

  • @components, @defaults, and @utilities create on-demand managed class definitions.
  • Native style rules lower to ordinary CSS in the stylesheet that contains them.
@utilities {    flow {        @compose grid gap:md;    }}.marketing-card {    @compose flow p:lg r:xl shadow:md surface:raised b:1px|solid|base;    @dark {        @compose shadow:none;    }}

Use native CSS when the selector is part of a stylesheet surface you already ship. Use components when the class should behave like a generated Master CSS class and only emit when it appears in markup or source scanning.


Use local CSS Modules and SFC styles

CSS Modules and SFC <style> blocks can use @compose directly. They do not need @master entry; because they are local composed stylesheets, not project CSS entries.

.button {    @compose inline-flex align-items:center justify-content:center gap:xs px:md h:10x r:lg;    @compose bg:blue fg:white;}.button:hover {    @compose bg:blue/.9;}
import styles from './Button.module.css'export function Button() {    return <button className={styles.button}>Submit</button>}

Master CSS lowers @compose to native declarations first. Then CSS Modules rewrites .button to its scoped class name.

SFC style blocks use the same model:

<template>    <article class="card">        <slot />    </article></template><style scoped>.card {    @compose grid gap:md p:lg r:xl shadow:md;    @dark {        @compose shadow:none;    }}</style>

Local composed stylesheets share the project CSS entry and plugin-provided manifest context, so theme tokens, custom variants, and managed component classes remain available. Local files do not define the global project manifest, do not insert generated Master CSS, and do not participate as native CSS pruning roots.


Organize component files

Split component definitions only when it helps ownership. Local CSS imports from the project CSS entry are compiled into one graph.

styles
button.css
base.css
index.css
@import '@master/css';@import './styles/button.css';@import './styles/base.css';
@components {    yellow {        @compose b:1px|solid|yellow-70 bg:yellow fg:yellow-95;    }    touch-yellow {        &:hover {            @compose bg:yellow-50;        }    }}
<button class="btn btn-md yellow touch-yellow">Submit</button>

@theme, @settings, top-level custom directives, managed definition directives, and native style rules with @compose or @variant are consumed by the Master CSS compiler. Ordinary CSS outside those managed targets, including top-level native @keyframes, remains native CSS in your app stylesheet pipeline.


Rules and summary

Component rules

  • Put component definitions in @components; use @defaults only when a definition intentionally belongs to the earlier defaults layer.
  • The first level inside @defaults, @components, and @utilities must be a single bare name such as btn, card, or field.
  • Put states and descendants inside the named block, such as btn { &:hover { ... } } or prose { p { ... } }.
  • @compose is valid inside managed class definitions and native style rules.
  • Component definitions accept native declarations, @compose, nested selectors, native nested at-rules, and @variant condition blocks.
  • CSS-defined managed classes can compose other CSS-defined managed classes. Circular @compose dependencies are invalid.
  • Standard HTML tag selectors such as body or html belong in native CSS; first-level entries inside managed definition directives are always class names.
  • Utilities still win over components because @master/css/base.css declares @layer theme, base, defaults, components, utilities;.

Summary

  • Keep local styling in markup until a pattern proves it should be shared.
  • Move shared values into theme tokens before creating new classes.
  • Use custom utilities for reusable primitives that are not product UI roles.
  • Use component classes for reusable UI roles, not for every repeated declaration.
  • Keep component classes small enough to combine with tokens, utilities, states, and conditions.
  • Use native CSS when the selector belongs to a stylesheet you already ship; use managed component classes when the class should be generated on demand.

  • Master UI


© 2026 Aoyue Design LLC.MIT License
Trademark Policy