news 2026/9/11 12:17:04

OpenMontage 技能库实战:基于 Tailwind CSS v4 构建可扩展设计系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenMontage 技能库实战:基于 Tailwind CSS v4 构建可扩展设计系统

OpenMontage 技能库实战:基于 Tailwind CSS v4 构建可扩展设计系统

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

本指南以 OpenMontage 仓库中 tailwind-design-system 技能文档 为骨架,系统讲解 Tailwind CSS v4 的 CSS-first 配置范式:从@theme设计令牌、@custom-variant暗黑模式,到 CVA 组件变体、React 19 复合组件、原生动画与 v3→v4 迁移清单。读完本文,你将掌握用 v4 的纯 CSS 方式构建一整套生产级、可访问、响应式的组件库,并能对照仓库内 HyperFrames Tailwind 参考 在固定视口渲染场景下规避 v4 的坑点。

何时使用这套技能

在 OpenMontage 的 Agent 技能体系中,tailwind-design-system属于 skills/INDEX.md 中Design类别下的核心设计技能。按照技能文档的 frontmatter 定义,以下场景应主动调用它:

  • 使用 Tailwind v4 创建组件库;
  • 以 CSS-first 配置实现设计令牌(design tokens)与主题化;
  • 构建响应式、可访问的组件;
  • 在代码库中统一 UI 模式;
  • 从 Tailwind v3 迁移到 v4;
  • 使用原生 CSS 特性搭建暗黑模式。

注意:该技能面向 Tailwind CSS v4(2024+)。对于 v3 项目,应优先参考官方升级指南,再回到本技能落地。

v3 到 v4 的关键变化速查表

技能文档用一张对照表总结了 v4 相对 v3 的范式转移,这是理解全文的基础:

v3 模式v4 模式
tailwind.config.ts@theme写在 CSS 中
@tailwind base/components/utilities@import "tailwindcss"
darkMode: "class"@custom-variant dark (&:where(.dark, .dark *))
theme.extend.colors@theme { --color-*: value }
require("tailwindcss-animate")CSS@keyframes放进@theme+ 用@starting-style做入场动画

一句话概括:配置从 JS 配置文件搬进了 CSS,一切以原生 CSS 变量为中心

CSS-first 配置:从零搭建完整主题

v4 的核心是“配置即 CSS”。以下是一份完整的app.css示例,它同时定义了语义色板(使用 OKLCH 色彩空间)、圆角令牌、动画令牌与暗黑模式覆盖:

/* app.css - Tailwind v4 CSS-first configuration */ @import "tailwindcss"; /* Define your theme with @theme */ @theme { /* Semantic color tokens using OKLCH for better color perception */ --color-background: oklch(100% 0 0); --color-foreground: oklch(14.5% 0.025 264); --color-primary: oklch(14.5% 0.025 264); --color-primary-foreground: oklch(98% 0.01 264); --color-secondary: oklch(96% 0.01 264); --color-secondary-foreground: oklch(14.5% 0.025 264); --color-muted: oklch(96% 0.01 264); --color-muted-foreground: oklch(46% 0.02 264); --color-accent: oklch(96% 0.01 264); --color-accent-foreground: oklch(14.5% 0.025 264); --color-destructive: oklch(53% 0.22 27); --color-destructive-foreground: oklch(98% 0.01 264); --color-border: oklch(91% 0.01 264); --color-ring: oklch(14.5% 0.025 264); --color-card: oklch(100% 0 0); --color-card-foreground: oklch(14.5% 0.025 264); /* Ring offset for focus states */ --color-ring-offset: oklch(100% 0 0); /* Radius tokens */ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; /* Animation tokens - keyframes inside @theme are output when referenced by --animate-* variables */ --animate-fade-in: fade-in 0.2s ease-out; --animate-fade-out: fade-out 0.2s ease-in; --animate-slide-in: slide-in 0.3s ease-out; --animate-slide-out: slide-out 0.3s ease-in; @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } @keyframes slide-in { from { transform: translateY(-0.5rem); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slide-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-0.5rem); opacity: 0; } } } /* Dark mode variant - use @custom-variant for class-based dark mode */ @custom-variant dark (&:where(.dark, .dark *)); /* Dark mode theme overrides */ .dark { --color-background: oklch(14.5% 0.025 264); --color-foreground: oklch(98% 0.01 264); --color-primary: oklch(98% 0.01 264); --color-primary-foreground: oklch(14.5% 0.025 264); --color-secondary: oklch(22% 0.02 264); --color-secondary-foreground: oklch(98% 0.01 264); --color-muted: oklch(22% 0.02 264); --color-muted-foreground: oklch(65% 0.02 264); --color-accent: oklch(22% 0.02 264); --color-accent-foreground: oklch(98% 0.01 264); --color-destructive: oklch(42% 0.15 27); --color-destructive-foreground: oklch(98% 0.01 264); --color-border: oklch(22% 0.02 264); --color-ring: oklch(83% 0.02 264); --color-card: oklch(14.5% 0.025 264); --color-card-foreground: oklch(98% 0.01 264); --color-ring-offset: oklch(14.5% 0.025 264); } /* Base styles */ @layer base { * { @apply border-border; } body { @apply bg-background text-foreground antialiased; } }

