mcpField
T
tailwindcss-advanced-components

tailwindcss-advanced-components

github.com/josiahsiegel设计与 UI59248 次安装19 周前更新
代码仓库

关于 tailwindcss-advanced-components

面向使用 Tailwind CSS 构建 React 界面的开发者,介绍高级组件模式及其实现示例,涵盖变体、复合组件、状态属性和无头组件集成。

主要功能

  1. 01使用 CVA 定义组件变体
  2. 02基于上下文的复合组件
  3. 03通过数据属性管理样式状态
  4. 04Headless UI 集成示例
  5. 05Radix UI 集成示例
  6. 06组件变体测试示例

适用场景

  1. 01为按钮定义多种样式变体
  2. 02组合共享状态的界面组件
  3. 03依据数据属性控制组件样式
  4. 04将无头组件库与 Tailwind CSS 配合使用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill tailwindcss-advanced-components

安装后在 Claude Code 中输入/tailwindcss-advanced-components确认已加载。

详细介绍

Tailwind CSS 高级组件模式

使用 CVA 实现组件变体

集成 Class Variance Authority
npm install class-variance-authority
// components/Button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-brand-500 text-white hover:bg-brand-600 focus-visible:ring-brand-500',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500',
        outline: 'border border-gray-300 bg-transparent hover:bg-gray-100 focus-visible:ring-gray-500',
        ghost: 'hover:bg-gray-100 focus-visible:ring-gray-500',
        destructive: 'bg-red-500 text-white hover:bg-red-600 focus-visible:ring-red-500',
        link: 'text-brand-500 underline-offset-4 hover:underline',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
        xl: 'h-14 px-8 text-lg',
        icon: 'h-10 w-10',
      },
      fullWidth: {
        true: 'w-full',
      },
    },
    compoundVariants: [
      {
        variant: 'outline',
        size: 'sm',
        className: 'border',
      },
      {
        variant: 'outline',
        size: ['md', 'lg', 'xl'],
        className: 'border-2',
      },
    ],
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

export function Button({
  className,
  variant,
  size,
  fullWidth,
  asChild = false,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size, fullWidth, className }))}
      {...props}
    />
  );
}
用法
<Button>Default</Button>
<Button variant="secondary" size="lg">Large Secondary</Button>
<Button variant="destructive" fullWidth>Delete</Button>
<Button variant="ghost" size="icon"><IconMenu /></Button>

复合组件模式

基于上下文的组件系统
// components/Card/index.tsx
import { createContext, useContext, forwardRef } from 'react';
import { cn } from '@/lib/utils';

// Context for shared state
const CardContext = createContext<{ variant?: 'default' | 'elevated' | 'outline' }>({});

// Root component
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  variant?: 'default' | 'elevated' | 'outline';
}

const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ className, variant = 'default', children, ...props }, ref) => {
    const variants = {
      default: 'bg-white border border-gray-200',
      elevated: 'bg-white shadow-lg',
      outline: 'bg-transparent border-2 border-gray-300',
    };

    return (
      <CardContext.Provider value={{ variant }}>
        <div
          ref={ref}
          className={cn(
            'rounded-xl',
            variants[variant],
            className
          )}
          {...props}
        >
          {children}
        </div>
      </CardContext.Provider>
    );
  }
);

// Sub-components
const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn('flex flex-col gap-1.5 p-6 pb-0', className)}
      {...props}
    />
  )
);

const CardTitle = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
  ({ className, ...props }, ref) => (
    <h3
      ref={ref}
      className={cn('text-xl font-semibold leading-none tracking-tight', className)}
      {...props}
    />
  )
);

const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
  ({ className, ...props }, ref) => (
    <p
      ref={ref}
      className={cn('text-sm text-gray-500', className)}
      {...props}
    />
  )
);

const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('p-6', className)} {...props} />
  )
);

const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn('flex items-center p-6 pt-0', className)}
      {...props}
    />
  )
);

// Named exports
Card.displayName = 'Card';
CardHeader.displayName = 'CardHeader';
CardTitle.displayName = 'CardTitle';
CardDescription.displayName = 'CardDescription';
CardContent.displayName = 'CardContent';
CardFooter.displayName = 'CardFooter';

