web-component-design について
再利用可能な UI コンポーネントを設計するフロントエンド開発者向けのスキルです。React、Vue、Svelte の合成パターン、スタイル、API 設計を扱います。
主な機能
- 01コンポーネント合成パターン
- 02CSS-in-JS の選択
- 03コンポーネント API 設計
- 04React、Vue、Svelte のパターン
- 05アクセシビリティとレスポンシブ対応
活用シーン
- 01コンポーネントライブラリやデザインシステムの設計
- 02複雑な UI のコンポーネント化
- 03既存コンポーネントのリファクタリング
- 04キーボードやスクリーンリーダー対応の改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ wshobson/ agents --skill web-component-designインストール後、Claude Code で/web-component-designを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/accessibility-patterns.mdcomponent-patterns.mdcss-styling-approaches.md詳細
Web コンポーネント設計
クリーンなコンポジションパターンとスタイリング手法を用いて、モダンなフレームワークで再利用可能で保守しやすい UI コンポーネントを構築します。
このスキルを使う場面
- 再利用可能なコンポーネントライブラリやデザインシステムを設計する
- 複雑なコンポーネントコンポジションパターンを実装する
- CSS-in-JS ソリューションを選定して適用する
- アクセシブルでレスポンシブな UI コンポーネントを構築する
- コードベース全体で一貫したコンポーネント API を作成する
- レガシーコンポーネントをモダンなパターンにリファクタリングする
- 複合コンポーネントやレンダープロップスを実装する
中核となる概念
1. コンポーネントコンポジションパターン
複合コンポーネント: 連携して動作する関連コンポーネント
// 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>
レンダープロップス: レンダリングを親に委譲する
<DataFetcher url="/api/users">
{({ data, loading, error }) =>
loading ? <Spinner /> : <UserList users={data} />
}
</DataFetcher>
スロット (Vue/Svelte): 名前付きコンテンツ注入ポイント
<template>
<Card>
<template #header>Title</template>
<template #content>Body text</template>
<template #footer><Button>Action</Button></template>
</Card>
</template>
2. CSS-in-JS のアプローチ
| ソリューション | アプローチ | 最適な用途 |
|---|---|---|
| Tailwind CSS | ユーティリティクラス | 迅速なプロトタイピング、デザインシステム |
| CSS Modules | スコープ付き CSS ファイル | 既存の CSS、段階的な導入 |
| styled-components | テンプレートリテラル | React、動的スタイリング |
| Emotion | オブジェクト/テンプレートスタイル | 柔軟、SSR フレンドリー |
| Vanilla Extract | ゼロランタイム | パフォーマンス重視のアプリ |
3. コンポーネント API 設計
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;
}
原則:
- 意味のあるプロパティ名を使う(
loadingではなくisLoading) - 適切なデフォルト値を提供する
childrenによるコンポジションをサポートするclassNameやstyleによるスタイルの上書きを可能にする
クイックスタート: Tailwind を使った React コンポーネント
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";
フレームワーク別パターン
React: 複合コンポーネント
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: コンポーザブル
<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: ルーン
<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>
ベストプラクティス
- 単一責任: 各コンポーネントは 1 つのことをうまくやる
- プロップドリリングの防止: 深くネストしたデータにはコンテキストを使う
- デフォルトでアクセシブル: ARIA 属性やキーボードサポートを含める
- 制御コンポーネントと非制御コンポーネント: 適切な場合は両方のパターンをサポートする
- Ref の転送: 親が DOM ノードにアクセスできるようにする
- メモ化: 高コストなレンダリングには
React.memo、useMemoを使う - エラーバウンダリ: 失敗する可能性のあるコンポーネントをラップする
よくある問題
- プロップの爆発: プロップが多すぎる場合は、代わりにコンポジションを検討する
- スタイルの競合: スコープ付きスタイルや CSS Modules を使う
- 再レンダリングの連鎖: React DevTools でプロファイリングし、適切にメモ化する
- アクセシビリティの不足: スクリーンリーダーとキーボードナビゲーションでテストする
- バンドルサイズ: 未使用のコンポーネントバリアントをツリーシェイクする
コメント
Markdown 対応