CSS実践入門第10回:SEO・アクセシビリティ - 誰もが使いやすく検索エンジンに最適化されたWebサイト

SEO対策とアクセシビリティを考慮したWebサイト構築の実践的手法を解説。セマンティックHTML、構造化データ、ARIA属性、キーボードナビゲーションなど、包括的なWebアクセシビリティの実装方法を紹介します。

Webサイトの成功には、検索エンジンによる適切な理解と、すべてのユーザーが利用できるアクセシビリティが欠かせません。今回は、SEO対策とWebアクセシビリティを同時に実現する実践的な手法を詳しく解説し、包括的で持続可能なWeb開発のアプローチを紹介します。

この記事で学べること

  • セマンティックHTMLによる構造化されたマークアップ
  • ARIA属性を活用したアクセシブルなインターフェース
  • 構造化データ(JSON-LD)によるSEO最適化
  • キーボードナビゲーションとスクリーンリーダー対応
  • WCAG 2.1 AA準拠のアクセシビリティ実装

1. セマンティックHTMLの基礎

適切な要素の選択

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!-- セマンティックな文書構造 -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>技術ブログ - 最新のフロントエンド技術情報</title>
    <meta name="description" content="CSS、JavaScript、パフォーマンス最適化など、最新のフロントエンド技術情報をお届けします。初心者から上級者まで役立つ実践的な記事を多数掲載。">
    <link rel="canonical" href="https://example.com/">
</head>
<body>
    <!-- メインヘッダー -->
    <header role="banner">
        <nav aria-label="メインナビゲーション">
            <ul>
                <li><a href="/" aria-current="page">ホーム</a></li>
                <li><a href="/articles/">記事一覧</a></li>
                <li><a href="/about/">About</a></li>
            </ul>
        </nav>
    </header>

    <!-- メインコンテンツ -->
    <main role="main">
        <!-- ヒーローセクション -->
        <section aria-labelledby="hero-heading">
            <h1 id="hero-heading">最新のフロントエンド技術</h1>
            <p>実践的な技術記事で、あなたのスキルアップをサポートします</p>
        </section>

        <!-- 記事セクション -->
        <section aria-labelledby="articles-heading">
            <h2 id="articles-heading">新着記事</h2>
            
            <article>
                <header>
                    <h3><a href="/article/css-performance/">CSSパフォーマンス最適化</a></h3>
                    <p>
                        <time datetime="2025-09-13">2025年9月13日</time>
                        <span aria-hidden="true"></span>
                        <span>投稿者: <span>田中太郎</span></span>
                    </p>
                </header>
                
                <p>高速なWebサイトを実現するCSSの最適化手法について詳しく解説します...</p>
                
                <footer>
                    <p>
                        タグ: 
                        <a href="/tag/css/" rel="tag">CSS</a>                        <a href="/tag/performance/" rel="tag">パフォーマンス</a>
                    </p>
                </footer>
            </article>
        </section>
    </main>

    <!-- サイドバー -->
    <aside role="complementary" aria-labelledby="sidebar-heading">
        <h2 id="sidebar-heading">関連情報</h2>
        
        <!-- 人気記事 -->
        <section aria-labelledby="popular-heading">
            <h3 id="popular-heading">人気記事</h3>
            <nav aria-label="人気記事">
                <ol>
                    <li><a href="/article/1/">JavaScript入門ガイド</a></li>
                    <li><a href="/article/2/">レスポンシブデザインの基礎</a></li>
                    <li><a href="/article/3/">モダンCSS入門</a></li>
                </ol>
            </nav>
        </section>
    </aside>

    <!-- フッター -->
    <footer role="contentinfo">
        <p>&copy; 2025 技術ブログ. All rights reserved.</p>
        
        <!-- フッターナビゲーション -->
        <nav aria-label="フッターナビゲーション">
            <ul>
                <li><a href="/privacy/">プライバシーポリシー</a></li>
                <li><a href="/terms/">利用規約</a></li>
                <li><a href="/contact/">お問い合わせ</a></li>
            </ul>
        </nav>
    </footer>
