为韩国政府设计系统构建 shadcn/ui 风格组件库
发布: (2025年12月10日 GMT+8 09:16)
5 min read
原文: Dev.to
Source: Dev.to
架构
┌─────────────────────────────────────────────┐
│ CLI (npx hanui add button) │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Component Registry (components.json) │
│ - Component definitions │
│ - Dependencies │
│ - File paths │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Your Project │
│ components/ui/button.tsx ← copied here │
└─────────────────────────────────────────────┘
CLI
CLI 是入口点。它负责:
- init – 为你的项目进行初始化
- add – 将组件复制到你的项目中
- diff – 显示有哪些更改(即将推出)
npx hanui init
# 创建:
# - components.json(配置文件)
# - 复制 variables.css(设计令牌)
# - 更新 tailwind.config.ts(添加预设)
npx hanui add button modal input
# 复制:
# - components/ui/button.tsx
# - components/ui/modal.tsx
# - components/ui/input.tsx
# - 任何依赖(如 cn 工具函数)
组件结构
每个组件遵循统一的模式:
// button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// 基础样式
'inline-flex items-center justify-center rounded font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-krds-primary-base text-white hover:bg-krds-primary-60',
secondary: 'bg-krds-gray-10 text-krds-gray-90 hover:bg-krds-gray-20',
tertiary: 'bg-transparent text-krds-primary-base hover:bg-krds-primary-5',
danger: 'bg-krds-danger-base text-white hover:bg-krds-danger-60',
},
size: {
sm: 'h-8 px-3 text-krds-label-sm',
md: 'h-10 px-4 text-krds-label-md',
lg: 'h-12 px-6 text-krds-label-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
loading?: boolean;
}
export function Button({
className,
variant,
size,
loading,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
aria-busy={loading}
{...props}
>
{loading && /* loading indicator placeholder */}
{children}
</button>
);
}
关键模式
- 使用
cva实现类型安全的变体 - 使用
cn工具函数合并类名 - 在 Tailwind 类中使用 KRDS 令牌(
krds-primary-base、krds-label-md) - 内置可访问性支持(
aria-busy)
设计令牌系统
KRDS 定义了具体的颜色、排版和间距值。HANUI 将它们实现为 CSS 变量并提供 Tailwind 预设。
CSS 变量 (variables.css)
:root {
/* 颜色 */
--krds-color-light-primary-50: #256ef4;
--krds-color-light-primary-60: #0b50d0;
/* 语义令牌 */
--krds-primary-base: var(--krds-color-light-primary-50);
--krds-primary-text: var(--krds-color-light-primary-60);
/* 排版 */
--krds-fs-body-md: 17px;
--krds-fs-heading-lg: 24px;
}
.dark {
/* 暗色模式覆盖 */
--krds-primary-base: var(--krds-color-light-primary-50);
--krds-primary-text: var(--krds-color-light-primary-40);
}
Tailwind 预设 (tailwind.preset.ts)
const hanuiPreset = {
theme: {
extend: {
colors: {
'krds-primary': {
DEFAULT: 'var(--krds-primary-base)',
base: 'var(--krds-primary-base)',
text: 'var(--krds-primary-text)',
50: 'var(--krds-color-light-primary-50)',
60: 'var(--krds-color-light-primary-60)',
},
},
fontSize: {
'krds-body-md': ['17px', { lineHeight: '150%' }],
'krds-heading-lg': [
'var(--krds-fs-heading-lg)',
{ lineHeight: '150%', fontWeight: '700' },
],
},
},
},
};
现在你可以直接使用映射到 KRDS 值的 Tailwind 类:
// 示例用法
<div className="text-krds-primary-base">
符合 KRDS 的文本
</div>
Radix UI 集成
复杂组件(Modal、Select、Tabs)基于 Radix UI 基础组件构建。
为什么选用 Radix?
- 可访问性 – 开箱即符合 WAI‑ARIA 标准
- 无样式 – 让我们可以自行应用 KRDS 样式
- 可组合 – 灵活的复合组件 API
// modal.tsx
import * as Dialog from '@radix-ui/react-dialog';
export function Modal({ children, ...props }) {
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="bg-white p-4 rounded">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export const ModalTitle = Dialog.Title;
export const ModalDescription = Dialog.Description;
export const ModalClose = Dialog.Close;
产生的特性:
- 焦点捕获
- Escape 键处理
- 屏幕阅读器提示
- 滚动锁定
全部免费提供;我们只添加 KRDS 样式。
为什么采用这种方式?
- 完全控制 – 代码归你所有。想修改按钮外观?直接编辑
button.tsx。 - 无运行时依赖 – 组件是复制过去的,而不是从
node_modules导入。你的包只会包含实际使用的代码。 - 原生 Tailwind – 所有内容均使用 Tailwind,符合现代 React 项目惯例。
- 类型安全 – TypeScript +
cva提供变体自动完成和编译时检查。 - 默认可访问 – Radix 基础组件 + ARIA 属性让 WCAG 合规变得轻而易举。
权衡
- 没有自动更新 – 上游组件有改动时,需要手动同步。
- 初始设置较大 – 会向项目中添加更多文件。
- 学习成本 – 需要花时间了解组件结构和 CLI 的使用方式。
对于不可避免需要定制的 KRDS 项目来说,这些权衡是值得的。
入门
npx hanui init
npx hanui add button input modal select tabs
GitHub:
Documentation: