Web Component Design

作者 wshobson46891e7e60da无许可证收录于 2026年10月8日更新于 2026年10月8日

Master React, Vue, and Svelte component patterns including CSS-in-JS, composition strategies, and reusable component architecture. Use when building UI component libraries, designing component APIs, or implementing frontend design systems.

AI 生成的概览

指导在 React、Vue 和 Svelte 中构建可复用 UI 组件,涵盖组合模式、CSS-in-JS 选择与组件 API 设计。

功能
为跨 React、Vue 和 Svelte 设计可复用前端 UI 组件提供参考指导。内容涵盖复合组件、render props 和插槽等组合模式,比较 CSS-in-JS 与样式方案,并阐述组件 API 设计原则。还包含各框架的具体示例、最佳实践与常见问题,并附有无障碍、组件模式和 CSS 样式方案等参考文件。
适用场景
适用于构建 UI 组件库或设计系统、设计组件 API,或实现前端设计系统时。也适合将旧组件重构为现代模式,以及构建无障碍、响应式组件。
运行要求
无需脚本或特殊工具,仅为说明文档与参考文件。示例涉及 React、Vue、Svelte、Tailwind CSS、class-variance-authority 等前端库,但阅读这些指导无需安装任何内容。

Web Component Design

Build reusable, maintainable UI components using modern frameworks with clean composition patterns and styling approaches.

When to Use This Skill

  • Designing reusable component libraries or design systems
  • Implementing complex component composition patterns
  • Choosing and applying CSS-in-JS solutions
  • Building accessible, responsive UI components
  • Creating consistent component APIs across a codebase
  • Refactoring legacy components into modern patterns
  • Implementing compound components or render props

Core Concepts

1. Component Composition Patterns

Compound Components: Related components that work together

tsx
// Usage<Select value={value} onChange={setValue}>  <Select.Trigger>Choose option</Select.Trigger>  <Select.Options>    <Select.Option value="a">Option A</Select.Option>    <Select.Option value="b">Option B</Select.Option>  </Select.Options></Select>

Render Props: Delegate rendering to parent

tsx
<DataFetcher url="/api/users">  {({ data, loading, error }) =>    loading ? <Spinner /> : <UserList users={data} />  }</DataFetcher>

Slots (Vue/Svelte): Named content injection points

vue
<template>  <Card>    <template #header>Title</template>    <template #content>Body text</template>    <template #footer><Button>Action</Button></template>  </Card></template>

2. CSS-in-JS Approaches

SolutionApproachBest For
Tailwind CSSUtility classesRapid prototyping, design systems
CSS ModulesScoped CSS filesExisting CSS, gradual adoption
styled-componentsTemplate literalsReact, dynamic styling
EmotionObject/template stylesFlexible, SSR-friendly
Vanilla ExtractZero-runtimePerformance-critical apps

3. Component API Design

tsx
interface ButtonProps {  variant?: "primary" | "secondary" | "ghost";  size?: "sm" | "md" | "lg";  isLoading?: boolean;  isDisabled?: boolean;  leftIcon?: React.ReactNode;  rightIcon?: React.ReactNode;  children: React.ReactNode;  onClick?: () => void;}

Principles:

  • Use semantic prop names (isLoading vs loading)
  • Provide sensible defaults
  • Support composition via children
  • Allow style overrides via className or style

Quick Start: React Component with Tailwind

tsx
import { forwardRef, type ComponentPropsWithoutRef } from "react";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";
const buttonVariants = cva(  "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",  {    variants: {      variant: {        primary: "bg-blue-600 text-white hover:bg-blue-700",        secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",        ghost: "hover:bg-gray-100 hover:text-gray-900",      },      size: {        sm: "h-8 px-3 text-sm",        md: "h-10 px-4 text-sm",        lg: "h-12 px-6 text-base",      },    },    defaultVariants: {      variant: "primary",      size: "md",    },  },);
interface ButtonProps  extends    ComponentPropsWithoutRef<"button">,    VariantProps<typeof buttonVariants> {  isLoading?: boolean;}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(  ({ className, variant, size, isLoading, children, ...props }, ref) => (    <button      ref={ref}      className={cn(buttonVariants({ variant, size }), className)}      disabled={isLoading || props.disabled}      {...props}    >      {isLoading && <Spinner className="mr-2 h-4 w-4" />}      {children}    </button>  ),);Button.displayName = "Button";

Framework Patterns

React: Compound Components

tsx
import { createContext, useContext, useState, type ReactNode } from "react";
interface AccordionContextValue {  openItems: Set<string>;  toggle: (id: string) => void;}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {  const context = useContext(AccordionContext);  if (!context) throw new Error("Must be used within Accordion");  return context;}
export function Accordion({ children }: { children: ReactNode }) {  const [openItems, setOpenItems] = useState<Set<string>>(new Set());
  const toggle = (id: string) => {    setOpenItems((prev) => {      const next = new Set(prev);      next.has(id) ? next.delete(id) : next.add(id);      return next;    });  };
  return (    <AccordionContext.Provider value={{ openItems, toggle }}>      <div className="divide-y">{children}</div>    </AccordionContext.Provider>  );}
Accordion.Item = function AccordionItem({  id,  title,  children,}: {  id: string;  title: string;  children: ReactNode;}) {  const { openItems, toggle } = useAccordion();  const isOpen = openItems.has(id);
  return (    <div>      <button onClick={() => toggle(id)} className="w-full text-left py-3">        {title}      </button>      {isOpen && <div className="pb-3">{children}</div>}    </div>  );};

Vue 3: Composables

vue
<script setup lang="ts">import { ref, computed, provide, inject, type InjectionKey } from "vue";
interface TabsContext {  activeTab: Ref<string>;  setActive: (id: string) => void;}
const TabsKey: InjectionKey<TabsContext> = Symbol("tabs");
// Parent componentconst activeTab = ref("tab-1");provide(TabsKey, {  activeTab,  setActive: (id: string) => {    activeTab.value = id;  },});
// Child component usageconst tabs = inject(TabsKey);const isActive = computed(() => tabs?.activeTab.value === props.id);</script>

Svelte 5: Runes

svelte
<script lang="ts">  interface Props {    variant?: 'primary' | 'secondary';    size?: 'sm' | 'md' | 'lg';    onclick?: () => void;    children: import('svelte').Snippet;  }
  let { variant = 'primary', size = 'md', onclick, children }: Props = $props();
  const classes = $derived(    `btn btn-${variant} btn-${size}`  );</script>
<button class={classes} {onclick}>  {@render children()}</button>

Best Practices

  1. Single Responsibility: Each component does one thing well
  2. Prop Drilling Prevention: Use context for deeply nested data
  3. Accessible by Default: Include ARIA attributes, keyboard support
  4. Controlled vs Uncontrolled: Support both patterns when appropriate
  5. Forward Refs: Allow parent access to DOM nodes
  6. Memoization: Use React.memo, useMemo for expensive renders
  7. Error Boundaries: Wrap components that may fail

Common Issues

  • Prop Explosion: Too many props - consider composition instead
  • Style Conflicts: Use scoped styles or CSS Modules
  • Re-render Cascades: Profile with React DevTools, memo appropriately
  • Accessibility Gaps: Test with screen readers and keyboard navigation
  • Bundle Size: Tree-shake unused component variants

来源与署名

来源:wshobson/agents位于plugins/ui-design/skills/web-component-design提交46891e7

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架