TailwindCSS组件模式构建Skill tailwind-components

这个技能专注于使用Tailwind CSS创建和维护可重用的前端组件模式,涵盖组件提取策略、@apply指令的合理使用、设计系统集成以及最佳实践如Class Variance Authority和条件类管理。关键词:Tailwind CSS, 组件模式, 前端开发, 可重用组件, 设计系统, 实用程序优先。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: tailwind-components 用户可调用: false 描述: 用于使用Tailwind CSS构建可重用组件模式。涵盖组件提取、@apply指令和可组合设计模式。 允许工具:

  • 读取
  • 写入
  • 编辑
  • Bash
  • Grep
  • Glob

Tailwind CSS - 组件模式

虽然Tailwind是实用程序优先的,但你通常希望将常见模式提取为可重用组件。本技能涵盖了使用Tailwind构建可维护组件架构的策略。

关键概念

组件提取策略

有几种方法可以使用Tailwind创建可重用组件:

  1. 模板/组件抽象(推荐)
  2. CSS @apply 指令(谨慎使用)
  3. JavaScript/TypeScript组件类
  4. Tailwind插件

模板组件抽象

最可维护的方法是在模板级别提取组件:

// Button.tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'outline'
  size?: 'sm' | 'md' | 'lg'
  children: React.ReactNode
  onClick?: () => void
}

export function Button({
  variant = 'primary',
  size = 'md',
  children,
  onClick
}: ButtonProps) {
  const baseClasses = 'font-semibold rounded transition-colors focus:ring-2 focus:ring-offset-2'

  const variantClasses = {
    primary: 'bg-blue-500 hover:bg-blue-600 text-white focus:ring-blue-300',
    secondary: 'bg-gray-500 hover:bg-gray-600 text-white focus:ring-gray-300',
    outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50 focus:ring-blue-300',
  }

  const sizeClasses = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  }

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

最佳实践

1. 使用Class Variance Authority (CVA)

对于复杂的组件变体,使用cva以获得更好的类型安全性:

import { cva, type VariantProps } from 'class-variance-authority'

const button = cva(
  // 基础类
  'font-semibold rounded transition-colors focus:ring-2',
  {
    variants: {
      intent: {
        primary: 'bg-blue-500 hover:bg-blue-600 text-white',
        secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
        danger: 'bg-red-500 hover:bg-red-600 text-white',
      },
      size: {
        small: 'text-sm px-3 py-1.5',
        medium: 'text-base px-4 py-2',
        large: 'text-lg px-6 py-3',
      },
      disabled: {
        true: 'opacity-50 cursor-not-allowed',
      },
    },
    compoundVariants: [
      {
        intent: 'primary',
        size: 'medium',
        className: 'uppercase',
      },
    ],
    defaultVariants: {
      intent: 'primary',
      size: 'medium',
    },
  }
)

interface ButtonProps extends VariantProps<typeof button> {
  children: React.ReactNode
}

export function Button({ intent, size, disabled, children }: ButtonProps) {
  return (
    <button className={button({ intent, size, disabled })}>
      {children}
    </button>
  )
}

2. 使用clsx/cn进行条件类

高效组合多个类字符串:

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

// 用法
<div className={cn(
  'base-classes',
  isActive && 'active-classes',
  isDisabled && 'disabled-classes',
  className // 允许属性覆盖
)} />

3. 创建复合组件

构建灵活的组件API:

// Card.tsx
interface CardProps {
  children: React.ReactNode
  className?: string
}

export function Card({ children, className }: CardProps) {
  return (
    <div className={cn(
      'bg-white rounded-lg shadow-md overflow-hidden',
      className
    )}>
      {children}
    </div>
  )
}

Card.Header = function CardHeader({ children, className }: CardProps) {
  return (
    <div className={cn('px-6 py-4 border-b border-gray-200', className)}>
      {children}
    </div>
  )
}

Card.Body = function CardBody({ children, className }: CardProps) {
  return (
    <div className={cn('px-6 py-4', className)}>
      {children}
    </div>
  )
}

Card.Footer = function CardFooter({ children, className }: CardProps) {
  return (
    <div className={cn('px-6 py-4 bg-gray-50 border-t border-gray-200', className)}>
      {children}
    </div>
  )
}