export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
用法
<Card variant="elevated">
  <CardHeader>
    <CardTitle>Account Settings</CardTitle>
    <CardDescription>Manage your account preferences</CardDescription>
  </CardHeader>
  <CardContent>
    <form>...</form>
  </CardContent>
  <CardFooter className="justify-between">
    <Button variant="ghost">Cancel</Button>
    <Button>Save Changes</Button>
  </CardFooter>
</Card>

数据属性变体

基于 CSS 的状态管理
/* Define data attribute variants */
@custom-variant data-state-open (&[data-state="open"]);
@custom-variant data-state-closed (&[data-state="closed"]);
@custom-variant data-side-top (&[data-side="top"]);
@custom-variant data-side-bottom (&[data-side="bottom"]);
@custom-variant data-side-left (&[data-side="left"]);
@custom-variant data-side-right (&[data-side="right"]);
@custom-variant data-highlighted (&[data-highlighted]);
@custom-variant data-disabled (&[data-disabled]);
// Dropdown component using data attributes
function DropdownContent({ children, ...props }) {
  return (
    <div
      data-state={isOpen ? 'open' : 'closed'}
      data-side={side}
      className="
        absolute z-50 min-w-[8rem] overflow-hidden rounded-md
        border border-gray-200 bg-white p-1 shadow-lg

        data-state-open:animate-in
        data-state-open:fade-in-0
        data-state-open:zoom-in-95

        data-state-closed:animate-out
        data-state-closed:fade-out-0
        data-state-closed:zoom-out-95

        data-side-top:slide-in-from-bottom-2
        data-side-bottom:slide-in-from-top-2
        data-side-left:slide-in-from-right-2
        data-side-right:slide-in-from-left-2
      "
      {...props}
    >
      {children}
    </div>
  );
}

function DropdownItem({ children, disabled, ...props }) {
  return (
    <div
      data-highlighted={isHighlighted || undefined}
      data-disabled={disabled || undefined}
      className="
        relative flex cursor-pointer select-none items-center
        rounded-sm px-2 py-1.5 text-sm outline-none

        data-highlighted:bg-gray-100
        data-disabled:pointer-events-none
        data-disabled:opacity-50
      "
      {...props}
    >
      {children}
    </div>
  );
}

Group 和 Peer 模式

复杂的状态传递

<div class="group relative overflow-hidden rounded-xl">
  <img
    src="image.jpg"
    class="transition-transform duration-300 group-hover:scale-110"
  />
  <div class="
    absolute inset-0 bg-gradient-to-t from-black/80 to-transparent
    opacity-0 transition-opacity group-hover:opacity-100
  ">
    <div class="
      absolute bottom-0 left-0 right-0 p-6
      translate-y-4 transition-transform group-hover:translate-y-0
    ">
      <h3 class="text-xl font-bold text-white">Title</h3>
      <p class="text-gray-200">Description</p>
    </div>
  </div>
</div>


<div class="group/card">
  <div class="group/header">
    <button class="group-hover/header:text-blue-500">
      Header Action
    </button>
  </div>
  <div class="group-hover/card:bg-gray-50">
    Card content
  </div>
</div>


<div class="relative">
  <input
    type="email"
    class="peer w-full border rounded-lg px-4 py-2 placeholder-transparent"
    placeholder="Email"
  />
  <label class="
    absolute left-4 top-2 text-gray-500
    transition-all
    peer-placeholder-shown:top-2 peer-placeholder-shown:text-base
    peer-focus:-top-6 peer-focus:text-sm peer-focus:text-blue-500
    peer-[:not(:placeholder-shown)]:-top-6 peer-[:not(:placeholder-shown)]:text-sm
  ">
    Email address
  </label>
</div>


<div>
  <input
    type="email"
    class="peer"
    required
  />
  <p class="hidden text-red-500 peer-invalid:block">
    Please enter a valid email
  </p>
</div>

使用 @apply 的插槽模式