关键点解读

  • @import "tailwindcss"替代三条@tailwind指令:v4 默认输出 base、components、utilities 三层,无需再手动拆分。
  • @theme即配置:在@theme中声明的--color-*--radius-*--animate-*变量会自动生成对应的工具类(如--color-primarybg-primarytext-primaryborder-primary)。
  • @keyframes放入@theme:只有被--animate-*变量引用时才会随主题输出,避免产生无用的关键帧代码。
  • @custom-variant dark恢复 class 暗黑模式:选择器&:where(.dark, .dark *)会同时命中.dark元素及其所有后代,实现整棵子树切换。
  • OKLCH 语义色板:技能文档推荐优先使用 OKLCH 而非 HSL,因为其感知均匀性更好;前景/背景、主/辅色均围绕同一个色调(264)搭配,保证对比度协调。

设计令牌的三级层次

技能文档给出了清晰的令牌分层模型,这是“可扩展设计系统”的灵魂:

Brand Tokens (abstract) └── Semantic Tokens (purpose) └── Component Tokens (specific) Example: oklch(45% 0.2 260) → --color-primary → bg-primary
  • Brand Tokens(品牌令牌):最底层抽象,如裸色值oklch(45% 0.2 260),不绑定任何语义;
  • Semantic Tokens(语义令牌):把品牌色映射到用途,如--color-primary
  • Component Tokens(组件令牌):语义令牌落到具体组件用法,如bg-primary

由此带来的硬性规范是:组件里只写bg-primary,绝不写bg-blue-500。这样当品牌色调整时,只需改动@theme一处。

组件架构与 CVA 变体模式

组件设计遵循“基础样式 → 变体 → 尺寸 → 状态 → 覆盖”的分层架构:

Base styles → Variants → Sizes → States → Overrides

Pattern 1:CVA(Class Variance Authority)组件

class-variance-authority实现类型安全的变体组合,是 v4 组件库的标配写法:

// components/ui/button.tsx import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( // Base styles - v4 uses native CSS variables 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'size-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean } // React 19: No forwardRef needed export function Button({ className, variant, size, asChild = false, ref, ...props }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) { const Comp = asChild ? Slot : 'button' return ( <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ) } // Usage <Button variant="destructive" size="lg">Delete</Button> <Button variant="outline">Cancel</Button> <Button asChild><Link href="/home">Home</Link></Button>

要点:基础样式集中描述“排版、圆角、聚焦环、禁用态”,变体只叠加差异;/90/80这类透明度修饰符在 v4 中基于color-mix()实现;size-10是 v4 新增的宽高简写;asChild配合 Radix 的Slot让按钮可以渲染为Link等任意元素。

Pattern 2:React 19 复合组件

React 19 中ref成为普通 prop,不再需要forwardRef。技能文档以 Card 系列组件演示复合组件写法:

