Checkbox
ユーザーがセットから1つ以上のオプションを選択できるコントロール。
🤖 AI Implementation Guideデモ
ネイティブ HTML
ネイティブ HTML を優先
このカスタムコンポーネントを使用する前に、ネイティブの <input type="checkbox"> 要素の使用を検討してください。
ネイティブ要素は組み込みのアクセシビリティを提供し、JavaScript なしで動作し、ARIA 属性を必要としません。
<label>
<input type="checkbox" name="agree" />
I agree to the terms
</label> カスタム実装は、ネイティブ要素では提供できないカスタムスタイリングが必要な場合、またはチェックボックスグループの複雑な不確定状態管理が必要な場合にのみ使用してください。
| ユースケース | ネイティブ HTML | カスタム実装 |
|---|---|---|
| 基本的なフォーム入力 | 推奨 | 不要 |
| JavaScript 無効時のサポート | ネイティブで動作 | フォールバックが必要 |
| 不確定(混在)状態 | JS プロパティのみ* | 完全に制御可能 |
| カスタムスタイリング | 制限あり(ブラウザ依存) | 完全に制御可能 |
| フォーム送信 | 組み込み | hidden input が必要 |
*ネイティブの indeterminate は JavaScript プロパティであり、HTML 属性ではありません。宣言的に設定することはできません。
アクセシビリティ
WAI-ARIA ロール
| ロール | 要素 | 説明 |
|---|---|---|
checkbox | <input type="checkbox"> または role="checkbox" を持つ要素
|
要素をチェックボックスとして識別します。ネイティブの <input type="checkbox"> は このロールを暗黙的に持ちます。
|
この実装ではネイティブの <input type="checkbox"> を使用しており、checkbox ロールを暗黙的に提供します。<div> や <button> を使用したカスタム実装では、明示的に
role="checkbox" が必要です。
WAI-ARIA ステート
aria-checked / checked
チェックボックスの現在のチェック状態を示します。すべてのチェックボックス実装で必須です。
| 値 | true | false | mixed (不確定状態の場合)
|
| 必須 | はい |
| ネイティブ HTML | checked プロパティ(暗黙的な aria-checked)
|
| カスタム ARIA | aria-checked="true|false|mixed" |
| 変更トリガー | クリック、Space |
indeterminate(ネイティブプロパティ)
混合状態を示します。通常、一部のアイテムが選択されている場合の「すべて選択」チェックボックスで使用されます。
| 値 | true | false |
| 必須 | いいえ(混合状態の場合のみ) |
| 注意 | JavaScriptプロパティのみ、HTML属性ではありません |
| 動作 | ユーザー操作時に自動的にクリアされます |
disabled(ネイティブ属性)
チェックボックスがインタラクティブでなく、変更できないことを示します。
| 値 | 存在 | 不在 |
| 必須 | いいえ(無効化時のみ) |
| 効果 | タブ順序から除外され、入力を無視します |
キーボードサポート
| キー | アクション |
|---|---|
| Space | チェックボックスの状態を切り替える(チェック/未チェック) |
| Tab | 次のフォーカス可能な要素にフォーカスを移動 |
| Shift + Tab | 前のフォーカス可能な要素にフォーカスを移動 |
注意: Switchパターンとは異なり、Enterキーではチェックボックスが切り替わりません。
アクセシブルな名前
チェックボックスにはアクセシブルな名前が必要です。これは以下の方法で提供できます:
- label要素(推奨) -
<label>をfor属性で使用するか、inputをラップします -
aria-label- チェックボックスに非表示のラベルを提供します -
aria-labelledby- 外部要素をラベルとして参照します
ビジュアルデザイン
この実装は、色のみに依存せずに状態を示すことで、WCAG 1.4.1(色の使用)に準拠しています:
- チェックマークアイコン - チェック時に表示
- ダッシュ/マイナスアイコン - 不確定状態時に表示
- 空のボックス - 未チェック時に表示
- 強制カラーモード - Windowsハイコントラストモードでのアクセシビリティのためにシステムカラーを使用
参考資料
ソースコード
---
/**
* APG Checkbox Pattern - Astro Implementation
*
* A control that allows users to select one or more options.
* Uses native input[type="checkbox"] with Web Components for enhanced interactivity.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/
*/
export interface Props {
/** Initial checked state */
initialChecked?: boolean;
/** Indeterminate (mixed) state */
indeterminate?: boolean;
/** Whether the checkbox is disabled */
disabled?: boolean;
/** Form field name */
name?: string;
/** Form field value */
value?: string;
/** Checkbox id for label association */
id?: string;
/** Additional CSS class */
class?: string;
}
const {
initialChecked = false,
indeterminate = false,
disabled = false,
name,
value,
id,
class: className = '',
} = Astro.props;
---
<apg-checkbox
class={`apg-checkbox ${className}`.trim()}
data-indeterminate={indeterminate ? 'true' : undefined}
>
<input
type="checkbox"
id={id}
class="apg-checkbox-input"
checked={initialChecked}
disabled={disabled}
name={name}
value={value}
/>
<span class="apg-checkbox-control" aria-hidden="true">
<span class="apg-checkbox-icon apg-checkbox-icon--check">
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</span>
<span class="apg-checkbox-icon apg-checkbox-icon--indeterminate">
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 6H9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
</svg>
</span>
</span>
</apg-checkbox>
<script>
class ApgCheckbox extends HTMLElement {
private input: HTMLInputElement | null = null;
private rafId: number | null = null;
connectedCallback() {
this.rafId = requestAnimationFrame(() => this.initialize());
}
private initialize() {
this.rafId = null;
this.input = this.querySelector('input[type="checkbox"]');
if (!this.input) {
console.warn('apg-checkbox: input element not found');
return;
}
// Set initial indeterminate state if specified
if (this.dataset.indeterminate === 'true') {
this.input.indeterminate = true;
}
this.input.addEventListener('change', this.handleChange);
}
disconnectedCallback() {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.input?.removeEventListener('change', this.handleChange);
this.input = null;
}
private handleChange = (event: Event) => {
const target = event.target as HTMLInputElement;
// Clear indeterminate on user interaction
if (target.indeterminate) {
target.indeterminate = false;
}
this.dispatchEvent(
new CustomEvent('checkedchange', {
detail: { checked: target.checked },
bubbles: true,
})
);
};
}
if (!customElements.get('apg-checkbox')) {
customElements.define('apg-checkbox', ApgCheckbox);
}
</script> 使い方
---
import Checkbox from './Checkbox.astro';
---
<form>
<!-- With wrapping label -->
<label class="inline-flex items-center gap-2">
<Checkbox name="terms" />
I agree to the terms and conditions
</label>
<!-- With separate label -->
<label for="newsletter">Subscribe to newsletter</label>
<Checkbox id="newsletter" name="newsletter" initialChecked={true} />
<!-- Indeterminate state for "select all" -->
<label class="inline-flex items-center gap-2">
<Checkbox indeterminate />
Select all items
</label>
</form>
<script>
// Listen for change events
document.querySelectorAll('apg-checkbox').forEach((checkbox) => {
checkbox.addEventListener('checkedchange', (e) => {
console.log('Checked:', e.detail.checked);
});
});
</script> API
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
initialChecked | boolean | false | 初期のチェック状態 |
indeterminate | boolean | false | チェックボックスが不確定(混在)状態かどうか |
disabled | boolean | false | チェックボックスが無効かどうか |
name | string | - | フォームフィールド名 |
value | string | - | フォームフィールド値 |
id | string | - | 外部ラベルとの関連付け用ID |
class | string | "" | 追加のCSSクラス |
カスタムイベント
| イベント | 詳細 | 説明 |
|---|---|---|
checkedchange | { checked: boolean } | チェックボックスの状態が変更されたときに発火 |
テスト
テストは、キーボード操作、ARIA属性、アクセシビリティ要件全般にわたってAPG準拠を検証します。Checkboxコンポーネントは2層テスト戦略を採用しています。
テスト戦略
ユニットテスト (Container API)
Astro Container APIを使用してコンポーネントのHTML出力を検証します。ブラウザを必要とせずに正しいテンプレートレンダリングを確認できます。
- HTML構造と要素の階層
- 初期属性値(checked、disabled、indeterminate)
- フォーム連携属性(name、value、id)
- CSSクラスの適用
E2Eテスト (Playwright)
実際のブラウザ環境でWeb Componentの動作を検証します。JavaScript実行が必要なインタラクションをカバーします。
- クリック・キーボード操作
- カスタムイベントのディスパッチ(checkedchange)
- ユーザー操作によるindeterminate状態のクリア
- ラベル関連付けとクリック動作
- フォーカス管理とタブナビゲーション
テストカテゴリ
高優先度: HTML構造 (Unit)
| テスト | 説明 |
|---|---|
input type | type="checkbox"のinputをレンダリング |
checked attribute | checked属性がinitialChecked propを反映 |
disabled attribute | disabled propがtrueのときdisabled属性が設定される |
data-indeterminate | indeterminate状態用のdata属性が設定される |
control aria-hidden | 視覚的コントロール要素にaria-hidden="true"が設定される |
高優先度: キーボード操作 (E2E)
| テスト | 説明 |
|---|---|
Space key | チェックボックスの状態を切り替える |
Tab navigation | Tabでチェックボックス間のフォーカスを移動 |
Disabled Tab skip | 無効なチェックボックスはTab順序でスキップされる |
Disabled key ignore | 無効なチェックボックスはキー入力を無視する |
注意: Switchパターンとは異なり、Enterキーではチェックボックスが切り替わりません。
高優先度: クリック操作 (E2E)
| テスト | 説明 |
|---|---|
checked toggle | クリックでチェック状態を切り替える |
disabled click | 無効なチェックボックスはクリック操作を防ぐ |
indeterminate clear | ユーザー操作でindeterminate状態がクリアされる |
checkedchange event | 正しいdetailでカスタムイベントがディスパッチされる |
中優先度: フォーム連携 (Unit)
| テスト | 説明 |
|---|---|
name attribute | フォームのname属性がレンダリングされる |
value attribute | フォームのvalue属性がレンダリングされる |
id attribute | ラベル関連付けのためにID属性が正しく設定される |
中優先度: ラベル関連付け (E2E)
| テスト | 説明 |
|---|---|
Label click | 外部ラベルをクリックするとチェックボックスが切り替わる |
Wrapping label | ラップするラベルをクリックするとチェックボックスが切り替わる |
低優先度: CSSクラス (Unit)
| テスト | 説明 |
|---|---|
default class | apg-checkboxクラスがラッパーに適用される |
custom class | カスタムクラスがコンポーネントクラスとマージされる |
テストツール
- Vitest (opens in new tab) - ユニットテスト用テストランナー
- Astro Container API (opens in new tab) - ユニットテスト用サーバーサイドコンポーネントレンダリング
- Playwright (opens in new tab) - E2Eテスト用ブラウザ自動化
- Testing Library (opens in new tab) - フレームワーク固有のテストユーティリティ(React、Vue、Svelte)
完全なドキュメントについては、 testing-strategy.md (opens in new tab) を参照してください。
/**
* Checkbox Astro Component Tests using Container API
*
* These tests verify the actual Checkbox.astro component output using Astro's Container API.
* This ensures the component renders correct HTML structure and attributes.
*
* Note: Web Component behavior tests (click interaction, event dispatching) require
* E2E testing with Playwright as they need a real browser environment.
*
* @see https://docs.astro.build/en/reference/container-reference/
*/
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { describe, it, expect, beforeEach } from 'vitest';
import { JSDOM } from 'jsdom';
import Checkbox from './Checkbox.astro';
describe('Checkbox (Astro Container API)', () => {
let container: AstroContainer;
beforeEach(async () => {
container = await AstroContainer.create();
});
// Helper to render and parse HTML
async function renderCheckbox(
props: {
initialChecked?: boolean;
indeterminate?: boolean;
disabled?: boolean;
name?: string;
value?: string;
id?: string;
class?: string;
} = {}
): Promise<Document> {
const html = await container.renderToString(Checkbox, { props });
const dom = new JSDOM(html);
return dom.window.document;
}
// 🔴 High Priority: HTML Structure
describe('HTML Structure', () => {
it('renders apg-checkbox custom element wrapper', async () => {
const doc = await renderCheckbox();
const wrapper = doc.querySelector('apg-checkbox');
expect(wrapper).not.toBeNull();
});
it('renders input with type="checkbox"', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input[type="checkbox"]');
expect(input).not.toBeNull();
});
it('renders checkbox control span with aria-hidden', async () => {
const doc = await renderCheckbox();
const control = doc.querySelector('.apg-checkbox-control');
expect(control).not.toBeNull();
expect(control?.getAttribute('aria-hidden')).toBe('true');
});
it('renders check icon inside control', async () => {
const doc = await renderCheckbox();
const checkIcon = doc.querySelector('.apg-checkbox-icon--check');
expect(checkIcon).not.toBeNull();
expect(checkIcon?.querySelector('svg')).not.toBeNull();
});
it('renders indeterminate icon inside control', async () => {
const doc = await renderCheckbox();
const indeterminateIcon = doc.querySelector('.apg-checkbox-icon--indeterminate');
expect(indeterminateIcon).not.toBeNull();
expect(indeterminateIcon?.querySelector('svg')).not.toBeNull();
});
});
// 🔴 High Priority: Checked State
describe('Checked State', () => {
it('renders unchecked by default', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
expect(input?.hasAttribute('checked')).toBe(false);
});
it('renders checked when initialChecked is true', async () => {
const doc = await renderCheckbox({ initialChecked: true });
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('checked')).toBe(true);
});
});
// 🔴 High Priority: Disabled State
describe('Disabled State', () => {
it('renders without disabled attribute by default', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('disabled')).toBe(false);
});
it('renders with disabled attribute when disabled is true', async () => {
const doc = await renderCheckbox({ disabled: true });
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('disabled')).toBe(true);
});
});
// 🟡 Medium Priority: Indeterminate State
describe('Indeterminate State', () => {
it('does not have data-indeterminate by default', async () => {
const doc = await renderCheckbox();
const wrapper = doc.querySelector('apg-checkbox');
expect(wrapper?.hasAttribute('data-indeterminate')).toBe(false);
});
it('has data-indeterminate="true" when indeterminate is true', async () => {
const doc = await renderCheckbox({ indeterminate: true });
const wrapper = doc.querySelector('apg-checkbox');
expect(wrapper?.getAttribute('data-indeterminate')).toBe('true');
});
});
// 🟡 Medium Priority: Form Integration
describe('Form Integration', () => {
it('renders without name attribute by default', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('name')).toBe(false);
});
it('renders with name attribute when provided', async () => {
const doc = await renderCheckbox({ name: 'terms' });
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.getAttribute('name')).toBe('terms');
});
it('renders without value attribute by default', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('value')).toBe(false);
});
it('renders with value attribute when provided', async () => {
const doc = await renderCheckbox({ value: 'accepted' });
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.getAttribute('value')).toBe('accepted');
});
});
// 🟡 Medium Priority: Label Association
describe('Label Association', () => {
it('renders without id attribute by default', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('id')).toBe(false);
});
it('renders with id attribute when provided for external label association', async () => {
const doc = await renderCheckbox({ id: 'my-checkbox' });
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.getAttribute('id')).toBe('my-checkbox');
});
});
// 🟢 Low Priority: CSS Classes
describe('CSS Classes', () => {
it('has default apg-checkbox class on wrapper', async () => {
const doc = await renderCheckbox();
const wrapper = doc.querySelector('apg-checkbox');
expect(wrapper?.classList.contains('apg-checkbox')).toBe(true);
});
it('has apg-checkbox-input class on input', async () => {
const doc = await renderCheckbox();
const input = doc.querySelector('input');
expect(input?.classList.contains('apg-checkbox-input')).toBe(true);
});
it('appends custom class to wrapper', async () => {
const doc = await renderCheckbox({ class: 'custom-class' });
const wrapper = doc.querySelector('apg-checkbox');
expect(wrapper?.classList.contains('apg-checkbox')).toBe(true);
expect(wrapper?.classList.contains('custom-class')).toBe(true);
});
});
// 🟢 Low Priority: Combined States
describe('Combined States', () => {
it('renders checked and disabled together', async () => {
const doc = await renderCheckbox({ initialChecked: true, disabled: true });
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.hasAttribute('checked')).toBe(true);
expect(input?.hasAttribute('disabled')).toBe(true);
});
it('renders with all form attributes', async () => {
const doc = await renderCheckbox({
id: 'terms-checkbox',
name: 'terms',
value: 'accepted',
initialChecked: true,
});
const input = doc.querySelector('input[type="checkbox"]');
expect(input?.getAttribute('id')).toBe('terms-checkbox');
expect(input?.getAttribute('name')).toBe('terms');
expect(input?.getAttribute('value')).toBe('accepted');
expect(input?.hasAttribute('checked')).toBe(true);
});
});
}); リソース
- WAI-ARIA APG: Checkbox パターン (opens in new tab)
- MDN: <input type="checkbox"> (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist