8.1 KiB
8.1 KiB
Tailwind Design System: Advanced Patterns
高级 Tailwind CSS v4 模式,包括动画、暗色模式主题化、自定义工具类、主题修饰符、命名空间覆盖,以及 v3 到 v4 的迁移清单。
Pattern 5: Native CSS Animations (v4)
/* 在你的 CSS 文件中 —— 用于入场动画的原生 @starting-style */
@theme {
--animate-dialog-in: dialog-fade-in 0.2s ease-out;
--animate-dialog-out: dialog-fade-out 0.15s ease-in;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: scale(0.95) translateY(-0.5rem);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes dialog-fade-out {
from {
opacity: 1;
transform: scale(1) translateY(0);
}
to {
opacity: 0;
transform: scale(0.95) translateY(-0.5rem);
}
}
/* 使用 @starting-style 实现原生 popover 动画 */
[popover] {
transition:
opacity 0.2s,
transform 0.2s,
display 0.2s allow-discrete;
opacity: 0;
transform: scale(0.95);
}
[popover]:popover-open {
opacity: 1;
transform: scale(1);
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
// components/ui/dialog.tsx - 使用原生 popover API
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { cn } from '@/lib/utils'
const DialogPortal = DialogPrimitive.Portal
export function DialogOverlay({
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
ref?: React.Ref<HTMLDivElement>
}) {
return (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80',
'data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out',
className
)}
{...props}
/>
)
}
export function DialogContent({
className,
children,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
ref?: React.Ref<HTMLDivElement>
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-border bg-background p-6 shadow-lg sm:rounded-lg',
'data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out',
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
)
}
Pattern 6: Dark Mode with CSS (v4)
// providers/ThemeProvider.tsx - 为 v4 简化
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
resolvedTheme: 'dark' | 'light'
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme',
}: {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}) {
const [theme, setTheme] = useState<Theme>(defaultTheme)
const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light')
useEffect(() => {
const stored = localStorage.getItem(storageKey) as Theme | null
if (stored) setTheme(stored)
}, [storageKey])
useEffect(() => {
const root = document.documentElement
root.classList.remove('light', 'dark')
const resolved = theme === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme
root.classList.add(resolved)
setResolvedTheme(resolved)
// 更新移动端浏览器的 meta theme-color
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', resolved === 'dark' ? '#09090b' : '#ffffff')
}
}, [theme])
return (
<ThemeContext.Provider value={{
theme,
setTheme: (newTheme) => {
localStorage.setItem(storageKey, newTheme)
setTheme(newTheme)
},
resolvedTheme,
}}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme must be used within ThemeProvider')
return context
}
// components/ThemeToggle.tsx
import { Moon, Sun } from 'lucide-react'
import { useTheme } from '@/providers/ThemeProvider'
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
>
<Sun className="size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
高级 v4 模式
使用 @utility 定义自定义工具类
定义可复用的自定义工具类:
/* 装饰线的自定义工具类 */
@utility line-t {
@apply relative before:absolute before:top-0 before:-left-[100vw] before:h-px before:w-[200vw] before:bg-gray-950/5 dark:before:bg-white/10;
}
/* 文本渐变色的自定义工具类 */
@utility text-gradient {
@apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent;
}
主题修饰符
/* 在引用其他 CSS 变量时使用 @theme inline */
@theme inline {
--font-sans: var(--font-inter), system-ui;
}
/* 使用 @theme static 始终生成 CSS 变量(即使未被使用时也生成) */
@theme static {
--color-brand: oklch(65% 0.15 240);
}
/* 带主题选项的导入 */
@import "tailwindcss" theme(static);
命名空间覆盖
@theme {
/* 清除所有默认颜色并定义你自己的颜色 */
--color-*: initial;
--color-white: #fff;
--color-black: #000;
--color-primary: oklch(45% 0.2 260);
--color-secondary: oklch(65% 0.15 200);
/* 为最小化设置清除所有默认值 */
/* --*: initial; */
}
半透明颜色变体
@theme {
/* 使用 color-mix() 创建 alpha 变体 */
--color-primary-50: color-mix(in oklab, var(--color-primary) 5%, transparent);
--color-primary-100: color-mix(
in oklab,
var(--color-primary) 10%,
transparent
);
--color-primary-200: color-mix(
in oklab,
var(--color-primary) 20%,
transparent
);
}
容器查询
@theme {
--container-xs: 20rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-lg: 32rem;
}
v3 到 v4 迁移清单
- 将
tailwind.config.ts替换为 CSS@theme块 - 将
@tailwind base/components/utilities改为@import "tailwindcss" - 将颜色定义移至
@theme { --color-*: value } - 将
darkMode: "class"替换为@custom-variant dark - 将
@keyframes移入@theme块内部(确保 keyframes 随主题一起输出) - 将
require("tailwindcss-animate")替换为原生 CSS 动画 - 将
h-10 w-10更新为size-10(新工具类) - 移除
forwardRef(React 19 将 ref 作为 prop 传递) - 考虑使用 OKLCH 颜色以获得更好的颜色感知
- 将自定义插件替换为
@utility指令
最佳实践
推荐做法
- 使用
@theme块 —— CSS 优先配置是 v4 的核心模式 - 使用 OKLCH 颜色 —— 比 HSL 具有更好的感知均匀性
- 结合 CVA 进行组合 —— 类型安全的变体
- 使用语义化令牌 —— 使用
bg-primary而非bg-blue-500 - 使用
size-*——w-* h-*的新简写形式 - 增加无障碍支持 —— ARIA 属性、焦点状态
避免做法
- 不要使用
tailwind.config.ts—— 改用 CSS@theme - 不要使用
@tailwind指令 —— 改用@import "tailwindcss" - 不要使用
forwardRef—— React 19 将 ref 作为 prop 传递 - 不要使用任意值 —— 改用扩展
@theme - 不要硬编码颜色 —— 使用语义化令牌
- 不要忘记暗色模式 —— 测试两种主题