可复用组件插槽
@layer components {
  /* Base dialog structure */
  .dialog {
    @apply fixed inset-0 z-50 flex items-center justify-center;
  }

  .dialog-overlay {
    @apply fixed inset-0 bg-black/50 backdrop-blur-sm;
    @apply data-state-open:animate-in data-state-open:fade-in-0;
    @apply data-state-closed:animate-out data-state-closed:fade-out-0;
  }

  .dialog-content {
    @apply relative z-50 w-full max-w-lg rounded-xl bg-white p-6 shadow-xl;
    @apply data-state-open:animate-in data-state-open:fade-in-0 data-state-open:zoom-in-95;
    @apply data-state-closed:animate-out data-state-closed:fade-out-0 data-state-closed:zoom-out-95;
  }

  .dialog-header {
    @apply flex flex-col gap-1.5 text-center sm:text-left;
  }

  .dialog-title {
    @apply text-lg font-semibold leading-none tracking-tight;
  }

  .dialog-description {
    @apply text-sm text-gray-500;
  }

  .dialog-footer {
    @apply flex flex-col-reverse gap-2 sm:flex-row sm:justify-end;
  }

  .dialog-close {
    @apply absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100;
    @apply focus:outline-none focus:ring-2 focus:ring-offset-2;
  }
}

多态组件