</body>
</html>

見出し階層の適切な構造化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- 正しい見出し階層 -->
<article>
    <h1>CSS実践入門シリーズ</h1> <!-- メイン見出し -->
    
    <section>
        <h2>基礎編</h2> <!-- セクション見出し -->
        
        <section>
            <h3>セレクターの基本</h3> <!-- サブセクション -->
            
            <section>
                <h4>要素セレクター</h4> <!-- 詳細セクション -->
                <p>要素名を使ってスタイルを適用する方法...</p>
                
                <h4>クラスセレクター</h4>
                <p>クラス名を使ってスタイルを適用する方法...</p>
            </section>
        </section>
        
        <section>
            <h3>カスケードと継承</h3>
            <p>CSSの重要な概念について...</p>
        </section>
    </section>
    
    <section>
        <h2>応用編</h2>
        
        <section>
            <h3>レイアウト技術</h3>
            <p>Flexbox、Grid、Position について...</p>
        </section>
    </section>
</article>

2. ARIA属性によるアクセシビリティ向上

ランドマークとナビゲーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!-- ARIA ランドマークロール -->
<div role="banner"><!-- ヘッダー領域 --></div>
<div role="navigation"><!-- ナビゲーション領域 --></div>
<div role="main"><!-- メインコンテンツ領域 --></div>
<div role="complementary"><!-- サイドバー領域 --></div>
<div role="contentinfo"><!-- フッター領域 --></div>
<div role="search"><!-- 検索領域 --></div>

<!-- ARIA ラベルによる説明 -->
<nav aria-label="メインナビゲーション">
    <ul>
        <li><a href="/" aria-current="page">ホーム</a></li>
        <li>
            <a href="/articles/" aria-expanded="false" aria-haspopup="true">記事</a>
            <ul aria-hidden="true">
                <li><a href="/articles/css/">CSS</a></li>
                <li><a href="/articles/javascript/">JavaScript</a></li>
                <li><a href="/articles/performance/">パフォーマンス</a></li>
            </ul>
        </li>
    </ul>
</nav>

<!-- 検索フォーム -->
<form role="search" aria-label="サイト内検索">
    <div>
        <label for="search-input">検索キーワード</label>
        <input 
            type="search" 
            id="search-input"
            name="q"
            aria-describedby="search-help"
            placeholder="記事を検索..."
            required>
        <div id="search-help">記事のタイトルや内容から検索できます</div>
    </div>
    
    <button type="submit" aria-label="検索を実行">
        <span aria-hidden="true">🔍</span>
        検索
    </button>
</form>

インタラクティブ要素のアクセシビリティ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!-- アコーディオンメニュー -->
<div class="accordion">
    <h3>
        <button 
            type="button"
            aria-expanded="false"
            aria-controls="panel-1"
            id="accordion-header-1"
            class="accordion-trigger">
            CSSの基礎
        </button>
    </h3>
    
    <div 
        id="panel-1"
        role="region"
        aria-labelledby="accordion-header-1"
        class="accordion-panel"
        hidden>
        <p>CSSの基本的な使い方について説明します...</p>
    </div>
</div>

<!-- タブインターフェース -->
<div class="tabs" role="tablist" aria-label="記事カテゴリ">
    <button 
        role="tab"
        aria-selected="true"
        aria-controls="panel-css"
        id="tab-css"
        tabindex="0">
        CSS
    </button>
    
    <button 
        role="tab"
        aria-selected="false"
        aria-controls="panel-js"
        id="tab-js"
        tabindex="-1">
        JavaScript
    </button>
</div>

<div 
    role="tabpanel"
    id="panel-css"
    aria-labelledby="tab-css"
    tabindex="0">
    <h3>CSS記事一覧</h3>
    <!-- CSS記事コンテンツ -->
