网站导航

编写样式

复用样式

根据哪些内容应一起变化,选择令牌、组件类名、工具类和混入,同时保留易于表达的局部变化。

本页内容

两个页面可以共享保存按钮,又让其中一个实例拥有更大间距。将按钮的共享外观放入组件类名,局部调整则保留在标记中。更小尺度也适用相同方法:多个使用方应一起变化时为值命名;多条规则需要同一段 CSS 时就共享它。

请先完成安装和语法教程。下方每个示例都提供自己的 CSS 和标记。将 CSS 加在应用样式表现有的预设导入之后,并将标记放入集成会扫描的源文件:

app.css
@import '@master/css';

#将组件共享外观放在一起

操作按钮这类产品角色无论用于哪里,都需要可识别的外观。请在 @layer components 中用原生类名定义此角色,并包含每个实例都应有的状态。

同一种按钮,局部间距调整

@theme {  --spacing-action-x: 1rem;  --color-action: var(--color-surface-raised);}@layer components {  .action {    display: inline-flex;    align-items: center;    justify-content: center;    min-height: 2.375rem;    padding: .5rem var(--spacing-action-x);    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-md);    background-color: var(--color-action);    color: var(--color-text-strong);    box-shadow: var(--shadow-sm);    font: inherit;    font-size: var(--font-size-sm);    font-weight: 400;    line-height: 1.2;    &:is(:hover, :focus-visible) {      box-shadow: var(--shadow-md);    }    &:active {      background-color: var(--color-surface-inset);      box-shadow: none;    }    &:focus-visible {      outline: 2px solid var(--color-text-strong);      outline-offset: 3px;    }  }}
生成的 CSS
@layer components {  .action {    min-height: 2.375rem;    padding: .5rem var(--spacing-action-x);    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-md);    background-color: var(--color-action);    color: var(--color-text-strong);    box-shadow: var(--shadow-sm);    font: inherit;    font-size: var(--font-size-sm);    justify-content: center;    align-items: center;    font-weight: 400;    line-height: 1.2;    display: inline-flex;    &:is(:hover, :focus-visible) {      box-shadow: var(--shadow-md);    }    &:active {      background-color: var(--color-surface-inset);      box-shadow: none;    }    &:focus-visible {      outline: 2px solid var(--color-text-strong);      outline-offset: 3px;    }  }}@layer theme {  :root,  :host {    --color-zinc-5: oklch(96.5% .0011 265);    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100));    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-size-xs: .75rem;    --font-size-sm: .875rem;    --text-xs: var(--font-size-xs);    --text-xs--line-height: 1.5;    --text-xs--letter-spacing: 0;    --radius-md: .375rem;    --spacing-sm: .75rem;    --spacing-lg: 1.5rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24));    --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26))  }  :root,  :host {    --spacing-action-x: 1rem;    --color-action: var(--color-surface-raised)  }}@layer utilities {  .text-xs {    font-size: var(--text-xs);    line-height: var(--text-xs--line-height, normal);    letter-spacing: var(--text-xs--letter-spacing, normal)  }  .items-start {    align-items: start  }  .fg-text-muted {    color: var(--color-text-muted)  }  .flex {    display: flex  }  .flex-col {    flex-direction: column  }  .flex-wrap {    flex-wrap: wrap  }  .gap-lg {    gap: var(--spacing-lg)  }  .gap-sm {    gap: var(--spacing-sm)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .px-lg {    padding-inline: var(--spacing-lg)  }}
比较水平内边距,再悬停各按钮或用 Tab 聚焦。两者共享操作状态,只有第二个使用 px-lg。这些按钮只演示样式,不会保存数据。

px-lg 的普通工具类声明优先于组件内边距,因为预设将 utilities 层放在 components 之后。HTML 类名顺序不决定胜负。请将局部调整保留在第二个按钮上;仅当所有实例都需要时才放入 .action。竞争声明请参阅层叠层。

一份定义,两处使用

两个按钮共享 .action。辅助线标示水平内边距:组件每边 1rem,使用 px-lg 则为 1.5rem。蓝线是注释,不是按钮样式。

文字说明
  1. 共享定义. components 层中的原生 .action 规则为两个按钮提供外观、交互状态和默认内边距。
  2. 第一个实例. class="action" 使用组件左右各 1rem 的水平内边距。
  3. 第二个实例. class="action px-lg" 请求左右各 1.5rem 的水平内边距。utilities 层位于 components 之后,因此 px-lg 只覆盖此实例的水平内边距,其他组件样式仍然适用。图中的蓝色带和尺标仅标示内边距,是检查注释,不是工具类新增的样式。

原生组件规则默认随样式表输出;原生裁剪需要显式启用。将组件状态写在 CSS 规则中。原生 .action 定义不会注册 action:hover 工具类。

共享外观和共享交付是两个不同决策。如果可复用组件让无关页面也加载其规则,请参阅优化 CSS 交付。

#选择哪些内容应一起变化

抽取重复类名列表前,先确认共享的是哪个决策。外观相似的元素不一定应一起变化。

共享内容使用方式
跨样式共享值主题令牌
产品角色和状态原生组件类名
在标记中组合的能力工具类
CSS 内的声明或包装混入

第一个示例为操作颜色和内边距分别提供令牌。其他组件可以在 CSS 中用 var(),或在标记中用 px-action-x 读取 --spacing-action-x,无需一并采用按钮背景、形状和状态。两个使用方都应变化时才修改令牌;数值只是碰巧相同但用途不同时,请分别定义。主题令牌说明了命名、别名和局部覆盖。

重复使用的组件也可将类名列表保留在框架组件内部。需要有用的共享样式契约时再引入 CSS 名称;不要只因 class 属性很长就抽取它。

#为样式能力注册工具类

假设多个控件都需要内容居中、带小间隔的行内排列,但各自保有外观和间距。仅将共享布局注册为 @utility:

相同布局,不同控件

@utility action-row {  display: inline-flex;  align-items: center;  justify-content: center;  gap: var(--spacing-xs);}
生成的 CSS
@layer theme {  :root,  :host {    --color-zinc-5: oklch(96.5% .0011 265);    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-70: oklch(39.8% .0028 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100));    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-size-sm: .875rem;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --radius-md: .375rem;    --spacing-xs: .5rem;    --spacing-sm: .75rem;    --spacing-md: 1rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24));    --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26))  }}@layer utilities {  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .action-row {    display: inline-flex;    align-items: center;    justify-content: center;    gap: var(--spacing-xs)  }  .items-center {    align-items: center  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .b-line-divider {    border-color: var(--color-line-divider)  }  .r-md {    border-radius: var(--radius-md)  }  .b-solid {    border-style: solid  }  .b\(1\) {    border-width: calc(1 * 1px)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .fg-text-body {    color: var(--color-text-body)  }  .fg-text-strong {    color: var(--color-text-strong)  }  .flex {    display: flex  }  .flex-wrap {    flex-wrap: wrap  }  .gap-md {    gap: var(--spacing-md)  }  .py-xs {    padding-block: var(--spacing-xs)  }  .px-md {    padding-inline: var(--spacing-md)  }  .px-sm {    padding-inline: var(--spacing-sm)  }  .underline {    text-decoration-line: underline  }  .shadow-md\:hover:hover {    box-shadow: var(--shadow-md)  }  .fg-text-strong\:hover:hover {    color: var(--color-text-strong)  }  .bg-surface-inset\:active:active {    background-color: var(--color-surface-inset)  }}
action-row 让两个控件具有相同对齐和间隔;其他类名提供外观。Add note 是样式示例;Choose tokens 会打开主题指南。

完整类名提出请求时,工具类才生成规则。它也接受与预设工具类相同的选择器和条件后缀,例如 action-row@sm。名称描述布局能力,不会规定整个按钮的样式。

预设中已有工具类能表达共享决策时,直接使用即可。例如,只改间隔需要的是 gap-sm 或 gap(8),无需另一个自定义工具类。多个声明形成调用方应一起复用的能力时,再定义配方。工具类契约定义了注册和替换方式。

#通过混入复用 CSS

如果原生 CSS 规则需要相同的行布局,请定义混入并用 @apply 展开。本示例让操作按钮和工具栏链接保留不同组件角色,同时共享对齐方式:

@mixin --action-row {  display: inline-flex;  align-items: center;  justify-content: center;  gap: var(--spacing-xs);}@layer components {  .action {    @apply --action-row;    padding: .5rem 1rem;    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-md);    background-color: var(--color-surface-raised);    color: var(--color-text-strong);    box-shadow: var(--shadow-sm);    font: inherit;    font-size: var(--font-size-sm);  }  .toolbar-link {    @apply --action-row;    color: var(--color-text-body);    font: inherit;    font-size: var(--font-size-sm);    text-decoration-line: underline;  }}
<button type="button" class="action"><span aria-hidden="true">+</span> Add note</button><a href="/guide/theme" class="toolbar-link">Choose tokens <span aria-hidden="true">→</span></a>
生成的 CSS
@layer components {  .action {    display: inline-flex;    align-items: center;    justify-content: center;    gap: var(--spacing-xs);    padding: .5rem 1rem;    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-md);    background-color: var(--color-surface-raised);    color: var(--color-text-strong);    box-shadow: var(--shadow-sm);    font: inherit;    font-size: var(--font-size-sm)  }  .toolbar-link {    display: inline-flex;    align-items: center;    justify-content: center;    gap: var(--spacing-xs);    color: var(--color-text-body);    font: inherit;    font-size: var(--font-size-sm);    text-decoration-line: underline  }}@layer theme {  :root,  :host {    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-70: oklch(39.8% .0028 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-size-sm: .875rem;    --radius-md: .375rem;    --spacing-xs: .5rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24))  }}

