google-material-design について
Googleのオープンソースデザインシステムです。Material 3のテーマ、自動レイアウト、モーション、アクセシビリティなど、画面設計と実装の指針を提供します。
主な機能
- 01Material 3のカラーとテーマ
- 02コンポーネントと操作状態の指針
- 03アダプティブレイアウトとナビゲーション
- 04タイポグラフィ、形状、標高のルール
- 05タッチとフォーカスのアクセシビリティ指針
活用シーン
- 01一貫したビジュアル言語の構築
- 02画面サイズに応じたナビゲーション調整
- 03画面の色、コンポーネント、モーションの選定
- 04タッチ領域とキーボードフォーカスの確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ copyleftdev/ sk1llz --skill google-material-designインストール後、Claude Code で/google-material-designを入力して読み込まれたか確認します。
詳細
Google Material Design
概要
Material Design は、物理世界とデジタル世界の隔たりを埋める、Google のオープンソースデザインシステムです。2014 年に発表され、Material Design 2(2018 年)、Material Design 3(2021 年)を経て進化してきました。あらゆるプラットフォームで美しく、機能的で、アクセシブルなインターフェースを構築するための包括的なフレームワークを提供します。
参考資料
- ガイドライン: https://m3.material.io/
- 基礎: https://m3.material.io/foundations
- コンポーネント: https://m3.material.io/components
- テーマ設定: https://m3.material.io/styles
- GitHub: https://github.com/material-components
基本理念
「Material はメタファーです。」
「大胆で、グラフィカルで、意図的。」
「動きが意味をもたらします。」
「アダプティブなデザイン、心躍る体験。」
Material Design は、優れたデザインの古典的な原則と、テクノロジーや科学がもたらす革新と可能性を融合したビジュアル言語を生み出します。
Material のメタファー
物理世界からの着想
┌─────────────────────────────────────────────────────────┐
│ MATERIAL = DIGITAL PAPER + INK + PHYSICAL PROPERTIES │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ Light │
│ │ Surface │ ◄── casts shadows │
│ │ (z=2) │ based on elevation │
│ └─────────┘ │
│ │ │
│ │ Elevation │
│ ▼ │
│ ╔═════════════════════════════════════════════════╗ │
│ ║ Ground Plane (z=0) ║ │
│ ╚═════════════════════════════════════════════════╝ │
│ │
│ • Surfaces exist in 3D space with depth │
│ • Material has physical properties and affordances │
│ • Shadows communicate spatial relationships │
│ • Content is printed on material, not within it │
└─────────────────────────────────────────────────────────┘
エレベーションシステム
Elevation (dp) Component Examples Shadow Intensity
─────────────────────────────────────────────────────────────
0dp Background, disabled None
1dp Card (resting), Search bar Subtle
2dp Card (raised), Button Light
3dp Refresh indicator Light-Medium
4dp App Bar Medium
6dp FAB (resting), Snackbar Medium
8dp Bottom sheet, Menu Pronounced
12dp FAB (pressed), Dialog Strong
16dp Nav drawer Strong
24dp Modal bottom sheet Maximum
デザイン原則
1. Material はメタファーです
/* Material surfaces have consistent properties */
.surface {
/* Surfaces are opaque and cast shadows */
background: var(--md-sys-color-surface);
/* Elevation creates depth */
box-shadow: var(--md-sys-elevation-level2);
/* Corners define character */
border-radius: var(--md-sys-shape-corner-medium);
}
/* Content rests ON material, not inside it */
.content-on-surface {
/* Ink metaphor - content is printed on surface */
color: var(--md-sys-color-on-surface);
}
2. 大胆で、グラフィカルで、意図的
Typography Hierarchy (Material 3)
═══════════════════════════════════════════════════════════
Display Large 57sp Headlines for hero moments
Display Medium 45sp Large display text
Display Small 36sp Smaller display text
─────────────────────────────────────────────────────────
Headline Large 32sp High-emphasis text
Headline Medium 28sp Medium emphasis headers
Headline Small 24sp Smaller headlines
─────────────────────────────────────────────────────────
Title Large 22sp Prominent titles
Title Medium 16sp Medium titles
Title Small 14sp Smaller titles, tabs
─────────────────────────────────────────────────────────
Label Large 14sp Buttons, prominent labels
Label Medium 12sp Navigation labels
Label Small 11sp Timestamps, hints
─────────────────────────────────────────────────────────
Body Large 16sp Primary body text
Body Medium 14sp Secondary body text
Body Small 12sp Captions, annotations
3. 動きが意味をもたらします
// motion_principles.js
// Motion in Material Design is choreographed and meaningful
const MaterialMotion = {
// Duration tokens (Material 3)
duration: {
short1: 50, // Micro-interactions
short2: 100, // Simple selections
short3: 150, // Checkbox, switch
short4: 200, // Small expand/collapse
medium1: 250, // Standard enter/exit
medium2: 300, // Card expand
medium3: 350, // Complex animations
medium4: 400, // Page transitions
long1: 450, // Large area transitions
long2: 500, // Complex choreography
long3: 550, // Elaborate animations
long4: 600, // Maximum duration
},
// Easing curves
easing: {
// Emphasized - for prominent elements
emphasized: 'cubic-bezier(0.2, 0.0, 0, 1.0)',
emphasizedDecelerate: 'cubic-bezier(0.05, 0.7, 0.1, 1.0)',
emphasizedAccelerate: 'cubic-bezier(0.3, 0.0, 0.8, 0.15)',
// Standard - for most transitions
standard: 'cubic-bezier(0.2, 0.0, 0, 1.0)',
standardDecelerate: 'cubic-bezier(0, 0, 0, 1)',
standardAccelerate: 'cubic-bezier(0.3, 0, 1, 1)',
},
// Transition patterns
patterns: {
// Container transform - element becomes new screen
containerTransform: {
use: 'Element expands into a new screen',
duration: 'medium2',
easing: 'emphasized',
},
// Shared axis - elements share spatial relationship
sharedAxis: {
use: 'Navigation between related destinations',
duration: 'medium1',
easing: 'emphasized',
},
// Fade through - unrelated content transition
fadeThrough: {
use: 'Switching between unrelated content',
duration: 'medium1',
easing: 'emphasized',
},
// Fade - simple appearance/disappearance
fade: {
use: 'UI elements appearing or disappearing',
duration: 'short4',
easing: 'standard',
},
},
};
// Example: Container transform animation
function containerTransform(element, options) {
const { duration, easing } = MaterialMotion.patterns.containerTransform;
return element.animate([
{ transform: 'scale(0.85)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 },
], {
duration: MaterialMotion.duration[duration],
easing: MaterialMotion.easing[easing],
fill: 'forwards',
});
}
カラーシステム(Material 3 のダイナミックカラー)
トーナルパレット
Source Color → Tonal Palette → Color Scheme
═══════════════════════════════════════════════════════════
Primary Source (#6750A4)
│
▼
┌─────────────────────────────────────────────────────────┐
│ Tonal Palette: Primary │
│ ─────────────────────────────────────────────────────── │
│ 0 10 20 30 40 50 60 70 80 90 95 100│
│ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ████ ████ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ │
│ Darkest ◄─────────────────────────────────► Lightest │
└─────────────────────────────────────────────────────────┘
│
▼
Light Scheme: Dark Scheme:
primary: tone40 primary: tone80
onPrimary: tone100 onPrimary: tone20
primaryContainer: tone90 primaryContainer: tone30
onPrimaryContainer: tone10 onPrimaryContainer: tone90
色の役割
/* Material 3 Color Tokens */
:root {
/* Primary - Key color, brand identity */
--md-sys-color-primary: #6750A4;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #EADDFF;
--md-sys-color-on-primary-container: #21005D;
/* Secondary - Supporting color */
--md-sys-color-secondary: #625B71;
--md-sys-color-on-secondary: #FFFFFF;
--md-sys-color-secondary-container: #E8DEF8;
--md-sys-color-on-secondary-container: #1D192B;
/* Tertiary - Accent, contrast */
--md-sys-color-tertiary: #7D5260;
--md-sys-color-on-tertiary: #FFFFFF;
--md-sys-color-tertiary-container: #FFD8E4;
--md-sys-color-on-tertiary-container: #31111D;
/* Error - Alerts and errors */
--md-sys-color-error: #B3261E;
--md-sys-color-on-error: #FFFFFF;
--md-sys-color-error-container: #F9DEDC;
--md-sys-color-on-error-container: #410E0B;
/* Surface - Backgrounds */
--md-sys-color-surface: #FFFBFE;
--md-sys-color-on-surface: #1C1B1F;
--md-sys-color-surface-variant: #E7E0EC;
--md-sys-color-on-surface-variant: #49454F;
/* Outline - Borders, dividers */
--md-sys-color-outline: #79747E;
--md-sys-color-outline-variant: #CAC4D0;
}
コンテンツからのダイナミックカラー
// dynamic_color.js
// Extract color scheme from images or user preference
class MaterialColorScheme {
constructor(sourceColor) {
this.source = sourceColor;
this.palette = this.generateTonalPalette(sourceColor);
}
generateTonalPalette(color) {
// HCT (Hue, Chroma, Tone) color space for perceptual uniformity
const hct = rgbToHct(color);
return {
0: hctToRgb({ ...hct, tone: 0 }),
10: hctToRgb({ ...hct, tone: 10 }),
20: hctToRgb({ ...hct, tone: 20 }),
30: hctToRgb({ ...hct, tone: 30 }),
40: hctToRgb({ ...hct, tone: 40 }),
50: hctToRgb({ ...hct, tone: 50 }),
60: hctToRgb({ ...hct, tone: 60 }),
70: hctToRgb({ ...hct, tone: 70 }),
80: hctToRgb({ ...hct, tone: 80 }),
90: hctToRgb({ ...hct, tone: 90 }),
95: hctToRgb({ ...hct, tone: 95 }),
99: hctToRgb({ ...hct, tone: 99 }),
100: hctToRgb({ ...hct, tone: 100 }),
};
}
getLightScheme() {
return {
primary: this.palette[40],
onPrimary: this.palette[100],
primaryContainer: this.palette[90],
onPrimaryContainer: this.palette[10],
// ... remaining roles
};
}
getDarkScheme() {
return {
primary: this.palette[80],
onPrimary: this.palette[20],
primaryContainer: this.palette[30],
onPrimaryContainer: this.palette[90],
// ... remaining roles
};
}
}
コンポーネントアーキテクチャ
Material コンポーネントの構成
┌─────────────────────────────────────────────────────────┐
│ BUTTON ANATOMY │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ [Icon] Label Text │ │
│ └─────────────────────────────────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ │ Leading Icon (optional) │ │
│ │ │ │
│ │ Label text (required) Container │
│ │ (required) │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ States: Enabled, Disabled, Hovered, │
│ Focused, Pressed │
└─────────────────────────────────────────────────────────┘
ボタンのバリエーション
// MaterialButton.tsx
// Material 3 button implementations
interface ButtonProps {
variant: 'elevated' | 'filled' | 'tonal' | 'outlined' | 'text';
icon?: React.ReactNode;
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
}
// Elevated - For secondary actions requiring emphasis
const ElevatedButton = styled.button`
background: var(--md-sys-color-surface-container-low);
color: var(--md-sys-color-primary);
box-shadow: var(--md-sys-elevation-level1);
border: none;
border-radius: 20px;
padding: 10px 24px;
font: var(--md-sys-typescale-label-large);
&:hover {
box-shadow: var(--md-sys-elevation-level2);
background: color-mix(
in srgb,
var(--md-sys-color-primary) 8%,
var(--md-sys-color-surface-container-low)
);
}
&:focus-visible {
outline: none;
box-shadow: var(--md-sys-elevation-level1);
background: color-mix(
in srgb,
var(--md-sys-color-primary) 12%,
var(--md-sys-color-surface-container-low)
);
}
&:active {
box-shadow: var(--md-sys-elevation-level1);
background: color-mix(
in srgb,
var(--md-sys-color-primary) 12%,
var(--md-sys-color-surface-container-low)
);
}
`;
// Filled - For primary actions, high emphasis
const FilledButton = styled.button`
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border: none;
border-radius: 20px;
padding: 10px 24px;
&:hover {
box-shadow: var(--md-sys-elevation-level1);
background: color-mix(
in srgb,
var(--md-sys-color-on-primary) 8%,
var(--md-sys-color-primary)
);
}
`;
// Tonal - For secondary actions, medium emphasis
const TonalButton = styled.button`
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
border: none;
border-radius: 20px;
padding: 10px 24px;
`;
// Outlined - For secondary actions, low emphasis
const OutlinedButton = styled.button`
background: transparent;
color: var(--md-sys-color-primary);
border: 1px solid var(--md-sys-color-outline);
border-radius: 20px;
padding: 10px 24px;
`;
// Text - For lowest emphasis actions
const TextButton = styled.button`
background: transparent;
color: var(--md-sys-color-primary);
border: none;
border-radius: 20px;
padding: 10px 12px;
`;
状態レイヤーシステム
/* Material 3 State Layer Implementation */
.interactive-element {
position: relative;
overflow: hidden;
}
/* State layer overlay */
.interactive-element::before {
content: '';
position: absolute;
inset: 0;
background: currentColor;
opacity: 0;
transition: opacity 150ms var(--md-sys-motion-easing-standard);
}
/* State layer opacities */
.interactive-element:hover::before {
opacity: 0.08; /* Hover state */
}
.interactive-element:focus-visible::before {
opacity: 0.12; /* Focus state */
}
.interactive-element:active::before {
opacity: 0.12; /* Pressed state */
}
.interactive-element[data-dragged]::before {
opacity: 0.16; /* Dragged state */
}
/* Disabled states - no state layer, reduced opacity */
.interactive-element:disabled {
opacity: 0.38;
pointer-events: none;
}
.interactive-element:disabled::before {
display: none;
}
形状システム
コーナーのスタイル
Shape Scale (Material 3)
═══════════════════════════════════════════════════════════
None (0dp) Extra Small (4dp) Small (8dp)
┌──────────┐ ╭──────────╮ ╭──────────╮
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
└──────────┘ ╰──────────╯ ╰──────────╯
Medium (12dp) Large (16dp) Extra Large (28dp)
╭──────────╮ ╭──────────╮ ╭──────────╮
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
╰──────────╯ ╰──────────╯ ╰──────────╯
Full (50%)
╭────╮
│ │
│ │
╰────╯
/* Shape tokens */
:root {
--md-sys-shape-corner-none: 0px;
--md-sys-shape-corner-extra-small: 4px;
--md-sys-shape-corner-small: 8px;
--md-sys-shape-corner-medium: 12px;
--md-sys-shape-corner-large: 16px;
--md-sys-shape-corner-extra-large: 28px;
--md-sys-shape-corner-full: 9999px;
}
/* Shape application by component */
.chip { border-radius: var(--md-sys-shape-corner-small); }
.card { border-radius: var(--md-sys-shape-corner-medium); }
.dialog { border-radius: var(--md-sys-shape-corner-extra-large); }
.fab { border-radius: var(--md-sys-shape-corner-large); }
.button { border-radius: var(--md-sys-shape-corner-full); }
アダプティブレイアウト
レスポンシブグリッドシステム
Window Size Classes
═══════════════════════════════════════════════════════════
Compact (< 600dp) Medium (600-839dp) Expanded (840dp+)
┌─────────────┐ ┌────────────────┐ ┌───────────────────┐
│ │ │ │ │ Nav │ │
│ Content │ │ Content │ │ Bar │ Content │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
├─────────────┤ │ │ │ │ │
│ Nav Bar │ ├────────────────┤ │ │ │
└─────────────┘ │ Nav Bar │ │ │ │
└────────────────┘ └───────────────────┘
4 columns 8 columns (body) 12 columns (body)
16dp margins 24dp margins 24dp margins
8dp gutters 16dp gutters 24dp gutters
レイアウトの実装
// AdaptiveLayout.tsx
// Material 3 adaptive scaffold
import { useWindowSize } from './hooks';
type WindowSizeClass = 'compact' | 'medium' | 'expanded' | 'large' | 'extraLarge';
function getWindowSizeClass(width: number): WindowSizeClass {
if (width < 600) return 'compact';
if (width < 840) return 'medium';
if (width < 1200) return 'expanded';
if (width < 1600) return 'large';
return 'extraLarge';
}
interface AdaptiveLayoutProps {
navigation: React.ReactNode;
content: React.ReactNode;
detail?: React.ReactNode;
}
export function AdaptiveLayout({ navigation, content, detail }: AdaptiveLayoutProps) {
const { width } = useWindowSize();
const sizeClass = getWindowSizeClass(width);
// Compact: Bottom navigation bar
if (sizeClass === 'compact') {
return (
<div className="layout-compact">
<main className="content">{content}</main>
<nav className="navigation-bar">{navigation}</nav>
</div>
);
}
// Medium: Navigation rail
if (sizeClass === 'medium') {
return (
<div className="layout-medium">
<nav className="navigation-rail">{navigation}</nav>
<main className="content">{content}</main>
</div>
);
}
// Expanded+: Navigation drawer with optional detail pane
return (
<div className="layout-expanded">
<nav className="navigation-drawer">{navigation}</nav>
<main className="content">{content}</main>
{detail && sizeClass !== 'expanded' && (
<aside className="detail-pane">{detail}</aside>
)}
</div>
);
}
ナビゲーションパターン
Navigation Components by Screen Size
═══════════════════════════════════════════════════════════
Compact Medium Expanded
─────────────────────────────────────────────
Primary Bottom Bar Rail Drawer
3-5 items 3-7 items Full labels
Secondary Modal Drawer Modal Drawer Persistent Drawer
Tertiary Tabs Tabs Tabs
Bottom Sheet Side Sheet Side Sheet
アクセシビリティ(A11y)
タッチターゲット
/* Minimum touch target: 48x48dp */
.touch-target {
min-width: 48px;
min-height: 48px;
/* Visual element can be smaller, but touch area must be 48dp */
display: flex;
align-items: center;
justify-content: center;
}
.icon-button {
/* Icon is 24dp, but touch target is 48dp */
width: 48px;
height: 48px;
padding: 12px;
}
.icon-button svg {
width: 24px;
height: 24px;
}
色のコントラスト
Contrast Requirements (WCAG 2.1)
═══════════════════════════════════════════════════════════
Text Size Minimum (AA) Enhanced (AAA)
─────────────────────────────────────────────────
Normal text 4.5:1 7:1
Large text 3:1 4.5:1
(18sp+ or 14sp bold)
UI Components 3:1 Not defined
(borders, icons)
Material Design ensures:
• on-primary meets 4.5:1 against primary
• on-surface meets 4.5:1 against surface
• All semantic color pairs are accessible
フォーカスインジケーター
/* Material 3 focus indicators */
.focusable {
outline: none;
position: relative;
}
.focusable:focus-visible {
/* Focus ring using secondary color */
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
/* Alternative: Focus indicator within element */
.button:focus-visible {
outline: none;
}
.button:focus-visible::after {
content: '';
position: absolute;
inset: -4px;
border: 3px solid var(--md-sys-color-secondary);
border-radius: inherit;
pointer-events: none;
}
実装時の指針
必ず行うこと
- 色、タイポグラフィ、形状にはデザイントークンシステムを使用する
- すべてのインタラクティブな状態(有効、無効、ホバー、フォーカス、押下)を実装する
- 最小 48dp のタッチターゲットを確保する
- 視認可能なフォーカスインジケーターを用意する
- 生の値ではなく、セマンティックな色の役割(primary、secondary、error)を使用する
- 空間的な階層にはエレベーションシステムを適用する
- 状態の変化に意味のある動きを取り入れる
決して行わないこと
- トークンの代わりに色の値をハードコードする
- 無効状態を無視する
- トーナルパレット以外のカスタムカラーを作成する
- キーボードユーザー向けのフォーカス状態を省略する
- 目的なくエレベーションを使用する
- 意味を伝えない動きを追加する
- タッチターゲットの最小サイズに違反する
優先すること
- 主要なアクションには塗りつぶしボタン、三次アクションにはテキストボタンを使用する
- 一貫性があり調和のとれた色にはトーナルパレットを使用する
- ほとんどのトランジションには標準のイージングを使用する
- オーバーレイには不透明度ではなくサーフェスの色のバリエーションを使用する
- カスタム実装よりコンポーネントライブラリを使用する
- 固定ブレークポイントよりアダプティブレイアウトを使用する
メンタルモデル
Material Design では、次の点を確認します。
- Material らしいか? 奥行きのある物理的な表面のように振る舞いますか?
- 意図的か? すべての要素に明確な目的がありますか?
- 意味があるか? 動きが状態を伝え、注意を導きますか?
- アクセシブルか? 能力にかかわらず、誰もが利用できますか?
- アダプティブか? あらゆる画面サイズやデバイスで動作しますか?
- 心躍るか? ポジティブな感情を生み出しますか?
特徴的なマテリアルの動き
- ダイナミックカラー:ユーザーコンテンツに基づくパーソナライズされたテーマ
- トーナルパレット:単一の色源から生まれる調和のとれたカラー
- ステートレイヤー:オーバーレイによる一貫したインタラクションフィードバック
- コンテナートランスフォーム:要素から画面へのシームレスな遷移
- エレベーション階層:重要度を伝えるシャドウ
- アダプティブスキャフォールド:画面サイズに応じて変化するナビゲーション
- タイポグラフィスケール:コンテンツの明確な階層を示す15種類のスタイル
- シェイプシステム:コンポーネントの個性を伝える角の形状
コメント
Markdown 対応