</div>

<div 
    role="tabpanel"
    id="panel-js"
    aria-labelledby="tab-js"
    tabindex="0"
    hidden>
    <h3>JavaScript記事一覧</h3>
    <!-- JavaScript記事コンテンツ -->
</div>

<!-- モーダルダイアログ -->
<div 
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    aria-describedby="modal-description"
    class="modal"
    hidden>
    
    <div class="modal-content">
        <header>
            <h2 id="modal-title">記事を共有</h2>
            <button 
                type="button"
                aria-label="モーダルを閉じる"
                class="modal-close">
                ×
            </button>
        </header>
        
        <div id="modal-description">
            <p>この記事をSNSで共有できます</p>
            <!-- 共有ボタン -->
        </div>
    </div>
</div>

3. フォームのアクセシビリティ

包括的なフォーム設計

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<form action="/contact" method="post" novalidate>
    <fieldset>
        <legend>お問い合わせ情報</legend>
        
        <!-- 必須フィールド -->
        <div class="form-group">
            <label for="name" class="required">
                お名前
                <span aria-label="必須">*</span>
            </label>
            <input 
                type="text"
                id="name"
                name="name"
                aria-describedby="name-error name-help"
                aria-required="true"
                aria-invalid="false"
                autocomplete="name"
                required>
            <div id="name-help" class="help-text">
                フルネームでご入力ください
            </div>
            <div id="name-error" class="error-text" aria-live="polite"></div>
        </div>
        
        <!-- メールアドレス -->
        <div class="form-group">
            <label for="email" class="required">
                メールアドレス
                <span aria-label="必須">*</span>
            </label>
            <input 
                type="email"
                id="email"
                name="email"
                aria-describedby="email-error email-help"
                aria-required="true"
                aria-invalid="false"
                autocomplete="email"
                required>
            <div id="email-help" class="help-text">
                返信に使用させていただきます
            </div>
            <div id="email-error" class="error-text" aria-live="polite"></div>
        </div>
        
        <!-- 選択フィールド -->
        <div class="form-group">
            <label for="category">お問い合わせ種別</label>
            <select id="category" name="category" aria-describedby="category-help">
                <option value="">選択してください</option>
                <option value="general">一般的なお問い合わせ</option>
                <option value="technical">技術的なご質問</option>
                <option value="business">ビジネスのご相談</option>
            </select>
            <div id="category-help" class="help-text">
                該当するカテゴリを選択してください
            </div>
        </div>
        
        <!-- ラジオボタン -->
        <fieldset class="form-group">
            <legend>返信方法</legend>
            <div class="radio-group">
                <input type="radio" id="reply-email" name="reply-method" value="email" checked>
                <label for="reply-email">メール</label>
                
                <input type="radio" id="reply-phone" name="reply-method" value="phone">
                <label for="reply-phone">電話</label>
            </div>
        </fieldset>
        
        <!-- チェックボックス -->
        <div class="form-group">
            <div class="checkbox-group">
                <input 
                    type="checkbox"
                    id="newsletter"
                    name="newsletter"
                    aria-describedby="newsletter-help">
                <label for="newsletter">
                    ニュースレターの配信を希望する
                </label>
            </div>
            <div id="newsletter-help" class="help-text">
                月1回程度、最新記事の情報をお届けします
            </div>
        </div>
        
        <!-- テキストエリア -->
        <div class="form-group">
            <label for="message" class="required">
                メッセージ
                <span aria-label="必須">*</span>
            </label>
            <textarea 
                id="message"
                name="message"
                rows="5"
                aria-describedby="message-error message-help"
                aria-required="true"
                aria-invalid="false"
                required></textarea>
            <div id="message-help" class="help-text">
                具体的にご記入いただけると、より適切にお答えできます
            </div>
            <div id="message-error" class="error-text" aria-live="polite"></div>
        </div>
    </fieldset>
    
    <!-- 送信ボタン -->
    <div class="form-actions">
        <button type="submit" class="btn-primary">
            送信する
        </button>
        <button type="reset" class="btn-secondary">
            リセット
        </button>
    </div>
    
    <!-- エラーサマリー -->
    <div id="error-summary" class="error-summary" aria-live="assertive" hidden>
        <h3>入力内容に問題があります</h3>
        <ul id="error-list"></ul>
    </div>
</form>

フォームバリデーションのアクセシビリティ

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
class AccessibleFormValidator {
    constructor(form) {
        this.form = form;
        this.errors = new Map();
        this.setupEventListeners();
    }
    
    setupEventListeners() {
        // リアルタイムバリデーション
        this.form.addEventListener('input', (e) => {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
                this.validateField(e.target);
            }
        });
        
        // 送信時バリデーション
        this.form.addEventListener('submit', (e) => {
            if (!this.validateForm()) {
                e.preventDefault();
                this.showErrorSummary();
                this.focusFirstError();
            }
        });
    }
    
    validateField(field) {
        const errors = [];
        
        // 必須チェック
        if (field.hasAttribute('required') && !field.value.trim()) {
            errors.push(`${this.getFieldLabel(field)}は必須です`);
        }
        
        // メールアドレス形式チェック
        if (field.type === 'email' && field.value) {
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            if (!emailRegex.test(field.value)) {
                errors.push('有効なメールアドレスを入力してください');
            }
        }
        
        // エラー状態の更新
        this.updateFieldError(field, errors);
    }
    
    updateFieldError(field, errors) {
        const errorContainer = document.getElementById(`${field.id}-error`);
        
        if (errors.length > 0) {
            this.errors.set(field.id, errors);
            field.setAttribute('aria-invalid', 'true');
            
            if (errorContainer) {
                errorContainer.textContent = errors[0];
                errorContainer.style.display = 'block';
            }
        } else {
            this.errors.delete(field.id);
            field.setAttribute('aria-invalid', 'false');
            
            if (errorContainer) {
                errorContainer.textContent = '';
                errorContainer.style.display = 'none';
            }
        }
    }
    
    validateForm() {
        const fields = this.form.querySelectorAll('input, textarea, select');
        let isValid = true;
        
        fields.forEach(field => {
            this.validateField(field);
            if (this.errors.has(field.id)) {
                isValid = false;
            }
        });
        
        return isValid;
    }
    
    showErrorSummary() {
        const errorSummary = document.getElementById('error-summary');
        const errorList = document.getElementById('error-list');
        
        if (!errorSummary || !errorList) return;
        
        // エラーリストを生成
        errorList.innerHTML = '';
        for (const [fieldId, fieldErrors] of this.errors) {
            const field = document.getElementById(fieldId);
            const label = this.getFieldLabel(field);
            
            fieldErrors.forEach(error => {
                const li = document.createElement('li');
                const link = document.createElement('a');
                link.href = `#${fieldId}`;
                link.textContent = `${label}: ${error}`;
                link.addEventListener('click', (e) => {
                    e.preventDefault();
                    field.focus();
                });
                li.appendChild(link);
                errorList.appendChild(li);
            });
        }
        
        errorSummary.hidden = false;
        errorSummary.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
    
    focusFirstError() {
        const firstErrorField = this.form.querySelector('[aria-invalid="true"]');
        if (firstErrorField) {
            firstErrorField.focus();
        }
    }
    
    getFieldLabel(field) {
        const label = this.form.querySelector(`label[for="${field.id}"]`);
        return label ? label.textContent.replace('*', '').trim() : field.name;
    }
}

// 初期化
document.addEventListener('DOMContentLoaded', () => {
    const forms = document.querySelectorAll('form[novalidate]');
    forms.forEach(form => new AccessibleFormValidator(form));
});

