web-component-design

Par wshobson · agents

Maîtrisez les patterns de composants React, Vue et Svelte, notamment le CSS-in-JS, les stratégies de composition et l'architecture de composants réutilisables. À utiliser lors de la création de bibliothèques de composants UI, de la conception d'API de composants ou de l'implémentation de design systems frontend.

npx skills add https://github.com/wshobson/agents --skill web-component-design

Web Component Design

Construisez des composants d'interface utilisateur réutilisables et maintenables en utilisant des frameworks modernes avec des motifs de composition propres et des approches de stylisation.

Quand utiliser cette compétence

  • Concevoir des bibliothèques de composants réutilisables ou des systèmes de conception
  • Implémenter des motifs complexes de composition de composants
  • Choisir et appliquer des solutions CSS-in-JS
  • Construire des composants d'interface utilisateur accessibles et réactifs
  • Créer des API de composants cohérentes dans une base de code
  • Refactoriser des composants legacy en motifs modernes
  • Implémenter des composants composés ou render props

Concepts fondamentaux

1. Motifs de composition de composants

Composants composés : Des composants liés qui travaillent ensemble

// 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 : Déléguer le rendu au composant parent

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

Slots (Vue/Svelte) : Points d'injection de contenu nommés

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

2. Approches CSS-in-JS

Solution Approche Idéal pour
Tailwind CSS Classes utilitaires Prototypage rapide, systèmes de conception
CSS Modules Fichiers CSS scoped CSS existant, adoption progressive
styled-components Template literals React, stylisation dynamique
Emotion Styles objet/template Flexible, compatible SSR
Vanilla Extract Zéro runtime Applications critiques en performance

3. Conception d'API de composants

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;
}

Principes :

  • Utiliser des noms de props sémantiques (isLoading au lieu de loading)
  • Fournir des valeurs par défaut judicieuses
  • Supporter la composition via children
  • Permettre les surcharges de style via className ou style

Démarrage rapide : Composant React avec Tailwind

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";

Motifs par framework

React : Composants composés

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

<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 component
const activeTab = ref("tab-1");
provide(TabsKey, {
  activeTab,
  setActive: (id: string) => {
    activeTab.value = id;
  },
});

// Child component usage
const tabs = inject(TabsKey);
const isActive = computed(() => tabs?.activeTab.value === props.id);
</script>

Svelte 5 : Runes

<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>

Bonnes pratiques

  1. Responsabilité unique : Chaque composant fait bien une seule chose
  2. Prévention du prop drilling : Utiliser le contexte pour les données imbriquées profondément
  3. Accessible par défaut : Inclure les attributs ARIA et le support clavier
  4. Contrôlé vs non contrôlé : Supporter les deux motifs quand approprié
  5. Forward Refs : Permettre l'accès parent aux nœuds DOM
  6. Mémoïsation : Utiliser React.memo, useMemo pour les rendus coûteux
  7. Error Boundaries : Envelopper les composants qui pourraient échouer

Problèmes courants

  • Explosion de props : Trop de props - considérer la composition à la place
  • Conflits de styles : Utiliser les styles scoped ou CSS Modules
  • Cascades de re-render : Profiler avec React DevTools, mémoïser appropriément
  • Lacunes d'accessibilité : Tester avec des lecteurs d'écran et la navigation au clavier
  • Taille du bundle : Tree-shake les variantes de composants inutilisées

Skills similaires