mcpField
W
web-styling-scss-modules

web-styling-scss-modules

github.com/agents-inc设计与 UI2674 次安装2 天前更新
代码仓库

关于 web-styling-scss-modules

面向使用 SCSS Modules 编写组件样式的开发者,介绍局部作用域、Sass 模块组织、层叠层,以及构建时值与运行时自定义属性的区别。

主要功能

  1. 01局部作用域模块类
  2. 02使用 @use 和 @forward 组织 Sass
  3. 03用层叠层控制样式覆盖
  4. 04用属性选择器表示组件状态
  5. 05构建时值与运行时属性
  6. 06混入与有意设置的全局选择器

适用场景

  1. 01编写样式隔离的 UI 组件
  2. 02整理跨文件 Sass
  3. 03允许应用样式覆盖库样式
  4. 04通过自定义属性支持主题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-styling-scss-modules

安装后在 Claude Code 中输入/web-styling-scss-modules确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
advanced.md
core.md
modules.md
patterns.md
theming.md
tokens.md

详细介绍

SCSS 模块模式

快速指南: *.module.scss 文件是局部作用域样式表:其类名会在构建时重写,并通过导入的对象访问,因此组件的样式不会与其他组件冲突。将库样式包裹在 @layer components {} 中,这样应用就能通过未分层规则覆盖它们,无需比拼优先级。使用 @use 和 @forward 加载 Sass——@import 已弃用,即将移除。使用属性选择器设置状态样式,而不是为每种状态创建一个类;任何必须无需重新构建即可更改的内容,都应放在自定义属性中,而不是 Sass 变量中。

详细资源:

  • examples/core.md — 局部作用域和 styles 对象、层叠层设置、分层组件样式、未分层应用覆盖样式
  • examples/modules.md — @use、@forward、命名空间、with 配置、内置模块、从 @import 迁移
  • examples/patterns.md — 模块文件结构、数据属性状态、混入、全局样式、图标尺寸
  • examples/tokens.md — 在模块样式中读取设计令牌、组件作用域自定义属性
  • examples/theming.md — 让模块不包含主题分支逻辑
  • examples/advanced.md — :has()、:global()、嵌套深度
  • reference.md — 反模式及修正方法、@use 与 @import 的对比、Sass 弃用说明

<critical_requirements>

编写 SCSS Modules 代码前

通过导入的对象访问每个类。 样式表中的名称并非浏览器实际接收的 CSS 中的名称,因此字面量 className="button" 不会匹配任何内容——不会报错,没有样式,也没有线索。

将库组件样式放在 @layer components {} 中。 应用可以用普通的未分层规则覆盖它们,无需争夺优先级,也不受样式表加载顺序影响。

使用 @use 加载 Sass 文件。 它会为加载的内容添加命名空间,在每次编译中只评估每个文件一次,并将以下划线 _ 开头的成员设为私有——这些都是 @import 不具备的功能,而且自 Dart Sass 1.80.0 起,@import 已弃用,并将在 3.0.0 中移除。

必须在不重新构建的情况下更改的值,应保留为自定义属性。 Sass 变量和 darken() 会在编译时解析,因此运行时无法访问它们。

使用组件自身类上的属性选择器设置状态样式。 &[data-state="open"] 会将所有状态保留在样式表中;而为每种状态设置一个类,则会将组件外观分散到用于选择这些状态的标记中。

</critical_requirements>


自动检测: .module.scss、.module.css、styles. 类访问、@use、@forward、@layer components、:global()、@mixin、@include、!default、sass:math、math.div、sass:color、color.scale、Sass 中的 @import、sass-migrator

适用于:

  • 在 *.module.scss 中编写组件样式,并使用局部作用域类名
  • 使用 @use 和 @forward 组织跨文件 Sass,并从 @import 迁移
  • 使用层叠层控制库样式与应用覆盖样式之间的优先级
  • 判断哪些内容应使用构建时 Sass 值,哪些应使用运行时自定义属性
  • 将重复声明提取为 mixin
  • 使用 :global() 有意跳出模块作用域

其他部分处理:

  • 有哪些设计令牌、它们如何分层以及如何命名——模块样式通过 var() 读取它们,但不会定义它们
  • 如何在运行时选择、持久化和应用主题——模块的职责是不包含任何主题分支,因此无需接收指示即可适配
  • 如何将一组组件属性转换为类列表——模块会_导出_类名;由什么来组合这些类名属于另一项工作
  • 覆盖整个设计空间的生成式工具类——模块系统为手写 CSS 限定作用域,这是另一种权衡

作用域隔离正是关键。因为唯一性由构建过程负责,样式表可以使用简短直观的名称,无需命名约定来维系整个代码库——不需要 BEM 风格的前缀,而且删除组件时也会一并删除其样式。