// components/ui/card.tsx import { cn } from '@/lib/utils' // React 19: ref is a regular prop, no forwardRef export function Card({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn( 'rounded-lg border border-border bg-card text-card-foreground shadow-sm', className )} {...props} /> ) } export function CardHeader({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} /> ) } export function CardTitle({ className, ref, ...props }: React.HTMLAttributes<HTMLHeadingElement> & { ref?: React.Ref<HTMLHeadingElement> }) { return ( <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} /> ) } export function CardDescription({ className, ref, ...props }: React.HTMLAttributes<HTMLParagraphElement> & { ref?: React.Ref<HTMLParagraphElement> }) { return ( <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> ) } export function CardContent({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> ) } export function CardFooter({ className, ref, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }) { return ( <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} /> ) } // Usage <Card> <CardHeader> <CardTitle>Account</CardTitle> <CardDescription>Manage your account settings</CardDescription> </CardHeader> <CardContent> <form>...</form> </CardContent> <CardFooter> <Button>Save</Button> </CardFooter> </Card>

复合组件把容器、头部、标题、描述、内容、底部按语义拆分成独立导出的子组件,每个子组件通过cn()合并外部传入的className,实现“封闭内部样式、开放覆盖入口”的扩展模型。

Pattern 3:表单组件与校验集成

表单输入框需要同时处理焦点环、错误态与无障碍标注。技能文档展示了Input+Label与 React Hook Form + Zod 的完整组合:

// components/ui/input.tsx import { cn } from '@/lib/utils' export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { error?: string ref?: React.Ref<HTMLInputElement> } export function Input({ className, type, error, ref, ...props }: InputProps) { return ( <div className="relative"> <input type={type} className={cn( 'flex h-10 w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', error && 'border-destructive focus-visible:ring-destructive', className )} ref={ref} aria-invalid={!!error} aria-describedby={error ? `${props.id}-error` : undefined} {...props} /> {error && ( <p id={`${props.id}-error`} className="mt-1 text-sm text-destructive" role="alert" > {error} </p> )} </div> ) } // components/ui/label.tsx import { cva, type VariantProps } from 'class-variance-authority' const labelVariants = cva( 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' ) export function Label({ className, ref, ...props }: React.LabelHTMLAttributes<HTMLLabelElement> & { ref?: React.Ref<HTMLLabelElement> }) { return ( <label ref={ref} className={cn(labelVariants(), className)} {...props} /> ) } // Usage with React Hook Form + Zod import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' const schema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters'), }) function LoginForm() { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), }) return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" {...register('email')} error={errors.email?.message} /> </div> <div className="space-y-2"> <Label htmlFor="password">Password</Label> <Input id="password" type="password" {...register('password')} error={errors.password?.message} /> </div> <Button type="submit" className="w-full">Sign In</Button> </form> ) }

无障碍要点:错误态通过aria-invalid标记输入框,通过aria-describedby把错误消息与输入框关联,错误消息本身使用role="alert";错误出现时边框与焦点环同步切换为destructive语义色。

Pattern 4:响应式网格与容器

把响应式断点抽象成 CVA 变体,可以让调用方用colsgap两个 prop 完成全部布局控制:

// components/ui/grid.tsx import { cn } from '@/lib/utils' import { cva, type VariantProps } from 'class-variance-authority' const gridVariants = cva('grid', { variants: { cols: { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3', 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4', 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5', 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6', }, gap: { none: 'gap-0', sm: 'gap-2', md: 'gap-4', lg: 'gap-6', xl: 'gap-8', }, }, defaultVariants: { cols: 3, gap: 'md', }, }) interface GridProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof gridVariants> {} export function Grid({ className, cols, gap, ...props }: GridProps) { return ( <div className={cn(gridVariants({ cols, gap, className }))} {...props} /> ) } // Container component const containerVariants = cva('mx-auto w-full px-4 sm:px-6 lg:px-8', { variants: { size: { sm: 'max-w-screen-sm', md: 'max-w-screen-md', lg: 'max-w-screen-lg', xl: 'max-w-screen-xl', '2xl': 'max-w-screen-2xl', full: 'max-w-full', }, }, defaultVariants: { size: 'xl', }, }) interface ContainerProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof containerVariants> {} export function Container({ className, size, ...props }: ContainerProps) { return ( <div className={cn(containerVariants({ size, className }))} {...props} /> ) } // Usage <Container> <Grid cols={4} gap="lg"> {products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </Grid> </Container>

注意网格变体的设计意图:移动端始终 1 列起步,sm断点升到 2 列,lg断点再升到满列数,容器负责控制最大宽度与水平内边距。

Pattern 5:v4 原生 CSS 动画