4. 構造化データによるSEO最適化

JSON-LD による構造化データ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "WebSite",
    "name": "技術ブログ",
    "description": "フロントエンド技術に関する最新情報を発信するブログ",
    "url": "https://example.com",
    "potentialAction": {
        "@type": "SearchAction",
        "target": "https://example.com/search?q={search_term_string}",
        "query-input": "required name=search_term_string"
    },
    "publisher": {
        "@type": "Organization",
        "name": "技術ブログ運営チーム",
        "logo": {
            "@type": "ImageObject",
            "url": "https://example.com/images/logo.png"
        }
    }
}
</script>

<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "CSS実践入門第10回:SEO・アクセシビリティ",
    "description": "SEO対策とアクセシビリティを考慮したWebサイト構築の実践的手法を解説",
    "image": "https://example.com/images/css-seo-accessibility.jpg",
    "author": {
        "@type": "Person",
        "name": "田中太郎",
        "url": "https://example.com/author/tanaka/"
    },
    "publisher": {
        "@type": "Organization",
        "name": "技術ブログ",
        "logo": {
            "@type": "ImageObject",
            "url": "https://example.com/images/logo.png"
        }
    },
    "datePublished": "2025-09-13T20:30:00+09:00",
    "dateModified": "2025-09-13T20:30:00+09:00",
    "mainEntityOfPage": {
        "@type": "WebPage",
        "@id": "https://example.com/article/css-seo-accessibility/"
    },
    "keywords": ["CSS", "SEO", "アクセシビリティ", "ARIA", "構造化データ"],
    "articleSection": "CSS実践入門",
    "wordCount": 8000,
    "speakable": {
        "@type": "SpeakableSpecification",
        "cssSelector": ["h1", "h2", "p"]
    }
}
</script>

<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "ホーム",
            "item": "https://example.com/"
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": "CSS実践入門",
            "item": "https://example.com/series/css-introduction/"
        },
        {
            "@type": "ListItem",
            "position": 3,
            "name": "第10回:SEO・アクセシビリティ",
            "item": "https://example.com/article/css-seo-accessibility/"
        }
    ]
}
</script>

パンくずナビゲーションの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- パンくずナビゲーション -->
<nav aria-label="パンくずナビゲーション">
    <ol class="breadcrumb" vocab="https://schema.org/" typeof="BreadcrumbList">
        <li property="itemListElement" typeof="ListItem">
            <a property="item" typeof="WebPage" href="/">
                <span property="name">ホーム</span>
            </a>
            <meta property="position" content="1">
        </li>
        <li property="itemListElement" typeof="ListItem">
            <a property="item" typeof="WebPage" href="/series/css-introduction/">
                <span property="name">CSS実践入門</span>
            </a>
            <meta property="position" content="2">
        </li>
        <li property="itemListElement" typeof="ListItem" aria-current="page">
            <span property="name">第10回:SEO・アクセシビリティ</span>
            <meta property="position" content="3">
        </li>
    </ol>
</nav>

5. キーボードナビゲーション

完全キーボード対応のコンポーネント

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* フォーカスの可視化 */
:focus {
    outline: 2px solid #4285f4;
    outline-offset: 2px;
}

/* カスタムフォーカススタイル */
.custom-focus:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.5);
    border-color: #4285f4;
}

/* スキップリンク */
.skip-link {
    position: absolute;
    top: -40px;
    left: 6px;
    background: #000;
    color: #fff;
    padding: 8px;
    text-decoration: none;
    border-radius: 0 0 4px 4px;
    z-index: 1000;
    transition: top 0.3s;
}

.skip-link:focus {
    top: 0;
}

/* キーボードナビゲーション専用スタイル */
body:not(.mouse-navigation) .focusable:focus {
    outline: 2px solid #4285f4;
    outline-offset: 2px;
}