由此得出两个结果,本技能的大部分内容都由此延伸。不会意外产生全局效果,因此任何真正的全局内容——重置样式、层声明、设计令牌——都应有意在模块之外编写。而且一个模块无法访问另一个模块的名称,因此共享决策应通过 @use 以 Sass 成员的形式传递,或通过层叠以自定义属性的形式传递,而不是依赖两个文件约定使用的类名。


核心模式

模式 1:局部作用域——类名会成为对象键

导入 *.module.scss 文件会得到一个对象,其键是文件中声明的类,其值是构建过程生成的全局唯一名称。这个对象是访问实际出现在 CSS 中的名称的唯一途径。

// button.module.scss
.button {
  padding: var(--space-md);
}
.sizeSm {
  padding: var(--space-sm);
}
// button.tsx
import styles from "./button.module.scss";

// styles.button is something like "button-module__x7f2q" — never "button"
<button className={`${styles.button} ${styles.sizeSm}`} />;

生成名称的格式由构建设置决定,因此任何内容都不得依赖它。键是编写时使用的类名:除非构建配置为添加 camelCase 别名,否则 .size-sm 应通过 styles["size-sm"] 访问——这也是样式表一开始就使用 camelCase 命名类的原因。

完整代码:examples/core.md


模式 2:级联层,实现可预测的优先级

只需声明一次层顺序,并将库的组件样式放在 @layer components 中。应用样式保持未分层状态;无论加载顺序如何,级联都将其排在所有层之上。

// layers.scss — loaded first, before anything it names
@layer reset, components;
// button.module.scss — a library component
@layer components {
  .button {
    background-color: var(--color-primary);
    color: var(--color-primary-foreground);
  }
}
// an application stylesheet — no layer, so it wins
.myCustomButton {
  background-color: var(--color-accent);
}

完整代码:examples/core.md


模式 3:Sass 模块系统

@use 会以命名空间加载文件,每次编译仅加载一次。@forward 会通过另一个文件重新导出一个文件的成员,目录正是通过这种方式获得统一入口。

// styles/_index.scss — the public surface
@forward "layers";
@forward "design-tokens";
@forward "mixins";
// button.module.scss
@use "../../styles" as s;

@layer components {
  .button {
    padding: s.$space-md;
    @include s.focus-ring;
  }
}

@use "…" as * 会移除命名空间,仅适用于在各处使用的基础模块。使用 with 配置的模块会在整个编译过程中保留该配置——首次加载时的配置会生效。

完整代码:examples/modules.md


模式 4:构建时 Sass 值与运行时自定义属性

浏览器看到 CSS 时,Sass 变量早已不存在。任何需要响应主题、密度设置或媒体条件的内容,都必须使用自定义属性。

@use "sass:math";

// Build-time: a value computed once, that never changes at runtime
$column-width: math.div(100%, 12);

.grid {
  grid-template-columns: repeat(12, $column-width);

  // Runtime: reassigning this token elsewhere re-styles the element
  gap: var(--space-md);
  background: color-mix(in srgb, var(--color-primary), black 5%);
}

math.div() 替代 /,因为 / 可能与 CSS 的斜杠分隔符产生歧义(grid-template: 1fr / 2fr)。对于真正的构建时颜色计算,color.scale() 替代 darken() 和 lighten();但主题需要能够更改的颜色应使用自定义属性,并与 color-mix() 或相对颜色语法搭配。

完整代码:examples/modules.md


模式 5:读取令牌,绝不重新声明

模块通过 var() 使用设计令牌。只有在某个值需要在组件_内部_变化时,才声明自己的自定义属性——并且将其声明在组件自身的类上,使其作用范围限定在该子树中。

@layer components {
  .button {
    // Component-scoped: the variants below reassign it
    --button-accent-bg: transparent;

    padding: var(--space-md);
    font-size: var(--font-size-body);
    color: var(--color-text-default);
    background: var(--button-accent-bg);
  }

  .outline:hover {
    --button-accent-bg: var(--color-background-muted);
  }
}

重新声明已经存在的令牌(在 --radius-sm 旁边写入 --card-radius: 0.5rem),会创建第二个值,使其不再跟随原来的值变化。

完整代码:examples/tokens.md


模式 6:用数据属性表示状态

一个类名用于标识组件,属性选择器用于标识组件状态。这样状态就能组合使用,无需为每种组合都创建一个类。

.dropdown {
  &[data-open="true"] {
    display: block;
  }

  &[data-size="large"][data-variant="primary"] {
    padding: var(--space-xlg);
  }

  &[aria-invalid="true"] {
    border-color: var(--color-destructive);
  }
}

使用字符串值(data-state="open"),而不是没有值的布尔属性——属性值选择器对每种状态的读取方式都一致,而且 DOM 会显示组件当前的状态。

完整代码:examples/patterns.md


模式 7:用混入复用重复的声明块