v4 用@starting-styleallow-discrete过渡实现了纯 CSS 的入场/出场动画,可以完全替代tailwindcss-animate插件。技能文档以对话框为例,先定义动画令牌:

/* In your CSS file - native @starting-style for entry animations */ @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); } } /* Native popover animations using @starting-style */ [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); } }

再结合 Radix Dialog 与data-[state=...]属性选择器驱动动画:

// components/ui/dialog.tsx - Using native 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>// providers/ThemeProvider.tsx - Simplified for 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) // Update meta theme-color for mobile browsers 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> ) }

实现要点:system模式通过window.matchMedia('(prefers-color-scheme: dark)')解析;resolvedTheme供需要感知实际明暗的代码使用;切换图标用rotate/scale过渡实现太阳与月亮的交叉淡入淡出;sr-only保证屏幕阅读器可读。

工具函数:cn / focusRing / disabled

组件库的样式合并统一收敛到cn,并把高频状态组合抽成常量:

// lib/utils.ts import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // Focus ring utility export const focusRing = cn( "focus-visible:outline-none focus-visible:ring-2", "focus-visible:ring-ring focus-visible:ring-offset-2", ); // Disabled utility export const disabled = "disabled:pointer-events-none disabled:opacity-50";

cn先用clsx处理条件类名,再用tailwind-merge去重冲突的 Tailwind 类(例如后传入的bg-red-500会正确覆盖前一个bg-blue-500),这也是所有组件能安全接收外部className的前提。

高级 v4 模式

@utility定义自定义工具类

@utility替代 v3 的addUtilities插件机制,直接在 CSS 中声明可被变体(如hover:dark:)组合的自定义工具:

/* Custom utility for decorative lines */ @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; } /* Custom utility for text gradients */ @utility text-gradient { @apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent; }

@theme inline@theme static

/* Use @theme inline when referencing other CSS variables */ @theme inline { --font-sans: var(--font-inter), system-ui; } /* Use @theme static to always generate CSS variables (even when unused) */ @theme static { --color-brand: oklch(65% 0.15 240); } /* Import with theme options */ @import "tailwindcss" theme(static);

inline适用于变量值引用其他 CSS 变量(需在渲染时解析)的场景;static强制所有变量无条件输出,适合需要对外暴露整套令牌(如第三方消费)的情况。

命名空间覆盖

需要从零定制色板时,可先清空默认颜色再定义自己的令牌:

@theme { /* Clear all default colors and define your own */ --color-*: initial; --color-white: #fff; --color-black: #000; --color-primary: oklch(45% 0.2 260); --color-secondary: oklch(65% 0.15 200); /* Clear ALL defaults for a minimal setup */ /* --*: initial; */ }

--color-*: initial只清空颜色族,--*: initial则清空全部默认令牌,实现极简最小化主题。

color-mix()生成半透明色阶

无需逐一手工调色,直接基于主色按比例混合透明色生成 50/100/200 色阶:

@theme { /* Use color-mix() for alpha variants */ --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 ); }

容器查询令牌

v4 支持通过--container-*令牌自定义容器查询断点:

@theme { --container-xs: 20rem; --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; }

v3 到 v4 迁移清单

技能文档给出的迁移清单可逐项勾选执行:

  • 用 CSS 的@theme块替换tailwind.config.ts
  • @tailwind base/components/utilities改为@import "tailwindcss"
  • 把颜色定义迁移到@theme { --color-*: value }
  • @custom-variant dark替换darkMode: "class"
  • @keyframes移入@theme块(保证关键帧随主题输出)
  • 用原生 CSS 动画替换require("tailwindcss-animate")
  • h-10 w-10更新为size-10(新简写工具类)
  • 移除forwardRef(React 19 已将 ref 作为 prop 传递)
  • 考虑改用 OKLCH 颜色以获得更好的感知一致性
  • @utility指令替换自定义插件

最佳实践

Do's

  • 使用@theme—— CSS-first 配置是 v4 的核心范式;
  • 使用 OKLCH 颜色—— 比 HSL 具有更好的感知均匀性;
  • 用 CVA 组合变体—— 类型安全的变体定义;
  • 使用语义令牌—— 写bg-primary而不是bg-blue-500
  • 使用size-*—— 新的w-* h-*简写;
  • 补充无障碍—— ARIA 属性与焦点态不可省略。