/* タブパネルのフォーカス管理 */
.tab-panel:focus {
    outline: 1px dotted #666;
    outline-offset: -1px;
}

/* モーダルのフォーカストラップ */
.modal[aria-hidden="false"] {
    display: flex;
}

.modal[aria-hidden="true"] {
    display: none;
}

キーボードナビゲーションのJavaScript実装

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
class KeyboardNavigationManager {
    constructor() {
        this.isMouseNavigation = true;
        this.focusableElements = 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])';
        
        this.setupEventListeners();
    }
    
    setupEventListeners() {
        // マウス使用の検出
        document.addEventListener('mousedown', () => {
            this.isMouseNavigation = true;
            document.body.classList.add('mouse-navigation');
            document.body.classList.remove('keyboard-navigation');
        });
        
        // キーボード使用の検出
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Tab') {
                this.isMouseNavigation = false;
                document.body.classList.add('keyboard-navigation');
                document.body.classList.remove('mouse-navigation');
            }
            
            // ESCキーでモーダルを閉じる
            if (e.key === 'Escape') {
                this.closeModal();
            }
        });
        
        // スキップリンクの処理
        this.setupSkipLinks();
        
        // ドロップダウンメニューのキーボード操作
        this.setupDropdownNavigation();
        
        // タブインターフェースのキーボード操作
        this.setupTabNavigation();
    }
    
    setupSkipLinks() {
        const skipLinks = document.querySelectorAll('.skip-link');
        
        skipLinks.forEach(link => {
            link.addEventListener('click', (e) => {
                e.preventDefault();
                const targetId = link.getAttribute('href').substring(1);
                const target = document.getElementById(targetId);
                
                if (target) {
                    target.tabIndex = -1;
                    target.focus();
                    target.scrollIntoView({ behavior: 'smooth', block: 'start' });
                }
            });
        });
    }
    
    setupDropdownNavigation() {
        const dropdownTriggers = document.querySelectorAll('[aria-haspopup="true"]');
        
        dropdownTriggers.forEach(trigger => {
            trigger.addEventListener('keydown', (e) => {
                const dropdown = document.getElementById(trigger.getAttribute('aria-controls'));
                
                switch (e.key) {
                    case 'Enter':
                    case ' ':
                    case 'ArrowDown':
                        e.preventDefault();
                        this.openDropdown(trigger, dropdown);
                        break;
                    case 'Escape':
                        this.closeDropdown(trigger, dropdown);
                        break;
                }
            });
        });
    }
    
    openDropdown(trigger, dropdown) {
        trigger.setAttribute('aria-expanded', 'true');
        dropdown.removeAttribute('aria-hidden');
        
        // 最初のメニュー項目にフォーカス
        const firstItem = dropdown.querySelector('a, button');
        if (firstItem) {
            firstItem.focus();
        }
        
        // メニュー内のキーボード操作
        this.setupMenuNavigation(dropdown, trigger);
    }
    
    closeDropdown(trigger, dropdown) {
        trigger.setAttribute('aria-expanded', 'false');
        dropdown.setAttribute('aria-hidden', 'true');
        trigger.focus();
    }
    
    setupMenuNavigation(menu, trigger) {
        const menuItems = menu.querySelectorAll('a, button');
        let currentIndex = 0;
        
        const keyHandler = (e) => {
            switch (e.key) {
                case 'ArrowDown':
                    e.preventDefault();
                    currentIndex = (currentIndex + 1) % menuItems.length;
                    menuItems[currentIndex].focus();
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    currentIndex = currentIndex === 0 ? menuItems.length - 1 : currentIndex - 1;
                    menuItems[currentIndex].focus();
                    break;
                case 'Escape':
                    this.closeDropdown(trigger, menu);
                    menu.removeEventListener('keydown', keyHandler);
                    break;
                case 'Tab':
                    this.closeDropdown(trigger, menu);
                    menu.removeEventListener('keydown', keyHandler);
                    break;
            }
        };
        
        menu.addEventListener('keydown', keyHandler);
    }
    
    setupTabNavigation() {
        const tabLists = document.querySelectorAll('[role="tablist"]');
        
        tabLists.forEach(tabList => {
            const tabs = tabList.querySelectorAll('[role="tab"]');
            let currentTab = 0;
            
            tabs.forEach((tab, index) => {
                tab.addEventListener('keydown', (e) => {
                    switch (e.key) {
                        case 'ArrowLeft':
                            e.preventDefault();
                            currentTab = index === 0 ? tabs.length - 1 : index - 1;
                            this.activateTab(tabs[currentTab], tabs, tabList);
                            break;
                        case 'ArrowRight':
                            e.preventDefault();
                            currentTab = (index + 1) % tabs.length;
                            this.activateTab(tabs[currentTab], tabs, tabList);
                            break;
                        case 'Home':
                            e.preventDefault();
                            this.activateTab(tabs[0], tabs, tabList);
                            break;
                        case 'End':
                            e.preventDefault();
                            this.activateTab(tabs[tabs.length - 1], tabs, tabList);
                            break;
                    }
                });
                
                tab.addEventListener('click', () => {
                    this.activateTab(tab, tabs, tabList);
                });
            });
        });
    }
    
    activateTab(activeTab, allTabs, tabList) {
        // 全タブを非アクティブに
        allTabs.forEach(tab => {
            tab.setAttribute('aria-selected', 'false');
            tab.tabIndex = -1;
            
            const panelId = tab.getAttribute('aria-controls');
            const panel = document.getElementById(panelId);
            if (panel) {
                panel.hidden = true;
            }
        });
        
        // アクティブタブを設定
        activeTab.setAttribute('aria-selected', 'true');
        activeTab.tabIndex = 0;
        activeTab.focus();
        
        const activePanelId = activeTab.getAttribute('aria-controls');
        const activePanel = document.getElementById(activePanelId);
        if (activePanel) {
            activePanel.hidden = false;
        }
    }
    
    // フォーカストラップ(モーダル用)
    trapFocus(element) {
        const focusableElements = element.querySelectorAll(this.focusableElements);
        const firstElement = focusableElements[0];
        const lastElement = focusableElements[focusableElements.length - 1];
        
        const trapHandler = (e) => {
            if (e.key === 'Tab') {
                if (e.shiftKey) {
                    if (document.activeElement === firstElement) {
                        e.preventDefault();
                        lastElement.focus();
                    }
                } else {
                    if (document.activeElement === lastElement) {
                        e.preventDefault();
                        firstElement.focus();
                    }
                }
            }
        };
        
        element.addEventListener('keydown', trapHandler);
        return () => element.removeEventListener('keydown', trapHandler);
    }
    
    closeModal() {
        const modal = document.querySelector('[role="dialog"][aria-modal="true"]:not([hidden])');
        if (modal) {
            modal.hidden = true;
            modal.setAttribute('aria-hidden', 'true');
            
            // モーダルを開いた要素にフォーカスを戻す
            const trigger = document.querySelector('[aria-expanded="true"]');
            if (trigger) {
                trigger.focus();
            }
        }
    }
}