当多个组件需要相同的声明块时,混入是合适的方案——尤其是无障碍相关声明,因为只复制其中一部分就会造成错误。

@mixin focus-ring {
  &:focus-visible {
    outline: 2px solid var(--color-ring);
    outline-offset: 2px;
  }
}

@mixin sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.button {
  @include focus-ring;
}

等到第三处使用时再提取,而不是第一次使用时。只使用一次的混入,不过是多绕了一步的 CSS 代码块。

完整代码:examples/patterns.md


模式 8:与主题无关的组件样式

模块只定义角色,不定义模式。样式表只需编写一次,每个主题都通过模块已读取的令牌来应用样式。

@layer components {
  .button {
    background-color: var(--color-primary);
    color: var(--color-primary-foreground);

    // No `.dark &`, no theme conditional — the tokens carry the mode
    &:hover {
      background-color: var(--color-primary-hover);
    }
  }
}

模块中的 .dark & 选择器会将该模块与一种主题实现绑定,而每增加一种主题,就意味着每个包含此选择器的组件都要增加一个分支。

完整代码:examples/theming.md


模式 9:审慎处理转义和嵌套

:global() 可让选择器不受作用域限制,从而能够访问第三方类名。使用 & 嵌套可让修饰符紧跟其所修饰的对象。

.component {
  padding: var(--space-md);

  // Reach a class this module does not own
  :global(.third-party-widget) & {
    padding: 0;
  }

  // `:has()` styles a parent from its children — no JavaScript needed
  &:has(input:focus) {
    background: var(--color-background-muted);
  }
}

嵌套层级尽量控制在约三层:每增加一层都会提高优先级,并让生成的选择器与标记中出现的名称相距更远。

完整代码:examples/advanced.md


<red_flags>

危险信号

在构建时失败:

  • 在新的 Sass 中使用 @import — 自 Dart Sass 1.80.0 起已弃用,并将在 3.0.0 中移除。@use 可替代它;sass-migrator module --migrate-deps 可以完成大部分转换。
  • 用 / 做除法(100% / 2)— 已弃用,且容易与 CSS 的斜杠分隔符混淆。使用 sass:math 中的 math.div() 可明确表达意图。calc() 中的 / 不受影响。
  • 在样式规则后使用 @use — 它必须位于除 @forward 和 @charset 之外的所有内容之前,因此如果它上方有规则,就会导致编译错误。变量声明是文档明确说明的例外,因为使用 with 配置模块时,需要先在上方声明变量。
  • 缺少层声明 — 必须在分层内容之前处理 @layer reset, components;,否则顺序就取决于各文件碰巧建立顺序。

静默地不起作用:

  • 在标记中将类名写成字符串(className="button")— 输出的名称是自动生成的,因此字面名称匹配不到任何规则。不会报错,元素也会以无样式状态渲染。
  • 将带连字符的类名作为属性读取(styles.size-sm)— 这会被解析为减法,而不是查找。类名使用 camelCase,或使用 styles["size-sm"]。
  • 在模块中声明类,但从未通过对象读取 — 该类的作用域限定在一个无人引用的名称上,因此会作为无用 CSS 打包,搜索类名也找不到它。

影响主题或覆盖能力:

  • 组件样式没有层包装 — 优先级会取决于加载顺序,而应用要覆盖它,就需要使用特殊的优先级技巧,后续覆盖还必须胜过这个技巧。
  • 将应用样式包装在 @layer components 中 — 它们会处于库的优先级,因此无法再覆盖库,而覆盖库正是这些样式存在的目的。
  • 对可切换主题的颜色使用 Sass 颜色函数(darken($primary, 10%))— 结果在构建时就已确定,因此运行时切换主题无法影响它。color-mix() 和相对颜色语法则会保持动态。
  • 在模块中使用主题选择器(.dark & { … })— 这会让组件绑定到一种主题机制,而每增加一种主题,每个使用该机制的组件都要增加一个分支。
  • 硬编码颜色、间距和圆角 — 这些未追踪的值不会随令牌变更而变化,也不会被审计发现。

容易出乎意料的行为:

  • 未分层的样式优先于所有层,不受加载顺序影响。这正是应用覆盖所依赖的机制,也因此,意外未分层的库规则很难覆盖。
  • 在同一层内,仍按常规特异性规则处理——层只在彼此之间决定优先级。
  • 使用 @use "…" with (…) 配置的模块,即使被其他文件以未配置的方式加载,也会保留该配置。首次加载优先。
  • 前缀为 _ 或 - 的成员是文件私有的,通过 @use 和 @forward 都不可见。
  • 内置模块变量(例如 math.$pi)是只读的。
  • :global() 会移除其包裹选择器的作用域,因此模块深处的 :global 块会悄然重新引入模块原本要避免的那些冲突。

评论

支持 Markdown