// 用法
<Card>
  <Card.Header>
    <h2 className="text-xl font-bold">标题</h2>
  </Card.Header>
  <Card.Body>
    <p>内容放在这里</p>
  </Card.Body>
  <Card.Footer>
    <button>操作</button>
  </Card.Footer>
</Card>

4. 谨慎使用@apply

仅对真正可重用的基础样式使用@apply

/* globals.css */
@layer components {
  .btn {
    @apply px-4 py-2 rounded font-semibold transition-colors;
  }

  .btn-primary {
    @apply bg-blue-500 hover:bg-blue-600 text-white;
  }

  .btn-secondary {
    @apply bg-gray-500 hover:bg-gray-600 text-white;
  }
}

注意: 优先使用组件抽象而非@apply,以保持Tailwind的实用程序优先优势。

5. 设计系统集成

创建集中式设计系统:

// design-system/index.ts
export const colors = {
  primary: 'blue-500',
  secondary: 'gray-500',
  success: 'green-500',
  danger: 'red-500',
  warning: 'yellow-500',
}

export const spacing = {
  xs: '1',
  sm: '2',
  md: '4',
  lg: '6',
  xl: '8',
}

export const typography = {
  heading: 'font-bold tracking-tight',
  body: 'font-normal leading-relaxed',
  caption: 'text-sm text-gray-600',
}

// 在组件中使用
import { colors, spacing } from '@/design-system'

<button className={`bg-${colors.primary} p-${spacing.md}`}>
  点击我
</button>

示例

表单输入组件

import { forwardRef } from 'react'
import { cn } from '@/lib/utils'

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string
  error?: string
  helperText?: string
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, helperText, className, ...props }, ref) => {
    return (
      <div className="w-full">
        {label && (
          <label className="block text-sm font-medium text-gray-700 mb-1">
            {label}
          </label>
        )}
        <input
          ref={ref}
          className={cn(
            'w-full px-3 py-2 border rounded-md',
            'focus:outline-none focus:ring-2 focus:ring-offset-0',
            'transition-colors',
            error
              ? 'border-red-500 focus:ring-red-300'
              : 'border-gray-300 focus:ring-blue-300',
            props.disabled && 'bg-gray-100 cursor-not-allowed',
            className
          )}
          {...props}
        />
        {error && (
          <p className="mt-1 text-sm text-red-600">{error}</p>
        )}
        {helperText && !error && (
          <p className="mt-1 text-sm text-gray-500">{helperText}</p>
        )}
      </div>
    )
  }
)

Input.displayName = 'Input'

模态组件

import { useEffect } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/lib/utils'

interface ModalProps {
  isOpen: boolean
  onClose: () => void
  children: React.ReactNode
  title?: string
  size?: 'sm' | 'md' | 'lg' | 'xl'
}

export function Modal({
  isOpen,
  onClose,
  children,
  title,
  size = 'md'
}: ModalProps) {
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden'
    } else {
      document.body.style.overflow = 'unset'
    }
    return () => {
      document.body.style.overflow = 'unset'
    }
  }, [isOpen])

  if (!isOpen) return null

  const sizeClasses = {
    sm: 'max-w-md',
    md: 'max-w-lg',
    lg: 'max-w-2xl',
    xl: 'max-w-4xl',
  }

  return createPortal(
    <div className="fixed inset-0 z-50 overflow-y-auto">
      {/* 背景层 */}
      <div
        className="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
        onClick={onClose}
      />

      {/* 模态 */}
      <div className="flex min-h-screen items-center justify-center p-4">
        <div
          className={cn(
            'relative bg-white rounded-lg shadow-xl',
            'w-full transform transition-all',
            sizeClasses[size]
          )}
        >
          {/* 头部 */}
          {title && (
            <div className="px-6 py-4 border-b border-gray-200">
              <h2 className="text-xl font-semibold">{title}</h2>
              <button
                onClick={onClose}
                className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
              >
                <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                </svg>
              </button>
            </div>
          )}

          {/* 内容 */}
          <div className="px-6 py-4">
            {children}
          </div>
        </div>
      </div>
    </div>,
    document.body
  )
}

下拉菜单组件

