x
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
<a class="skip-link" href="#preview-main">Skip to main content</a><div class="tw:p-6 tw:max-w-7xl tw:mx-auto tw:space-y-8"> <!-- Introductory callout --> <div class="callout"> <p class="tw:mb-1 tw:text-sm tw:font-mono"> <strong>SKIP LINK:</strong> Keyboard bypass for repeated page chrome (WCAG 2.4.1, Level A)<br> <strong>COMPOSES:</strong> nothing — own classes<br> <strong>CLASSES:</strong> .skip-link | .skip-link-target </p> <p class="tw:mb-0 tw:text-sm"> Without one, a keyboard or switch user landing on a consumer page tabs through the header logo, the auth CTAs, the sticky four-vertical section nav, every hero-search segment and the whole filter rail before reaching a single result. The link is parked outside the viewport until it takes focus, then slides in at the top-left and jumps straight to the page's main region. </p> </div> <div class="inline-notification inline-notification-info"> <i class="fa-regular fa-keyboard inline-notification-icon" aria-hidden="true"></i> <div class="inline-notification-content"> <p class="inline-notification-title">This one is live — press Tab to see it</p> <p class="inline-notification-message tw:mb-0"> Click anywhere in this preview, then press <kbd class="badge badge-key">Tab</kbd> until focus reaches the top of the document. A navy “Skip to main content” chip appears at the top-left. Press <kbd class="badge badge-key">Enter</kbd> and focus lands on the “Main content starts here” region below; the next <kbd class="badge badge-key">Tab</kbd> goes to the first control inside it, not back to the top. Automated checkers cannot test any of this — see “Why the a11y gate can’t catch this”. </p> </div> </div> <!-- ============================================ Anatomy ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Anatomy — two halves of one contract</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>The link and its target ship together.</strong> A bypass link pointing at an <code>id</code> that doesn't exist is a dead control that passes inspection and fails the user, so <code>.skip-link</code> is opt-in per page: the shared top frame renders it only when the page passes <code>skip_to:</code>, and the page adds the matching id, <code>tabindex="-1"</code> and <code>.skip-link-target</code> to its <code><main></code> in the same edit. </p> </div> <div class="info-grid info-grid-bordered"> <div class="info-grid-label">1. The link</div> <div class="info-grid-value"> First focusable element in the DOM. Hidden by transform, not by <code>tw:sr-only</code> — it must stay in the tab order, and a 1px clip-rect box cannot be reliably un-hidden on focus. </div> <div class="info-grid-label">2. <code>tabindex="-1"</code></div> <div class="info-grid-value"> Makes a non-interactive landmark focusable. Without it, some browsers scroll to the anchor but leave focus in the header, so the next Tab drops the user back into the nav they just skipped. </div> <div class="info-grid-label">3. <code>.skip-link-target</code></div> <div class="info-grid-value"> Suppresses the browser's own <code>outline: auto</code>, which otherwise paints a ring around the entire main region — a full-viewport blue frame that reads as a rendering fault. </div> </div> <div class="tw:mt-4 tw:text-sm tw:text-gray-600"> <code><a class="skip-link" href="#main-content">Skip to main content</a></code><br> <code><main id="main-content" tabindex="-1" class="skip-link-target">…</main></code> </div> </div> <!-- ============================================ In the shared top frame ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Wiring a page</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>Two lines per page.</strong> <code>shared/_consumer_topframe_session</code> hosts the link because it is already the first thing every consumer preview renders. Pass <code>skip_to:</code>, then mark up the target. </p> </div> <div class="tw:text-sm tw:text-gray-600 tw:space-y-1"> <div><code><%= render "shared/consumer_topframe_session", active: :providers, logged_in: false, skip_to: "#main-content" %></code></div> <div><code><main id="main-content" tabindex="-1" class="skip-link-target tw:min-h-screen tw:bg-white"></code></div> </div> <p class="tw:text-sm tw:text-neutral-700 tw:mt-4 tw:mb-0"> Omit <code>skip_to:</code> and no link renders — the safe default for a page whose <code><main></code> has not been given an id yet. </p> </div> <!-- ============================================ Why the a11y gate can't catch this ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Why the a11y gate can't catch this</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>Manual verification is the only verification.</strong> axe has no rule for SC 2.4.1 — no automated checker can tell whether a bypass mechanism exists, because deciding what counts as “repeated blocks” needs a human reading the page. A green a11y run on a consumer page says nothing at all about this criterion. </p> </div> <table class="table table-sm"> <thead> <tr> <th>Step</th> <th>Expected</th> </tr> </thead> <tbody> <tr> <td>Load the page, press <kbd class="badge badge-key">Tab</kbd> once</td> <td><code>document.activeElement</code> is the <code>.skip-link</code></td> </tr> <tr> <td>Look at the top-left</td> <td>Navy chip, legible white label, visible focus halo, inside the viewport</td> </tr> <tr> <td>Press <kbd class="badge badge-key">Enter</kbd></td> <td>Focus is on the <code><main></code>, and no ring is painted around it</td> </tr> <tr> <td>Press <kbd class="badge badge-key">Tab</kbd> again</td> <td>Focus is on the first control <em>inside</em> <code><main></code> — the chrome is genuinely skipped</td> </tr> <tr> <td>Tab past the link without activating it</td> <td>The chip parks itself off-screen again</td> </tr> </tbody> </table> </div> <!-- ============================================ Live target ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Live target</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>Target:</strong> the region below stands in for a page's <code><main></code>. It carries <code>id="preview-main"</code>, <code>tabindex="-1"</code> and <code>.skip-link-target</code> — the same three things a real page adds. </p> </div> <div id="preview-main" tabindex="-1" class="skip-link-target card"> <div class="card-body"> <h3 class="h5 card-title tw:mb-2">Main content starts here</h3> <p class="tw:mb-3"> On a real consumer page this is the results list, the application form or the school profile — whatever the visitor actually came for. </p> <button class="btn btn-primary" type="button">First control inside main</button> </div> </div> </div> <!-- ============================================ More than one skip target ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">More than one skip target</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>No extra class needed.</strong> A search page where the filter rail sits between the nav and the results can justify a second bypass ("Skip to results"). Two sibling <code>.skip-link</code> elements need nothing new: both park off-screen, and because only one can hold focus at a time they take turns occupying the same top-left slot as Tab moves through them. Each still needs its own id + <code>tabindex="-1"</code> + <code>.skip-link-target</code> landing zone. </p> </div> <div class="tw:text-sm tw:text-gray-600"> <code><a class="skip-link" href="#main-content">Skip to main content</a></code><br> <code><a class="skip-link" href="#results">Skip to results</a></code> </div> <p class="tw:text-sm tw:text-neutral-700 tw:mt-4 tw:mb-0"> Keep the list short. Every skip link is itself a tab stop, so three or four of them recreate the problem they exist to solve. </p> </div></div>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
<%# The live skip link has to be the first focusable element in the document, so it sits above the preview wrapper — exactly where it sits in shared/_consumer_topframe_session. %><a class="skip-link" href="#preview-main">Skip to main content</a><div class="tw:p-6 tw:max-w-7xl tw:mx-auto tw:space-y-8"> <!-- Introductory callout --> <div class="callout"> <p class="tw:mb-1 tw:text-sm tw:font-mono"> <strong>SKIP LINK:</strong> Keyboard bypass for repeated page chrome (WCAG 2.4.1, Level A)<br> <strong>COMPOSES:</strong> nothing — own classes<br> <strong>CLASSES:</strong> .skip-link | .skip-link-target </p> <p class="tw:mb-0 tw:text-sm"> Without one, a keyboard or switch user landing on a consumer page tabs through the header logo, the auth CTAs, the sticky four-vertical section nav, every hero-search segment and the whole filter rail before reaching a single result. The link is parked outside the viewport until it takes focus, then slides in at the top-left and jumps straight to the page's main region. </p> </div> <div class="inline-notification inline-notification-info"> <i class="fa-regular fa-keyboard inline-notification-icon" aria-hidden="true"></i> <div class="inline-notification-content"> <p class="inline-notification-title">This one is live — press Tab to see it</p> <p class="inline-notification-message tw:mb-0"> Click anywhere in this preview, then press <kbd class="badge badge-key">Tab</kbd> until focus reaches the top of the document. A navy “Skip to main content” chip appears at the top-left. Press <kbd class="badge badge-key">Enter</kbd> and focus lands on the “Main content starts here” region below; the next <kbd class="badge badge-key">Tab</kbd> goes to the first control inside it, not back to the top. Automated checkers cannot test any of this — see “Why the a11y gate can’t catch this”. </p> </div> </div> <!-- ============================================ Anatomy ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Anatomy — two halves of one contract</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>The link and its target ship together.</strong> A bypass link pointing at an <code>id</code> that doesn't exist is a dead control that passes inspection and fails the user, so <code>.skip-link</code> is opt-in per page: the shared top frame renders it only when the page passes <code>skip_to:</code>, and the page adds the matching id, <code>tabindex="-1"</code> and <code>.skip-link-target</code> to its <code><main></code> in the same edit. </p> </div> <div class="info-grid info-grid-bordered"> <div class="info-grid-label">1. The link</div> <div class="info-grid-value"> First focusable element in the DOM. Hidden by transform, not by <code>tw:sr-only</code> — it must stay in the tab order, and a 1px clip-rect box cannot be reliably un-hidden on focus. </div> <div class="info-grid-label">2. <code>tabindex="-1"</code></div> <div class="info-grid-value"> Makes a non-interactive landmark focusable. Without it, some browsers scroll to the anchor but leave focus in the header, so the next Tab drops the user back into the nav they just skipped. </div> <div class="info-grid-label">3. <code>.skip-link-target</code></div> <div class="info-grid-value"> Suppresses the browser's own <code>outline: auto</code>, which otherwise paints a ring around the entire main region — a full-viewport blue frame that reads as a rendering fault. </div> </div> <div class="tw:mt-4 tw:text-sm tw:text-gray-600"> <code><a class="skip-link" href="#main-content">Skip to main content</a></code><br> <code><main id="main-content" tabindex="-1" class="skip-link-target">…</main></code> </div> </div> <!-- ============================================ In the shared top frame ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Wiring a page</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>Two lines per page.</strong> <code>shared/_consumer_topframe_session</code> hosts the link because it is already the first thing every consumer preview renders. Pass <code>skip_to:</code>, then mark up the target. </p> </div> <div class="tw:text-sm tw:text-gray-600 tw:space-y-1"> <div><code><%= render "shared/consumer_topframe_session", active: :providers, logged_in: false, skip_to: "#main-content" %></code></div> <div><code><main id="main-content" tabindex="-1" class="skip-link-target tw:min-h-screen tw:bg-white"></code></div> </div> <p class="tw:text-sm tw:text-neutral-700 tw:mt-4 tw:mb-0"> Omit <code>skip_to:</code> and no link renders — the safe default for a page whose <code><main></code> has not been given an id yet. </p> </div> <!-- ============================================ Why the a11y gate can't catch this ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Why the a11y gate can't catch this</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>Manual verification is the only verification.</strong> axe has no rule for SC 2.4.1 — no automated checker can tell whether a bypass mechanism exists, because deciding what counts as “repeated blocks” needs a human reading the page. A green a11y run on a consumer page says nothing at all about this criterion. </p> </div> <table class="table table-sm"> <thead> <tr><th>Step</th><th>Expected</th></tr> </thead> <tbody> <tr><td>Load the page, press <kbd class="badge badge-key">Tab</kbd> once</td><td><code>document.activeElement</code> is the <code>.skip-link</code></td></tr> <tr><td>Look at the top-left</td><td>Navy chip, legible white label, visible focus halo, inside the viewport</td></tr> <tr><td>Press <kbd class="badge badge-key">Enter</kbd></td><td>Focus is on the <code><main></code>, and no ring is painted around it</td></tr> <tr><td>Press <kbd class="badge badge-key">Tab</kbd> again</td><td>Focus is on the first control <em>inside</em> <code><main></code> — the chrome is genuinely skipped</td></tr> <tr><td>Tab past the link without activating it</td><td>The chip parks itself off-screen again</td></tr> </tbody> </table> </div> <!-- ============================================ Live target ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">Live target</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>Target:</strong> the region below stands in for a page's <code><main></code>. It carries <code>id="preview-main"</code>, <code>tabindex="-1"</code> and <code>.skip-link-target</code> — the same three things a real page adds. </p> </div> <div id="preview-main" tabindex="-1" class="skip-link-target card"> <div class="card-body"> <h3 class="h5 card-title tw:mb-2">Main content starts here</h3> <p class="tw:mb-3"> On a real consumer page this is the results list, the application form or the school profile — whatever the visitor actually came for. </p> <button class="btn btn-primary" type="button">First control inside main</button> </div> </div> </div> <!-- ============================================ More than one skip target ============================================ --> <div class="tw:border-t tw:pt-6"> <h2 class="h4 tw:mb-4">More than one skip target</h2> <div class="callout tw:mb-4"> <p class="tw:mb-0 tw:text-sm"> <strong>No extra class needed.</strong> A search page where the filter rail sits between the nav and the results can justify a second bypass ("Skip to results"). Two sibling <code>.skip-link</code> elements need nothing new: both park off-screen, and because only one can hold focus at a time they take turns occupying the same top-left slot as Tab moves through them. Each still needs its own id + <code>tabindex="-1"</code> + <code>.skip-link-target</code> landing zone. </p> </div> <div class="tw:text-sm tw:text-gray-600"> <code><a class="skip-link" href="#main-content">Skip to main content</a></code><br> <code><a class="skip-link" href="#results">Skip to results</a></code> </div> <p class="tw:text-sm tw:text-neutral-700 tw:mt-4 tw:mb-0"> Keep the list short. Every skip link is itself a tab stop, so three or four of them recreate the problem they exist to solve. </p> </div></div>No notes provided.
No params configured.