关于 css-architecture
面向 Rails 应用的 CSS 架构技能,指导开发者组织可维护的样式表。通过设计令牌、组件样式和 CSS 层管理级联,并以现代 CSS 实现暗色模式。
主要功能
- 01按组件拆分样式表
- 02使用设计令牌统一样式值
- 03通过 @layer 管理级联
- 04使用 light-dark() 支持暗色模式
- 05提供第三方 CSS 集成指导
适用场景
- 01为 Rails 应用新建或重组 CSS
- 02编写可复用的组件样式
- 03实现暗色模式与设计令牌
- 04排查级联和优先级问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill css-architecture安装后在 Claude Code 中输入/css-architecture确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/components.mddark-mode.mddesign-tokens.mdlayers.mdresponsive.mdutilities.md详细介绍
Rails CSS 架构专家
使用现代 CSS 特性为 Rails 应用编写易于维护、可扩展的 CSS,包括自定义属性、@layer、light-dark() 和语义设计令牌。无需依赖框架。
理念
核心原则:
- 组件管理自己的样式 — 每个组件对应一个 CSS 文件,绝不在页面样式表中为组件设置样式
- 设计令牌优于魔法数字 — 所有值都来自
_global.css自定义属性 - 层控制层叠 — 使用
@layer,而不是特异性技巧或!important - 深色模式轻松实现 —
color-scheme+light-dark()无需额外工作即可实现深色模式 - 工具类是调味料,不是主菜 — 仅用于偶尔的调整,不要用作主要样式
架构金字塔:
/\ Utilities (few — spacing/text tweaks)
/ \
/____\ Components (many — reusable UI pieces)
/ \
/________\ Base (element defaults, resets)
/ \
/____________\ Tokens (_global.css — design system foundation)
何时使用此技能
- 在 Rails 应用中创建或重组 CSS 文件结构
- 编写新的组件样式表
- 实现深色模式支持
- 定义或扩展设计令牌
- 调试层叠/特异性问题
- 从 Tailwind 或其他框架迁移到原生 CSS
- 设置 CSS 层
- 创建工具类
- 集成第三方 CSS 库
说明
第 1 步:了解文件结构
使用此架构的每个 Rails 应用都有以下结构:
app/assets/stylesheets/
├── _global.css # Design tokens: colors, spacing, typography, radii
├── application.css # Entry point — may just import everything
├── reset.css # CSS reset / normalization
├── base.css # Element defaults (no classes)
├── utilities.css # Single-purpose utility classes
└── components/
├── app-layout.css # Main layout shell
├── forms.css # Form elements + buttons
├── cards.css # Card component
├── badges.css # Badge variants
├── alerts.css # Alerts/flash messages
├── tables.css # Table styles
└── [feature].css # Feature-specific components
务必先检查现有结构:
# See what exists
find app/assets/stylesheets -name "*.css" | sort
# Check for existing tokens
cat app/assets/stylesheets/_global.css
# Check layer declarations
rg "@layer" app/assets/stylesheets/
# Check for existing components
ls app/assets/stylesheets/components/
遵循现有项目约定 — 保持一致比追求“理想”模式更重要。
第 2 步:设置 CSS 层
在 _global.css(或 application.css)顶部声明一次各层:
/* _global.css — FIRST LINE */
@layer reset, base, components, utilities;
层级优先级(从低到高):
- reset — 浏览器规范化
- base — 元素默认样式(
h1、a、input) - components — UI 组件(
.card、.btn、.badge) - utilities — 覆盖辅助类(
.mt-4、.hidden)— 优先级始终最高
每条 CSS 规则都放在对应的层中:
/* base.css */
@layer base {
body { font-family: var(--font-sans); color: var(--color-ink); }
a { color: var(--color-link); }
}
/* components/cards.css */
@layer components {
.card { background: var(--color-surface); border: 1px solid var(--color-border); }
}
/* utilities.css */
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
}
层为何重要: 没有 @layer,你就得通过嵌套、!important 或加载顺序技巧来对抗特异性。层可以消除所有这些问题。无论特异性如何,utilities 层中的工具类都优先于任何组件规则。
第 3 步:定义设计令牌
所有设计值都以自定义属性的形式存放在 _global.css 的 :root 中:
:root {
/* Enable dark mode detection */
color-scheme: light dark;
/* === Raw Palette (OKLCH for perceptual uniformity) === */
--color-zinc-50: oklch(0.985 0 0);
--color-zinc-100: oklch(0.967 0.001 286.375);
--color-zinc-200: oklch(0.92 0.004 286.32);
--color-zinc-400: oklch(0.707 0.022 261);
--color-zinc-500: oklch(0.552 0.016 285.938);
--color-zinc-700: oklch(0.37 0.013 285.805);
--color-zinc-800: oklch(0.274 0.006 286.033);
--color-zinc-900: oklch(0.21 0.006 285.885);
--color-zinc-950: oklch(0.141 0.005 285.823);
/* === Semantic Color Tokens === */
/* Surfaces */
--color-canvas: light-dark(var(--color-zinc-50), var(--color-zinc-900));
--color-surface: light-dark(white, var(--color-zinc-800));
--color-surface-raised: light-dark(white, var(--color-zinc-700));
--color-surface-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));
/* Text */
--color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
--color-ink-muted: light-dark(var(--color-zinc-500), var(--color-zinc-400));
--color-ink-subtle: light-dark(var(--color-zinc-400), var(--color-zinc-500));
/* Borders */
--color-border: light-dark(var(--color-zinc-200), var(--color-zinc-700));
--color-border-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));
--color-border-strong: light-dark(var(--color-zinc-400), var(--color-zinc-600));
/* Interactive */
--color-primary: light-dark(var(--color-blue-600), var(--color-blue-500));
--color-primary-hover: light-dark(var(--color-blue-700), var(--color-blue-400));
--color-link: light-dark(var(--color-blue-600), var(--color-blue-400));
/* State */
--color-positive: light-dark(var(--color-green-600), var(--color-green-500));
--color-positive-canvas: light-dark(var(--color-green-50), var(--color-green-950));
--color-negative: light-dark(var(--color-red-600), var(--color-red-500));
--color-negative-canvas: light-dark(var(--color-red-50), var(--color-red-950));
/* === Spacing Scale === */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
/* === Typography === */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, "SF Mono", monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* === Radii === */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-full: 9999px;
/* === Transitions === */
--duration-fast: 150ms;
--duration-normal: 250ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
规则:组件绝不使用原始值。始终引用令牌。
/* GOOD */
.card { padding: var(--space-6); background: var(--color-surface); }
/* BAD — hardcoded values */
.card { padding: 1.5rem; background: white; }
第 4 步:编写组件样式
每个组件在 components/ 中都有自己的文件。组件只能使用令牌。
组件结构:
/* components/cards.css */
@layer components {
/* Base */
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
}
/* Structural subcomponents */
.card-header { padding-bottom: var(--space-4); border-bottom: 1px solid var(--color-border-muted); }
.card-body { padding-top: var(--space-4); }
.card-footer { padding-top: var(--space-4); border-top: 1px solid var(--color-border-muted); }
/* Variants */
.card-sm { padding: var(--space-4); }
.card-lg { padding: var(--space-8); }
/* States */
.card-interactive { transition: border-color var(--duration-fast) var(--ease-out); }
.card-interactive:hover { border-color: var(--color-border-strong); }
}
命名约定:
- 基础:
.card、.btn、.badge - 子组件:
.card-header、.card-body - 变体:
.card-sm、.btn-primary - 状态:
.is-loading、.is-active
在以下情况下创建新的组件文件:
- 某个模式使用了 3 次或更多
- 具有相关子类(
.foo、.foo-header、.foo-item) - 具有变体(
.foo-primary、.foo-small)
以下情况保留在页面样式表中:
- 确实仅限于特定页面(只使用一次)
- 简单布局(网格、弹性容器)
第 5 步:使用 light-dark() 实现深色模式
遵循令牌系统即可自动支持深色模式。方法如下:
- 在
:root上设置color-scheme: light dark— 浏览器会遵循操作系统偏好 - 使用
light-dark()定义语义令牌 — 第一个值表示浅色模式,第二个值表示深色模式 - 组件引用语义令牌 — 样式会自动适配
:root {
color-scheme: light dark;
--color-surface: light-dark(white, var(--color-zinc-800));
--color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
}
/* This component supports dark mode with ZERO extra work */
.card {
background: var(--color-surface);
color: var(--color-ink);
}
可选的手动切换:
html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"] { color-scheme: dark; }
function toggleTheme() {
const current = document.documentElement.dataset.theme;
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
}
防止闪烁 — 将以下内容放入 <head>:
<script>
const theme = localStorage.getItem('theme');
if (theme) document.documentElement.dataset.theme = theme;
</script>
深色模式规则:
- 阴影在深色模式下效果不佳 — 优先使用边框
- 使用
color-mix()创建微妙的着色背景 - 状态颜色需要同时提供前景和画布变体
- 绝不使用原始白色/黑色 — 始终使用语义令牌
第 6 步:编写工具类
工具类放在 utilities.css 的 @layer utilities 中。每个工具类只负责一项功能,且不可变:
@layer utilities {
/* Spacing */
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mb-4 { margin-bottom: var(--space-4); }
.p-4 { padding: var(--space-4); }
.gap-4 { gap: var(--space-4); }
/* Text */
.text-sm { font-size: var(--text-sm); }
.text-muted { color: var(--color-ink-muted); }
.font-medium { font-weight: var(--font-medium); }
/* Layout */
.flex { display: flex; }
.grid { display: grid; }
.hidden { display: none; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
}
使用工具类进行偶尔的调整:
<%# Good — utility for spacing adjustment %>
<div class="card mt-6">
<%# Bad — utilities replacing component styles %>
<div class="p-6 bg-white border rounded-lg shadow">
第 7 步:页面样式表应保持精简
页面专属样式应尽量精简——只负责布局:
/* pages/dashboard.css */
@layer components {
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-6);
}
}
页面由现有组件组合而成:
<div class="dashboard-grid">
<div class="card">...</div>
<div class="card">...</div>
<div class="card mt-4">...</div> <%# Component + utility %>
</div>
切勿在页面样式表中设置组件样式:
/* BAD — page overriding component */
.dashboard .card { padding: 2rem; }
/* GOOD — create a variant in the component file */
.card-lg { padding: var(--space-8); }
第 8 步:集成第三方 CSS
集成外部库时,将其变量映射到你的设计令牌:
:root {
--lexxy-color-ink: var(--color-ink);
--lexxy-color-canvas: var(--color-surface);
--lexxy-color-selected: light-dark(oklch(0.92 0.026 254), oklch(0.3 0.05 254));
}
加载顺序很重要——库的 CSS 应在你的覆盖样式之前加载:
<%= stylesheet_link_tag "lexxy" %>
<%= stylesheet_link_tag :app %>
反模式
| 不要 | 应该这样做 |
|---|---|
硬编码颜色(white、#333) | 使用语义化令牌(var(--color-surface)) |
使用 !important | 使用 @layer 控制层叠 |
深层嵌套(.page .section .card .title) | 使用扁平、明确的类名(.card-title) |
使用通用名称(.container、.title) | 使用带前缀的名称(.page-container、.card-title) |
| 在页面 CSS 中设置组件样式 | 创建组件变体 |
| 在 ERB 中使用内联样式 | 使用工具类或组件类 |
在深色模式中使用 box-shadow 表示层级 | 使用边框 |
使用魔法数字(padding: 23px) | 使用间距令牌(var(--space-6)) |
| 使用媒体查询实现深色模式 | 配合 color-scheme 使用 light-dark() |
| 在视图中使用框架专属类 | 使用你自己的语义化类 |
快速参考
命名约定
.component → .card, .btn, .badge
.component-sub → .card-header, .card-body
.component-variant → .card-sm, .btn-primary
.component-state → .card-interactive (or .is-loading for global states)
.page-element → .dashboard-grid, .settings-sidebar
响应式模式
/* Mobile-first with container queries where possible */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-6);
}
}
浏览器支持
light-dark() 需要 Chrome 123+、Firefox 120+、Safari 17.5+。
旧版浏览器的回退方案:
@media (prefers-color-scheme: dark) {
:root {
--color-surface: var(--color-zinc-800);
--color-ink: var(--color-zinc-50);
}
}
新组件检查清单
- 已创建
components/[name].css - 所有规则均包裹在
@layer components { }中 - 所有值均引用设计令牌(没有魔法数字)
- 使用语义化颜色令牌(而非原始调色板值)
- 类名明确具体,不会与其他类冲突
- 子组件遵循
.component-sub命名规则 - 深色模式自动生效(已验证)
- 如有需要,已在
application.css中导入
设计令牌检查清单
- 已将新令牌添加到
_global.css的:root下 - 颜色令牌使用
light-dark()支持两种模式 - 原始调色板值使用 OKLCH
- 间距遵循现有比例
- 令牌名称具有语义性(例如
--color-surface,而非--color-white)
有关详细模式和示例,请参阅 references/ 目录:
references/design-tokens.md— 完整的颜色调色板(OKLCH)、间距、字体排版和命名约定references/components.md— 按钮、卡片、徽章、警告框、表单、表格模式,以及 Rails 集成references/dark-mode.md—light-dark()深入解析、手动切换、阴影、color-mix()references/responsive.md— 移动优先断点、容器查询、流式字体排版、布局模式references/layers.md—@layer层叠控制、反模式、第三方 CSSreferences/utilities.md— 完整的推荐工具类集合