// 初期化
document.addEventListener('DOMContentLoaded', () => {
    new KeyboardNavigationManager();
});

6. カラーコントラストとレスポンシブテキスト

WCAG準拠のカラーシステム

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
:root {
    /* WCAG AA準拠のカラーパレット */
    --color-text-primary: #212529;        /* コントラスト比 16.9:1 */
    --color-text-secondary: #495057;      /* コントラスト比 10.7:1 */
    --color-text-muted: #6c757d;          /* コントラスト比 7.0:1 */
    
    --color-link: #0066cc;                /* コントラスト比 8.6:1 */
    --color-link-hover: #004c99;          /* コントラスト比 11.4:1 */
    --color-link-visited: #551a8b;        /* コントラスト比 9.2:1 */
    
    --color-success: #155724;             /* コントラスト比 10.4:1 */
    --color-success-bg: #d4edda;          /* 背景色との組み合わせ */
    
    --color-warning: #856404;             /* コントラスト比 8.1:1 */
    --color-warning-bg: #fff3cd;
    
    --color-danger: #721c24;              /* コントラスト比 9.8:1 */
    --color-danger-bg: #f8d7da;
    
    --color-info: #0c5460;                /* コントラスト比 8.9:1 */
    --color-info-bg: #d1ecf1;
}