import { useState, useRef, useEffect } from 'react'
import { cn } from '@/lib/utils'

interface DropdownProps {
  trigger: React.ReactNode
  children: React.ReactNode
  align?: 'left' | 'right'
}

export function Dropdown({ trigger, children, align = 'left' }: DropdownProps) {
  const [isOpen, setIsOpen] = useState(false)
  const dropdownRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
        setIsOpen(false)
      }
    }

    document.addEventListener('mousedown', handleClickOutside)
    return () => document.removeEventListener('mousedown', handleClickOutside)
  }, [])

  return (
    <div className="relative inline-block" ref={dropdownRef}>
      <div onClick={() => setIsOpen(!isOpen)}>
        {trigger}
      </div>

      {isOpen && (
        <div className={cn(
          'absolute z-50 mt-2 w-56',
          'bg-white rounded-md shadow-lg',
          'border border-gray-200',
          'py-1',
          align === 'right' ? 'right-0' : 'left-0'
        )}>
          {children}
        </div>
      )}
    </div>
  )
}

Dropdown.Item = function DropdownItem({
  children,
  onClick
}: {
  children: React.ReactNode
  onClick?: () => void
}) {
  return (
    <button
      className={cn(
        'w-full text-left px-4 py-2',
        'hover:bg-gray-100',
        'transition-colors'
      )}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

常见模式

加载状态

export function LoadingButton({
  isLoading,
  children,
  ...props
}: ButtonProps & { isLoading?: boolean }) {
  return (
    <button
      className={cn(
        'relative',
        isLoading && 'cursor-not-allowed opacity-70'
      )}
      disabled={isLoading}
      {...props}
    >
      {isLoading && (
        <span className="absolute inset-0 flex items-center justify-center">
          <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
            <circle
              className="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              strokeWidth="4"
              fill="none"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
            />
          </svg>
        </span>
      )}
      <span className={cn(isLoading && 'invisible')}>
        {children}
      </span>
    </button>
  )
}

徽章组件

const badge = cva(
  'inline-flex items-center rounded-full font-medium',
  {
    variants: {
      variant: {
        default: 'bg-gray-100 text-gray-800',
        primary: 'bg-blue-100 text-blue-800',
        success: 'bg-green-100 text-green-800',
        warning: 'bg-yellow-100 text-yellow-800',
        danger: 'bg-red-100 text-red-800',
      },
      size: {
        sm: 'px-2 py-0.5 text-xs',
        md: 'px-2.5 py-1 text-sm',
        lg: 'px-3 py-1.5 text-base',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
)

export function Badge({ variant, size, children }: VariantProps<typeof badge> & { children: React.ReactNode }) {
  return (
    <span className={badge({ variant, size })}>
      {children}
    </span>
  )
}

反模式

❌ 不要过度使用@apply

/* 不好:重新创建Bootstrap */
.btn {
  @apply px-4 py-2 rounded font-semibold transition-colors focus:ring-2;
}
.btn-primary {
  @apply bg-blue-500 hover:bg-blue-600 text-white focus:ring-blue-300;
}
.btn-secondary {
  @apply bg-gray-500 hover:bg-gray-600 text-white focus:ring-gray-300;
}
.btn-sm { @apply px-3 py-1 text-sm; }
.btn-lg { @apply px-6 py-3 text-lg; }

/* 好:使用组件抽象替代 */

❌ 不要创建过于通用的组件

// 不好:太灵活,失去Tailwind优势
<Box padding="4" margin="2" bg="blue" />

// 好:语义化组件
<Card className="p-4 m-2 bg-blue-500" />

❌ 不要忽略属性覆盖

// 不好:无法覆盖样式
export function Button({ children }: { children: React.ReactNode }) {
  return <button className="bg-blue-500 px-4 py-2">{children}</button>
}

// 好:接受className属性
export function Button({ children, className }: { children: React.ReactNode; className?: string }) {
  return (
    <button className={cn('bg-blue-500 px-4 py-2', className)}>
      {children}
    </button>
  )
}

相关技能

  • tailwind-utility-classes:有效使用Tailwind的实用程序类
  • tailwind-configuration:自定义Tailwind配置和主题
  • tailwind-responsive-design:构建响应式组件模式