Don'ts

  • 不要使用tailwind.config.ts—— 改用 CSS 的@theme
  • 不要使用@tailwind指令—— 改用@import "tailwindcss"
  • 不要使用forwardRef—— React 19 中 ref 直接作为 prop;
  • 不要滥用任意值—— 优先扩展@theme
  • 不要硬编码颜色—— 一律使用语义令牌;
  • 不要忘记暗黑模式—— 两种主题都要测试。

仓库落地:v4 在固定视口渲染场景的护栏

OpenMontage 仓库中的 HyperFrames Tailwind 参考 给出了 v4 在视频合成渲染场景下的硬性规则,与本文的设计系统技能互为补充。核心约束如下:

  • 版本契约:HyperFramesinit --tailwind固定使用@tailwindcss/browser@4.2.4浏览器运行时,保持确定性渲染,不替换为 unpinned 的 CDN;
  • CSS-first 写法:在<style type="text/tailwindcss">内使用@theme@utility定义令牌与自定义工具,禁止 v3 的@tailwind base/components/utilities写法,也不要仅为配色/字体新增tailwind.config.js
  • v3 迁移路径:若从 v3 迁移,需显式用@config "./tailwind.config.js";加载旧配置,v4 不会自动探测 v3 配置文件;
  • 动态类名安全:浏览器运行时只扫描它能看到的类名,切勿在 seek 时动态拼装bg-${color}-500这类类名,应把完整类令牌静态写进 HTML 或data-*变体;
  • 渲染安全护栏:固定视口下禁用md:/lg:断点;关键动画交给 GSAP 等可 seek 的适配器,不用transition-*hover:/focus:等交互变体在渲染期不会触发;v4 中裸border默认为currentColor(v3 是gray-200),必须显式写颜色;留意 v4 工具类改名(shadow-smshadow-xsrounded-smrounded-xsoutline-noneoutline-hiddenflex-shrink-*shrink-*flex-grow-*grow-*)。

这些规则与本文的 CVA 组件模式并不冲突:组件库负责常规交互型 Web 界面,而渲染场景应把布局与视觉交给静态类 +@theme令牌,把时间轴关键帧交给 GSAP—— 这正是 OpenMontage 将tailwind-design-systemhyperframes-*系列技能组合使用时遵循的分工边界。

小结

@theme设计令牌到 CVA 变体组件,从@starting-style原生动画到@utility自定义工具,Tailwind v4 把整套设计系统能力收敛到了纯 CSS 层。本文完整覆盖了 tailwind-design-system 技能文档 的全部模式、迁移清单与最佳实践,并结合 HyperFrames Tailwind 参考 给出了固定视口渲染场景下的落地护栏。按照迁移清单逐项改造,即可让存量 v3 项目平滑进入 CSS-first 时代,并在 Web 组件与视频合成两类场景中复用同一套语义令牌。

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:11:03

项目管理深度解析(三十八)——项目建设团队怎么开展

摘要&#xff1a;本文围绕「项目建设团队怎么开展」这一主题&#xff0c;系统梳理了建设团队的关键动作与落地方法。文章从团队组建入手&#xff0c;介绍了人员获取与角色职责定义&#xff08;RACI 矩阵&#xff09;的方法&#xff1b;随后阐述了协作机制、能力建设、团队激励与…

作者头像 李华
网站建设 2026/9/11 12:09:35

DeerFlow实战:用LLM构建数据分析自动化流水线

先聊个真实感受&#xff1a;这两年我在数据分析上花的时间&#xff0c;大头从来不是写SQL或者调Pandas&#xff0c;而是浪费在“拿到一份不知道底细的数据后&#xff0c;先得做一堆探索性分析&#xff0c;才能决定下一步怎么写”。这种活极其重复&#xff0c;每次都要清洗、看分…

作者头像 李华
网站建设 2026/9/11 12:08:52

Android ViewPager开发指南:从基础到高级应用

1. ViewPager基础概念与核心价值 ViewPager作为Android官方提供的页面滑动容器&#xff0c;在移动端开发中扮演着重要角色。它的核心功能是实现左右滑动的页面切换效果&#xff0c;这种交互模式已经成为现代App的基础体验标准。我在实际项目中最常遇到的应用场景包括&#xff1…

作者头像 李华