打开生成的 CSS:行声明会出现在两条原生规则中,而各规则保留其余独立声明。定义 --action-row 不会建立标记类名。@apply 指定的是混入,不接受工具类列表。

CSS 和标记都需要相同能力时,将混入保留为声明来源,再加入调用它的工具类:

@utility action-row {  @apply --action-row;}

将此定义加入混入示例的样式表。它替换先前独立的工具类主体:修改 --action-row 现在会同时影响原生组件和工具类。请按照全局样式将这些定义整理到应用入口图。

工具类拥有类名;混入填入选择器

方框代表 CSS 产物。@apply 展开不会注册另一个标记类名。

文字说明

请求 stack 工具类会输出带 display:grid 的 .stack。在 .card 内应用 --stack 混入,会在 .card 下输出相同声明;选择器由调用方拥有。

#通过 contents 共享包装

有时重复的是包住不同声明的条件。悬停与键盘焦点混入可以提供选择器,再由各调用方通过 @contents 提供样式:

一个交互包装,两处调用

@mixin --hocus {  &:is(:hover, :focus-visible) {    @contents;  }}@layer components {  .action {    @apply --hocus {      text-decoration-line: underline;    }  }}
生成的 CSS
@layer components {  .action:is(:hover, :focus-visible) {    text-decoration-line: underline  }}@layer theme {  :root,  :host {    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-size-sm: .875rem;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --radius-md: .375rem;    --spacing-xs: .5rem;    --spacing-md: 1rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24))  }}@layer utilities {  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .underline\@apply\(--hocus\):is(:hover, :focus-visible) {    text-decoration-line: underline  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .b-line-divider {    border-color: var(--color-line-divider)  }  .r-md {    border-radius: var(--radius-md)  }  .b-solid {    border-style: solid  }  .b\(1\) {    border-width: calc(1 * 1px)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .fg-text-strong {    color: var(--color-text-strong)  }  .flex {    display: flex  }  .flex-wrap {    flex-wrap: wrap  }  .gap-md {    gap: var(--spacing-md)  }  .outline-offset\(3\) {    outline-offset: calc(3 * 1px)  }  .py-xs {    padding-block: var(--spacing-xs)  }  .px-md {    padding-inline: var(--spacing-md)  }  .outline-solid\:focus-visible:focus-visible {    outline-style: solid  }  .outline\(2\)\:focus-visible:focus-visible {    outline-width: calc(2 * 1px)  }}
悬停或用 Tab 聚焦任一按钮,标签就会出现下划线。第一个在原生 CSS 内提供声明,第二个包装 underline 工具类。焦点轮廓单独指定。

CSS 中,@apply --hocus 后的块填入 @contents。标记中,@apply(--hocus) 包住该类名生成的声明。包装不会注册令牌,也不会将相邻类名收进主体。复用一组声明请使用工具类或混入;调用方需要自行选择共享选择器或条件内的内容时,才使用 contents。转发和回退行为请参阅 混入 contents。

#调用方需要时才开放参数

共享控件尺寸可能联动多个属性。如果分开的预设工具类会让调用方重复计算,请用参数表达此关系:

一个尺寸控制高度和内边距

@utility action-size(--height: 2.5rem) {  min-height: var(--height);  padding-inline: calc(var(--height) / 2);}
生成的 CSS
@layer theme {  :root,  :host {    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-size-sm: .875rem;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --radius-md: .375rem;    --spacing-md: 1rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24))  }}@layer utilities {  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .action-size\(\) {    min-height: 2.5rem;    padding-inline: calc(2.5rem / 2)  }  .action-size\(3rem\) {    min-height: 3rem;    padding-inline: calc(3rem / 2)  }  .items-center {    align-items: center  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .b-line-divider {    border-color: var(--color-line-divider)  }  .r-md {    border-radius: var(--radius-md)  }  .b-solid {    border-style: solid  }  .b\(1\) {    border-width: calc(1 * 1px)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .fg-text-strong {    color: var(--color-text-strong)  }  .flex {    display: flex  }  .flex-wrap {    flex-wrap: wrap  }  .gap-md {    gap: var(--spacing-md)  }}
action-size() 使用 2.5rem;action-size(3rem) 使用 3rem。最小高度和水平内边距都跟随实参。这些按钮只演示尺寸。

一份定义,各自传入值

根字体为 16px 时,最小高度为 40px 和 48px;padding-inline 为所选高度的一半。高亮的内缩区域是测量辅助线。

文字说明

相同 action-size 工具类分别以默认 2.5rem 和 3rem 调用。第二个控件具有更大最小高度和水平内边距,两者仍保有各自实参。

空调用使用声明的默认值。实参必须在编译时已知;选择尺寸时请使用完整类名字符串。随 JavaScript 连续变化的值应放入原生 CSS 自定义属性。参数契约说明了接受的类型和限制。

更改共享定义前,请至少比较两处调用,包括一个有局部覆盖的实例。检查悬停、键盘状态和生成的 CSS。如果调用方不再需要相同行为,请拆分定义,不要给每个实例追加例外。跨项目共享稳定词汇时,请继续阅读编写软件包。