Files
Hayden Bleasel 55fae375a4 Update docs, internationalization (#35)
* Update Geistdocs

* Update geistdocs.tsx

* Run translation script

* Delete toc.tsx

* Update route.ts

* Update route.ts
2025-12-04 23:57:35 -08:00

280 lines
8.2 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 样式
description: 使用 Tailwind 类进行条件化和可组合的样式。
---
现代组件库需要灵活的样式系统,能够在不牺牲开发者体验的情况下处理复杂需求。将 Tailwind CSS 与智能类合并相结合,已成为构建可定制组件的强大模式。
这种方法解决了提供合理默认值与允许完全自定义之间的根本矛盾 —— 这是多年来困扰组件库的挑战。
## 传统样式的问题
传统的 CSS 方法常常导致特异性竞赛、样式冲突和不可预测的覆盖。当你向已经具有 `bg-red-500` 的组件传入 `className="bg-blue-500"` 时,哪个会生效?
如果不正确处理,两者都会生效,结果取决于许多因素 —— CSS 源顺序、类的特异性、打包器的类合并算法等等。
## 智能合并类
`tailwind-merge` 库通过理解 Tailwind 的类结构并智能地解决冲突来解决这个问题。当两个类针对相同的 CSS 属性时,它只保留最后一个。
```tsx title="Without tailwind-merge"
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"
```
```tsx title="With tailwind-merge"
import { twMerge } from 'tailwind-merge';
// bg-blue-500 wins as it comes last
const className = twMerge('bg-red-500', 'bg-blue-500');
// Returns: "bg-blue-500"
```
这适用于所有 Tailwind 工具类:
```tsx
twMerge('px-4 py-2', 'px-8'); // Returns: "py-2 px-8"
twMerge('text-sm', 'text-lg'); // Returns: "text-lg"
twMerge('hover:bg-red-500', 'hover:bg-blue-500'); // Returns: "hover:bg-blue-500"
```
该库也理解 Tailwind 的修饰符系统:
```tsx
// Modifiers are handled correctly
twMerge('hover:bg-red-500 focus:bg-red-500', 'hover:bg-blue-500');
// Returns: "focus:bg-red-500 hover:bg-blue-500"
```
## 条件类
经常需要根据 props 或状态有条件地应用类。`clsx` 库提供了一个简洁的 API 来实现这一点:
```tsx title="Using clsx"
import clsx from 'clsx';
// Basic conditionals
clsx('base', isActive && 'active');
// Returns: "base active" (if isActive is true)
// Object syntax
clsx('base', {
'active': isActive,
'disabled': isDisabled,
});
// Arrays
clsx(['base', isLarge ? 'text-lg' : 'text-sm']);
// Mixed
clsx(
'base',
['array-item'],
{ 'object-conditional': true },
isActive && 'conditional'
);
```
一个常见的模式是将一组默认类与传入的 props 和我们任何自定义逻辑合并:
```tsx title="component.tsx"
const Component = ({ className, ...props }: ComponentProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={cn(
"rounded-lg border bg-white shadow-sm",
isOpen && "bg-blue-500",
className
)}
{...props}
/>
);
};
```
## `cn` 工具函数
由 shadcn/ui 推广的 `cn` 函数结合了 `clsx` 和 `tailwind-merge`,既提供了条件逻辑也提供了智能合并功能:
```tsx title="lib/utils.ts"
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
其强大之处在于顺序 —— 基础样式优先,条件样式其次,用户覆盖最后。这确保了可预测的行为,同时保持完全的自定义能力。
## Class Variance Authority (CVA)
对于具有许多变体的复杂组件,手动管理条件类会变得难以维护。[Class Variance Authority (CVA)](https://cva.style/docs) 提供了一个声明式 API 来定义组件变体。
例如,下面是 shadcn/ui 中 [Button](https://ui.shadcn.com/docs/components/button) 组件的一个摘录:
```tsx title="@/components/ui/button.tsx"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
```
## 最佳实践
### 1. 顺序很重要
始终按以下顺序应用类:
1. 基础样式(始终应用)
2. 变体样式(基于 props)
3. 条件样式(基于状态)
4. 用户覆盖(className prop)
```tsx
className={cn(
'base-styles', // 1. Base
variant && variantStyles, // 2. Variants
isActive && 'active', // 3. Conditionals
className // 4. User overrides
)}
```
### 2. 为你的变体编写文档
使用 TypeScript 和 JSDoc 来记录每个变体的作用:
```tsx
type ButtonProps = {
/**
* The visual style of the button
* @default "primary"
*/
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';
/**
* The size of the button
* @default "md"
*/
size?: 'sm' | 'md' | 'lg';
};
```
### 3. 提取重复模式
如果你发现自己反复编写相同的条件逻辑,就将其抽取出来:
```tsx title="utils/styles.ts"
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
// Use in components
className={cn(focusRing, disabled, className)}
```
## 迁移指南
如果你正在从不同的样式方法迁移,下面是如何调整常见模式:
### 从 CSS Modules
```tsx title="Before - CSS Modules"
import styles from './Button.module.css';
<button className={`${styles.button} ${styles[variant]} ${className}`} />
```
```tsx title="After - cn + Tailwind"
import { cn } from '@/lib/utils';
<button className={cn(
'px-4 py-2 rounded-lg',
variant === 'primary' && 'bg-blue-500 text-white',
className
)} />
```
### 从 styled-components
```tsx title="Before - styled-components"
const Button = styled.button<{ $primary?: boolean }>`
padding: 8px 16px;
background: ${props => props.$primary ? 'blue' : 'gray'};
`;
```
```tsx title="After - cn + Tailwind"
function Button({ primary, className, ...props }) {
return (
<button
className={cn(
'px-4 py-2',
primary ? 'bg-blue-500' : 'bg-gray-500',
className
)}
{...props}
/>
);
}
```
## 性能注意事项
尽管 `clsx` 和 `tailwind-merge` 都经过高度优化,但请记住以下建议:
1. **在组件外部定义变体** - CVA 变体应在组件外部定义,以避免在每次渲染时重新创建。
2. **对复杂计算进行记忆化** - 如果你有开销较大的条件逻辑,考虑使用记忆化:
```tsx
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);
```
3. **对于动态值使用 CSS 变量** - 不要动态生成类名,改用 CSS 变量:
```tsx title="Prefer CSS variables"
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />
```
将 Tailwind CSS、智能类合并和变体 API 结合使用,为组件样式提供了坚实的基础。这种方法可从简单按钮扩展到复杂的设计系统,同时保持可预测性和良好的开发者体验。