同じ「保存」ボタンなのに、画面ごとに色や大きさが違う。共通部品を作ったものの、例外が増えて、結局コピーして修正している。デザインシステムの悩みは、部品の数よりも、何を共通にして、どこを変えてよいかが共有されていないことから始まります。
この記事では、既存画面の点検から、色・文字・余白の定義、Reactのボタン、状態一覧、変更の運用までをつなぎます。UIは画面上の表示や操作、UXは利用者が目的を達成するまでの体験です。基礎はUI/UXの全体像、制作工程はWebサイト作成入門で補えます。
30秒で要点
- 共通化する前に、利用者が困る違いと、業務上必要な違いを分ける。
- トークン、部品、組合せのパターン、使い方と更新責任を一緒に管理する。
- 小さな部品を一つの実画面へ導入し、状態・操作・移行の負担を確かめて広げる。
デザインシステムと部品集の違い
デザインシステムは、再利用できる部品に加えて、設計の原則、使い方、変更を維持する仕組みを含みます。部品集を導入しても、どの場面で使うか、例外を誰が認めるかが不明なら、似た部品が再び増えることがあります。
表が収まらない場合は、横にスクロールできます
| 要素 | 決めること | 決めないまま進むと起きること |
|---|---|---|
| トークン | 色や余白などの値と意味の名前 | 同じ用途に少しずつ違う値が増える |
| コンポーネント | ボタンなど一つの部品の属性・状態・操作 | 見た目は同じでも送信や無効時の動きが違う |
| パターン | 入力→確認→完了などの組合せ | 単体は正しくても全体の手順で迷う |
| ガイドと運用 | 使用条件、例外、確認者、移行方法 | 作った部品が更新されず、別実装が増える |
まず、似ている画面を数枚選びます。違う色が必要な意味を持つか、単なる実装の揺れかを確認します。決済と記事の閲覧では、誤操作の影響も必要な説明も違います。同じ見た目に寄せることが、使いやすさを損なう場合もあります。
USWDSの成熟度モデルは、原則、利用者体験の指針、コードを段階的に取り入れる考え方を示しています。これは米国行政の文脈ですが、一度に全要素を置き換えず、実際の利用者の必要から始める点は導入計画の参考になります。色・文字・余白を、意味のある名前で持つ
トークンは、設計で再利用する値に名前を付けたものです。「青500」という色名と「主要操作の背景」という役割は別です。値と役割を分けると、ブランド色を変えたとき、どの用途まで変更すべきかを考えやすくなります。
ここからの7ファイルは一つの説明用教材です。ReactとTypeScriptが動くプロジェクトの同じディレクトリへ置き、CSSをアプリのスタイル入口から読み込む前提です。TailwindやStorybookの導入は必要ありません。Next.jsでクリック処理を渡す場合は、利用側のクライアント境界も適切に設定してください。
色は、前景と背景の組で決める
colors.tsでは最小限の配色を用意します。色名に成功・失敗を付けるだけで状態の意味が伝わるわけではないため、警告などには文章やアイコンも別途必要です。
// colors.ts
export const colors = {
action: '#1d4ed8',
onAction: '#ffffff',
surface: '#ffffff',
mutedSurface: '#f1f5f9',
text: '#172033',
border: '#64748b',
} as const;
文字は、大きさと行の高さを組み合わせる
typography.tsはOSの標準フォントを利用する例です。フォントの指定だけで可読性は決まらず、実際の日本語、長い名称、拡大時の折り返しを確認します。
// typography.ts
export const typography = {
fontFamily: 'system-ui, sans-serif',
fontSize: { sm: '0.875rem', md: '1rem', lg: '1.125rem' },
fontWeight: 600,
lineHeight: 1.5,
} as const;
余白は、選択肢を絞って使う
spacing.tsは小さな尺度です。すべての画面を同じ余白に固定するためではなく、隣り合う情報の関係を表すときの候補にします。
// spacing.ts
export const spacing = {
compact: '0.5rem',
regular: '0.75rem',
roomy: '1rem',
section: '2rem',
} as const;
部品から参照する入口をまとめる
design-tokens.tsで3種類の値をまとめます。このオブジェクトはプロジェクト内の実装例であり、トークンの交換形式そのものではありません。
// design-tokens.ts
import { colors } from './colors';
import { typography } from './typography';
import { spacing } from './spacing';
export const designTokens = { colors, typography, spacing } as const;
ツール間の受け渡しには、DTCGのDesign Tokens Format Module 2025.10という形式があります。2025年10月28日付のCommunity Group Reportで、W3C標準とは区別されます。型や値、参照などを扱うJSON形式なので、上のTypeScriptオブジェクトを置いただけで準拠するわけではありません。採用するなら、利用ツールが必要な型を読み書きできるかを別に確認します。
ボタンは見た目とHTMLの役割を両方保つ
ボタンは、小さな部品でも意外に多くの約束を持ちます。操作できる状態か、フォームを送信するか、キーボードのフォーカスが分かるか、長いラベルが収まるかを一緒に考えます。
次のButton.tsxでは、3種類の見た目と3段階の大きさを使い分けます。HTML標準のdisabled、name、value、aria-*、イベントなどを渡せる形にし、型の定義と実際の表示をつなぎます。直接のstyle上書きはこの例では公開しません。例外が必要なら、用途を確認して共通仕様へ加えます。
// Button.tsx
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { designTokens as tokens } from './design-tokens';
export type ButtonProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'style'
> & {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
};
export function Button({
children, variant = 'primary', size = 'md',
type = 'button', className = '', ...nativeProps
}: ButtonProps) {
const variants = {
primary: { backgroundColor: tokens.colors.action,
color: tokens.colors.onAction, borderColor: tokens.colors.action },
secondary: { backgroundColor: tokens.colors.mutedSurface,
color: tokens.colors.text, borderColor: tokens.colors.border },
outline: { backgroundColor: tokens.colors.surface,
color: tokens.colors.action, borderColor: tokens.colors.action },
};
const sizes = {
sm: { padding: tokens.spacing.compact, minHeight: '44px' },
md: { padding: tokens.spacing.regular, minHeight: '48px' },
lg: { padding: tokens.spacing.roomy, minHeight: '52px' },
};
return (
<button
{...nativeProps}
type={type}
className={`ds-button ${className}`.trim()}
style={{
...variants[variant], ...sizes[size],
fontFamily: tokens.typography.fontFamily,
fontSize: tokens.typography.fontSize[size],
fontWeight: tokens.typography.fontWeight,
lineHeight: tokens.typography.lineHeight,
}}
>
{children}
</button>
);
}
既定のtype="button"は、フォーム内で意図しない送信を起こしにくくする設計です。送信に使う場所ではtype="submit"を明示します。React公式も、typeを省略したボタンのフォーム送信に注意を促しています。フォームとボタンの注意点
操作中の見え方はCSSで補う
Button.cssをアプリのCSS入口から読み込みます。フォーカスの線を消さず、ホバーでは下線を加える例です。クラスを追加して上書きする場合も、部品の既存確認がそのまま通用するとは考えません。
/* Button.css */
.ds-button {
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-radius: 4px;
min-width: 44px;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
cursor: pointer;
}
.ds-button:hover:not(:disabled) { text-decoration: underline; }
.ds-button:focus-visible {
outline: 3px solid #172033;
outline-offset: 3px;
}
.ds-button:disabled { cursor: not-allowed; }
この44pxは教材として選んだ設計値です。WCAGの対象サイズの説明では、24×24 CSS pxの基準と、間隔・インライン等の例外を扱います。CSSの高さだけでなく、隣の操作対象や実際の領域も確認する必要があります。
文字の色は前景と背景の組で評価します。例の主要ボタンの白文字と#1d4ed8は約6.70:1ですが、この一組の計算だけで部品全体がアクセシブルとは言えません。WCAGのコントラスト説明には、通常文字4.5:1、大きな文字3:1と例外があり、判定では値を丸めません。フォーカス、無効状態、背景の変更も別に見ます。
状態の一覧を、使い方の文書にする
「青いボタンが出る」だけでは確認が足りません。次のButton.examples.tsxは、部品の状態を同じ引数形式で列挙する、独立したReactカタログです。値を変更したら、この一覧と実際の使用画面を照合します。
// Button.examples.tsx
import { Button, type ButtonProps } from './Button';
export const buttonExamples = [
{ name: 'Primary', props: { children: '保存する', variant: 'primary' } },
{ name: 'Secondary', props: { children: '戻る', variant: 'secondary' } },
{ name: 'OutlineSmall', props: {
children: '詳細を見る', variant: 'outline', size: 'sm' } },
{ name: 'Disabled', props: { children: '送信できません', disabled: true } },
{ name: 'Submit', props: {
children: '内容を送信', type: 'submit', name: 'intent', value: 'save' } },
{ name: 'LongLabel', props: {
children: '入力内容を確認して保存する', size: 'lg' } },
] satisfies Array<{ name: string; props: ButtonProps }>;
export function ButtonExamples() {
return (
<div style={{ display: 'grid', gap: '1rem', maxWidth: '20rem' }}>
{buttonExamples.map(({ name, props }) => (
<section key={name}>
<h2>{name}</h2>
<Button {...props} />
</section>
))}
</div>
);
}
Storybookのstoryも、引数に応じた状態を記述します。Storybookをすでに採用しているチームなら、この状態一覧をstoryへ移し、利用フレームワークに対応する型と設定を追加できます。ここで示したカタログはStorybook固有の設定ファイルではなく、Storybookの起動や表示確認を済ませた例でもありません。
最低限、既定のボタンが送信にならないこと、明示した送信属性を保つこと、無効状態とラベル、3種類の見た目と大きさの違いを確認します。掲載例の型と生成HTMLは静的に確認していますが、実ブラウザでのフォーカス移動、折り返し、クリック、支援技術による読み上げは利用する画面での追加確認が必要です。
小さく導入して、変更の責任を残す
導入は、使っている部品をすべて置き換えることから始めなくても構いません。似た役割のボタンを一つ選び、使用箇所と例外を洗い出し、変更を戻せる状態で一画面へ適用します。
小規模チームの仮想例なら、トークンと主要ボタン、確認する状態一覧から始め、担当者と見直す日を決めます。複数チームを持つ組織の仮想例なら、変更提案の窓口、互換性、移行期間、各画面の確認担当まで必要になるでしょう。どちらも実績事例ではなく、人数だけで最適な構成や効率向上率を決める例ではありません。
表が収まらない場合は、横にスクロールできます
| 運用で決める項目 | 残す内容 |
|---|---|
| 提案 | どの利用者の問題を、どの画面で見つけたか |
| 承認 | デザイン・実装・アクセシビリティを誰が確認するか |
| 互換性 | 属性や見た目を変えたとき、既存画面に何が起こるか |
| 移行 | 対象画面、変更手順、併存する期間、戻し方 |
| 廃止 | 旧部品を使っている場所と、置換が終わる条件 |
再利用率を見るなら、対象とする画面や部品の分母を固定します。制作時間を比較するなら、難しさの違い、初期導入費、レビューと移行の時間を含めます。導入しただけで30%や50%の効率改善が得られるという見込みを置くより、自社の一巡分を計測した方が判断材料になります。
最初の確認は、一つの操作を最後まで追う
カタログと実画面を並べ、同じボタンが長い日本語、無効状態、フォーム、拡大表示でどう使われるかを点検します。単体の見た目と、利用者が入力から完了まで進めることは別です。
新しい値や部品を足す前に、既存の仕様ではなぜ足りないのかを一文で残してください。共通仕様を広げるべきか、その画面だけの例外を認めるべきかを相談できます。レスポンシブデザインの考え方やWeb制作全体の進め方とつなぐと、部品の整備を実際の体験の改善へ戻しやすくなります。