为韩国政府设计系统构建 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-basekrds-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 样式。

为什么采用这种方式?

  1. 完全控制 – 代码归你所有。想修改按钮外观?直接编辑 button.tsx
  2. 无运行时依赖 – 组件是复制过去的,而不是从 node_modules 导入。你的包只会包含实际使用的代码。
  3. 原生 Tailwind – 所有内容均使用 Tailwind,符合现代 React 项目惯例。
  4. 类型安全 – TypeScript + cva 提供变体自动完成和编译时检查。
  5. 默认可访问 – Radix 基础组件 + ARIA 属性让 WCAG 合规变得轻而易举。

权衡

  • 没有自动更新 – 上游组件有改动时,需要手动同步。
  • 初始设置较大 – 会向项目中添加更多文件。
  • 学习成本 – 需要花时间了解组件结构和 CLI 的使用方式。

对于不可避免需要定制的 KRDS 项目来说,这些权衡是值得的。

入门

npx hanui init
npx hanui add button input modal select tabs

GitHub:
Documentation:

Back to Blog

相关文章

阅读更多 »

介绍平台元素

作为新产品的一部分,您现在可以使用一套预构建的 UI 块和操作,直接向您的应用程序添加功能。Vercel for Platforms of pr...

React 入门

什么是 React?React 是一个用于构建用户界面的 JavaScript 库。它由 Facebook(Meta)开发,现在是开源的,广泛用于网页开发。