“as” 属性模式
import { forwardRef, ElementType, ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils';

type PolymorphicRef<C extends ElementType> = ComponentPropsWithoutRef<C>['ref'];

type PolymorphicComponentProps<C extends ElementType, Props = {}> = Props & {
  as?: C;
  className?: string;
  children?: React.ReactNode;
} & Omit<ComponentPropsWithoutRef<C>, 'as' | 'className' | keyof Props>;

// Text component that can be any element
interface TextProps {
  size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl';
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
  color?: 'default' | 'muted' | 'accent';
}

type TextComponent = <C extends ElementType = 'span'>(
  props: PolymorphicComponentProps<C, TextProps> & { ref?: PolymorphicRef<C> }
) => React.ReactElement | null;

export const Text: TextComponent = forwardRef(
  <C extends ElementType = 'span'>(
    {
      as,
      size = 'base',
      weight = 'normal',
      color = 'default',
      className,
      children,
      ...props
    }: PolymorphicComponentProps<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    const Component = as || 'span';

    const sizes = {
      xs: 'text-xs',
      sm: 'text-sm',
      base: 'text-base',
      lg: 'text-lg',
      xl: 'text-xl',
      '2xl': 'text-2xl',
    };

    const weights = {
      normal: 'font-normal',
      medium: 'font-medium',
      semibold: 'font-semibold',
      bold: 'font-bold',
    };

    const colors = {
      default: 'text-gray-900',
      muted: 'text-gray-500',
      accent: 'text-brand-500',
    };

    return (
      <Component
        ref={ref}
        className={cn(sizes[size], weights[weight], colors[color], className)}
        {...props}
      >
        {children}
      </Component>
    );
  }
);
用法
<Text>Default span</Text>
<Text as="p" size="lg" color="muted">Large muted paragraph</Text>
<Text as="h1" size="2xl" weight="bold">Bold heading</Text>
<Text as="a" href="/link" color="accent">Accent link</Text>

无头组件集成

使用 Tailwind 的 Headless UI
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';

function Modal({ isOpen, onClose, title, children }) {
  return (
    <Transition appear show={isOpen} as={Fragment}>
      <Dialog as="div" className="relative z-50" onClose={onClose}>
        {/* Backdrop */}
        <Transition.Child
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black/50 backdrop-blur-sm" />
        </Transition.Child>

        <div className="fixed inset-0 overflow-y-auto">
          <div className="flex min-h-full items-center justify-center p-4">
            <Transition.Child
              as={Fragment}
              enter="ease-out duration-300"
              enterFrom="opacity-0 scale-95"
              enterTo="opacity-100 scale-100"
              leave="ease-in duration-200"
              leaveFrom="opacity-100 scale-100"
              leaveTo="opacity-0 scale-95"
            >
              <Dialog.Panel className="
                w-full max-w-md transform overflow-hidden rounded-2xl
                bg-white p-6 text-left align-middle shadow-xl transition-all
              ">
                <Dialog.Title className="text-lg font-medium leading-6 text-gray-900">
                  {title}
                </Dialog.Title>
                <div className="mt-2">
                  {children}
                </div>
              </Dialog.Panel>
            </Transition.Child>
          </div>
        </div>
      </Dialog>
    </Transition>
  );
}
使用 Tailwind 的 Radix UI
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';

function Dropdown() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger className="
        inline-flex items-center justify-center rounded-md
        bg-white px-4 py-2 text-sm font-medium
        border border-gray-300 hover:bg-gray-50
        focus:outline-none focus:ring-2 focus:ring-brand-500
      ">
        Options
        <ChevronDownIcon className="ml-2 h-4 w-4" />
      </DropdownMenu.Trigger>

      <DropdownMenu.Portal>
        <DropdownMenu.Content
          className="
            min-w-[200px] rounded-md bg-white p-1 shadow-lg
            border border-gray-200
            animate-in fade-in-0 zoom-in-95
            data-[side=bottom]:slide-in-from-top-2
            data-[side=top]:slide-in-from-bottom-2
          "
          sideOffset={5}
        >
          <DropdownMenu.Item className="
            relative flex cursor-pointer select-none items-center
            rounded-sm px-2 py-2 text-sm outline-none
            data-[highlighted]:bg-gray-100
          ">
            Profile
          </DropdownMenu.Item>

          <DropdownMenu.Separator className="my-1 h-px bg-gray-200" />

          <DropdownMenu.Item className="
            relative flex cursor-pointer select-none items-center
            rounded-sm px-2 py-2 text-sm text-red-600 outline-none
            data-[highlighted]:bg-red-50
          ">
            Delete
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

动画模式

交错动画
function StaggeredList({ items }) {
  return (
    <ul className="space-y-2">
      {items.map((item, index) => (
        <li
          key={item.id}
          className="animate-in fade-in-0 slide-in-from-left-4"
          style={{ animationDelay: `${index * 100}ms` }}
        >
          {item.content}
        </li>
      ))}
    </ul>
  );
}
骨架屏加载
function Skeleton({ className, ...props }) {
  return (
    <div
      className={cn(
        'animate-pulse rounded-md bg-gray-200',
        className
      )}
      {...props}
    />
  );
}

function CardSkeleton() {
  return (
    <div className="rounded-xl border border-gray-200 p-6">
      <div className="flex items-center gap-4">
        <Skeleton className="h-12 w-12 rounded-full" />
        <div className="space-y-2">
          <Skeleton className="h-4 w-[200px]" />
          <Skeleton className="h-3 w-[150px]" />
        </div>
      </div>
      <div className="mt-4 space-y-2">
        <Skeleton className="h-3 w-full" />
        <Skeleton className="h-3 w-full" />
        <Skeleton className="h-3 w-3/4" />
      </div>
    </div>
  );
}

最佳实践

1. 使用 cn() 工具合并类名
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
2. 提取常见模式
@layer components {
  /* Consistent focus ring */
  .focus-ring {
    @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2;
  }

  /* Consistent disabled state */
  .disabled-state {
    @apply disabled:pointer-events-none disabled:opacity-50;
  }

  /* Truncate text */
  .truncate-lines-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}
3. 编写组件 API 文档
/**
 * Button component with multiple variants
 *
 * @example
 * <Button variant="primary" size="lg">Click me</Button>
 *
 * @example
 * <Button variant="ghost" size="icon">
 *   <IconMenu />
 * </Button>
 */
export function Button({ ... }) { ... }
4. 测试组件变体
// Button.test.tsx
describe('Button', () => {
  it('renders all variants correctly', () => {
    const variants = ['default', 'secondary', 'outline', 'ghost', 'destructive'];

    variants.forEach(variant => {
      render(<Button variant={variant}>Test</Button>);
      // Assert classes are applied correctly
    });
  });
});

评论

支持 Markdown