完成形を見てみよう:Button コンポーネントの全体像
この章で学ぶこと
本書で作る Button コンポーネントの完成形を、先に確認します。全体像をつかんでから、各技術を 1 つずつ見ていきます。
なぜ完成形から始めるのか
本書は、部品を 1 つずつ積み上げる前に、完成形の Button を先に見せます。
学習アプローチ
・一般的:A → B → C → D → ようやく完成
・本書 :完成形を見る → A → B → C → D → 腑に落ちる
完成形を先に見ておくと、各技術が「どこで・なぜ」使われているのかが分かり、学習効率が大きく上がります。
Button.tsx の完成形
以下が、本書で組み立てていく Button コンポーネントです。これは shadcn/ui の Button を、学習用に少し整理したものです。
import * as React from "react"
import { Slot } from "radix-ui"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
本書では、この 1 つ 1 つの要素を順番に解説します。今は全体の形を眺めるだけで十分です。
bg-primary や text-primary-foreground といった色は、Tailwind に最初から入っている色ではなく、デザイントークン(プロジェクトで決めた意味のある色)です。これはデザイントークンの章で定義します。
末尾の export { Button, buttonVariants } で buttonVariants も公開しているのは、Button コンポーネントを介さずにスタイルだけを別の要素に当てたいときのためです。たとえば <a className={buttonVariants({ variant: "outline" })}> のように、リンクに Button の見た目だけを適用できます。Button だけ使うなら必須ではありませんが、shadcn/ui の慣習として公開しておくと後で便利です。
コードの構造を分解する
このコンポーネントは、大きく 4 つのパートでできています。
[Button.tsx の構造]
│
├─ Part 1: インポート
│ ├─ React
│ ├─ Slot(radix-ui から)
│ ├─ cva, VariantProps(class-variance-authority から)
│ └─ cn(@/lib/utils から)
│
├─ Part 2: buttonVariants(cva によるスタイル定義)
│ ├─ ベースクラス(共通スタイル)
│ ├─ variants(variant・size)
│ └─ defaultVariants(デフォルト値)
│
├─ Part 3: Button の引数の型
│ ├─ React.ComponentProps<"button">(button の HTML 属性)
│ ├─ VariantProps(variant・size の型)
│ └─ asChild
│
└─ Part 4: Button 本体(関数コンポーネント)
├─ props の受け取り
├─ Comp の選択(Slot か button か)
└─ JSX を返す
使われている技術の一覧
各パートで使われている技術と、それを詳しく学ぶ章は次のとおりです。
| 技術 | 役割 | 詳しく学ぶ章 |
|---|---|---|
デザイントークン(bg-primary 等) | 色・余白などを意味で管理 | デザイントークンの章 |
cva / buttonVariants | バリアントを宣言的に定義 | cva の章 |
cn | クラス名を安全に結合 | cn の章 |
ref(prop で受け取る) | DOM 要素への参照を渡す | ref の章 |
Slot / asChild | 要素を差し替える | Slot の章 |
data-slot | スタイリングや識別の目印 | 発展編 |
型(ComponentProps 等) | 型安全な props | TypeScript の章 |
このコンポーネントには forwardRef が出てきません。React 19 では ref を普通の prop として受け取れるようになったためです。以前は forwardRef という仕組みが必要でしたが、その経緯も含めてref の章で説明します。
data-slot="button" は、その要素が「Button という役割(slot)」であることを示す目印です。
data-slot は何のために使う?(クリックで展開)
data-slot は shadcn/ui が Tailwind CSS v4 対応のときに全コンポーネントへ導入した「スタイリングのフック」です。主な用途は次の 3 つです。
- コンポジションで親から子を狙う(最も重要):クラス名に頼らず、子要素の「役割」を頼りに親からスタイルを当てられます。たとえば
[data-slot="form"] [data-slot="button"]で「フォーム内のすべてのボタン」を、Card なら[&_[data-slot=card-header]]:border-bで「Card 内のヘッダー」を狙えます(発展編で実践します)。 - 役割ベースの選択(semantic targeting):構造や class ではなく「目的(役割)」で要素を選びます。
[data-state="open"]のような状態属性と組み合わせて、状態に応じたスタイルを当てることもできます。 - 状態の統一処理・テストでの特定:複合コンポーネントが子要素の状態(focus など)をまとめて扱うのに使ったり、変わりやすいクラス名ではなく安定した属性としてテストで要素を特定したりするのにも役立ちます。
class ではなく data-slot を使うのは、class は利用者が cn で上書きしうるのに対し、data-slot は変わらない「役割」のラベルだからです。
基本的な使い方
この Button は、次のように使います。
デフォルトのボタン
<Button>Click me</Button>
props を何も指定しなければ、defaultVariants のスタイル(variant: "default" / size: "default")が適用されます。
バリアントの指定
<Button variant="destructive">削除</Button>
<Button variant="outline">キャンセル</Button>
<Button variant="secondary">サブ</Button>
<Button variant="ghost">ゴースト</Button>
<Button variant="link">リンク</Button>
サイズの指定
<Button size="sm">小</Button>
<Button size="default">標準</Button>
<Button size="lg">大</Button>
<Button size="icon">🔍</Button>
バリアントとサイズの組み合わせ
<Button variant="destructive" size="lg">アカウントを削除</Button>
カスタムスタイルの追加
<Button className="mt-4 w-full">送信</Button>
デフォルトスタイルと className は cn 関数が安全に結合します(cn の章)。
asChild パターン
<Button variant="outline" asChild>
<a href="/profile">プロフィールを見る</a>
</Button>
見た目は Button のまま、実際にレンダリングされるのは <a> タグになります(Slot の章)。
ref の使用
import { useRef, useEffect } from "react"
function FocusButton() {
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
buttonRef.current?.focus()
}, [])
return <Button ref={buttonRef}>自動でフォーカスされます</Button>
}
ref を渡すと、ボタンの DOM 要素にアクセスできます。React 19 では forwardRef なしでこれが動きます(ref の章)。
なぜこの設計が優れているのか
1. 宣言的なスタイル管理
cva を使うと、どのバリアントがどのスタイルを持つかが一目で分かります。if 文や三項演算子の嵐になりません。
2. 安全なカスタマイズ
利用者は className でスタイルを安全に上書きできます。cn 関数がデフォルトとの衝突を解くので、後から渡した方が正しく優先されます。
3. セマンティック HTML の維持
asChild を使えば、見た目を保ったまま適切な HTML 要素(リンクなら <a>)を選べます。
4. 型安全性
TypeScript との連携で、使うときに補完が効き、無効な値を渡すとエラーになります。
5. アクセシビリティ
ref でフォーカス管理ができ、focus-visible や disabled の状態にも一貫したスタイルが当たります。
6. デザインシステムとの一貫性
bg-primary などのデザイントークンを使うことで、アプリ全体で色やテーマが揃います。1 箇所のトークンを変えれば、すべてのボタンに反映されます(デザイントークンの章)。
この章のまとめ
- 完成形は 4 つのパート(インポート / バリアント定義 / 型 / 本体)でできている
- 使われている技術は デザイントークン・cva・cn・ref・Slot・型
- React 19 では
forwardRefなしでrefを受け取れる - この設計は「宣言的・安全・一貫性」というデザインシステムの原則を満たしている
ここで見せたコードは学習用に整理したものです。実際の shadcn/ui の Button は、アイコンの自動調整や入力エラー時の表示など、さらに多くのクラスとサイズを持っています。その「本物」は発展編で扱います。
次章では、まず Tailwind CSS の基礎を確認します。