/* 高コントラストモード対応 */
@media (prefers-contrast: high) {
    :root {
        --color-text-primary: #000000;
        --color-background: #ffffff;
        --color-border: #000000;
        --color-link: #0000ee;
    }
}

/* カラーコントラストユーティリティ */
.text-high-contrast {
    color: #000000;
    background-color: #ffffff;
}

.bg-high-contrast {
    background-color: #000000;
    color: #ffffff;
}

/* エラー表示のアクセシブルなスタイリング */
.error-text {
    color: var(--color-danger);
    font-weight: 600;
    position: relative;
}

.error-text::before {
    content: '⚠️';
    margin-right: 0.5rem;
    font-weight: normal;
}

/* フォーカス可能要素の十分なサイズ確保 */
button, .btn, input, textarea, select, a {
    min-height: 44px; /* WCAG推奨の最小タッチターゲットサイズ */
    min-width: 44px;
    padding: 0.5rem 1rem;
}

/* テキストのスケーラビリティ */
.scalable-text {
    font-size: clamp(1rem, 2.5vw, 1.25rem);
    line-height: 1.6;
}

/* 読みやすさの向上 */
.readable-content {
    max-width: 65ch; /* 1行の文字数を制限 */
    line-height: 1.6;
    word-spacing: 0.1em;
    letter-spacing: 0.02em;
}

まとめ

SEO対策とアクセシビリティは、現代のWeb開発において必須の要素であり、これらを適切に実装することで、より多くのユーザーにリーチし、検索エンジンからの評価も向上させることができます。

重要な実装ポイント

セマンティックHTML

  • 適切な要素の選択と階層構造
  • ランドマークロールによる文書構造の明確化
  • 見出しの論理的な順序付け

ARIA属性の活用

  • スクリーンリーダーユーザーへの情報提供
  • インタラクティブ要素の状態表現
  • 複雑なUIコンポーネントのアクセシビリティ確保

構造化データ

  • JSON-LDによる検索エンジンへの情報提供
  • リッチスニペットの表示促進
  • サイト構造の検索エンジンへの伝達

キーボードナビゲーション

  • 完全なキーボード操作対応
  • 適切なフォーカス管理
  • ショートカットキーの実装

視覚的アクセシビリティ

  • 十分なカラーコントラストの確保
  • スケーラブルなテキストサイズ
  • カラー以外の情報伝達手段

継続的な改善プロセス

  • 自動アクセシビリティテストツールの活用
  • 実際のユーザーテストの実施
  • 検索エンジンランキングの監視
  • Web Vitalsとアクセシビリティ指標の定期的な測定

これらの実践により、すべてのユーザーが利用しやすく、検索エンジンに最適化された持続可能なWebサイトを構築することができます。アクセシビリティとSEOは一度の実装で完結するものではなく、継続的な改善と最新の基準への対応が重要です。

CSS実践入門シリーズ完結

この10回にわたるシリーズを通して、基礎的なCSSから高度なフロントエンド技術まで、実践的な知識を体系的に学んでいただきました。これらの知識を基に、より良いWeb体験の創造に取り組んでいただければと思います。


関連記事:

技術ネタ、趣味や備忘録などを書いているブログです
Hugo で構築されています。
テーマ StackJimmy によって設計されています。