Skip to content

Commit accc0b2

Browse files
committed
Improve Sverdle a11y
- make inputs hidden - respect reduced motion for confetti and jiggle - visually hidden headings - visually hidden text for letter state - remove aria-selected - increase color contrast on selected tile
1 parent e7a4c9a commit accc0b2

File tree

3 files changed

+86
-30
lines changed

3 files changed

+86
-30
lines changed

packages/create-svelte/templates/default/src/routes/styles.css

+13-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
--color-bg-1: hsl(209, 36%, 86%);
99
--color-bg-2: hsl(224, 44%, 95%);
1010
--color-theme-1: #ff3e00;
11-
--color-theme-2: #40b3ff;
11+
--color-theme-2: #4075a6;
1212
--color-text: rgba(0, 0, 0, 0.7);
1313
--column-width: 42rem;
1414
--column-margin-top: 4rem;
@@ -93,3 +93,15 @@ button:focus:not(:focus-visible) {
9393
font-size: 2.4rem;
9494
}
9595
}
96+
97+
.visually-hidden {
98+
border: 0;
99+
clip: rect(0 0 0 0);
100+
height: auto;
101+
margin: 0;
102+
overflow: hidden;
103+
padding: 0;
104+
position: absolute;
105+
width: 1px;
106+
white-space: nowrap;
107+
}

packages/create-svelte/templates/default/src/routes/sverdle/+page.svelte

+47-29
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { confetti } from '@neoconfetti/svelte';
33
import { enhance } from '$app/forms';
44
import type { PageData, ActionData } from './$types';
5+
import { reduced_motion } from './reduced-motion';
56
67
/** @type {import('./$types').PageData} */
78
export let data: PageData;
@@ -25,8 +26,16 @@
2526
*/
2627
let classnames: Record<string, 'exact' | 'close' | 'missing'>;
2728
29+
/**
30+
* A map of descriptions for all letters that have been guessed,
31+
* used for adding text for assistive technology (e.g. screen readers)
32+
* @type {Record<string, string>}
33+
*/
34+
let description: Record<string, string>;
35+
2836
$: {
2937
classnames = {};
38+
description = {};
3039
3140
data.answers.forEach((answer, i) => {
3241
const guess = data.guesses[i];
@@ -36,8 +45,10 @@
3645
3746
if (answer[i] === 'x') {
3847
classnames[letter] = 'exact';
48+
description[letter] = 'correct';
3949
} else if (!classnames[letter]) {
4050
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
51+
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
4152
}
4253
}
4354
});
@@ -83,6 +94,8 @@
8394
<meta name="description" content="A Wordle clone written in SvelteKit" />
8495
</svelte:head>
8596

97+
<h1 class="visually-hidden">Sverdle</h1>
98+
8699
<form
87100
method="POST"
88101
action="?/enter"
@@ -98,20 +111,30 @@
98111
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
99112
{#each Array(6) as _, row}
100113
{@const current = row === i}
101-
114+
<h2 class="visually-hidden">Row {row + 1}</h2>
102115
<div class="row" class:current>
103116
{#each Array(5) as _, column}
104117
{@const answer = data.answers[row]?.[column]}
105-
106-
<input
107-
name="guess"
108-
disabled={!current}
109-
readonly
110-
class:exact={answer === 'x'}
111-
class:close={answer === 'c'}
112-
aria-selected={current && column === data.guesses[row].length}
113-
value={data.guesses[row]?.[column] ?? ''}
114-
/>
118+
{@const value = data.guesses[row]?.[column] ?? ''}
119+
{@const selected = current && column === data.guesses[row].length}
120+
{@const exact = answer === 'x'}
121+
{@const close = answer === 'c'}
122+
{@const missing = answer === '_'}
123+
<div class="letter" class:exact class:close class:missing class:selected>
124+
{value}
125+
<span class="visually-hidden">
126+
{#if exact}
127+
(correct)
128+
{:else if close}
129+
(present)
130+
{:else if missing}
131+
(absent)
132+
{:else}
133+
empty
134+
{/if}
135+
</span>
136+
<input name="guess" disabled={!current} type="hidden" {value} />
137+
</div>
115138
{/each}
116139
</div>
117140
{/each}
@@ -122,12 +145,12 @@
122145
{#if !won && data.answer}
123146
<p>the answer was "{data.answer}"</p>
124147
{/if}
125-
<button data-key="enter" aria-selected="true" class="restart" formaction="?/restart">
148+
<button data-key="enter" class="restart selected" formaction="?/restart">
126149
{won ? 'you won :)' : `game over :(`} play again?
127150
</button>
128151
{:else}
129152
<div class="keyboard">
130-
<button data-key="enter" aria-selected={submittable} disabled={!submittable}>enter</button>
153+
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
131154

132155
<button
133156
on:click|preventDefault={update}
@@ -150,6 +173,7 @@
150173
formaction="?/update"
151174
name="key"
152175
value={letter}
176+
aria-label="{letter} {description[letter] || ''}"
153177
>
154178
{letter}
155179
</button>
@@ -165,6 +189,7 @@
165189
<div
166190
style="position: absolute; left: 50%; top: 30%"
167191
use:confetti={{
192+
particleCount: $reduced_motion ? 0 : undefined,
168193
force: 0.7,
169194
stageWidth: window.innerWidth,
170195
stageHeight: window.innerHeight,
@@ -225,15 +250,17 @@
225250
margin: 0 0 0.2rem 0;
226251
}
227252
228-
.grid.bad-guess .row.current {
229-
animation: wiggle 0.5s;
253+
@media (prefers-reduced-motion: no-preference) {
254+
.grid.bad-guess .row.current {
255+
animation: wiggle 0.5s;
256+
}
230257
}
231258
232259
.grid.playing .row.current {
233260
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
234261
}
235262
236-
input {
263+
.letter {
237264
aspect-ratio: 1;
238265
width: 100%;
239266
display: flex;
@@ -250,33 +277,24 @@
250277
color: rgba(0, 0, 0, 0.7);
251278
}
252279
253-
input:disabled:not(.exact):not(.close) {
280+
.letter.missing {
254281
background: rgba(255, 255, 255, 0.5);
255282
color: rgba(0, 0, 0, 0.5);
256283
}
257284
258-
input.exact {
285+
.letter.exact {
259286
background: var(--color-theme-2);
260287
color: white;
261288
}
262289
263-
input.close {
290+
.letter.close {
264291
border: 2px solid var(--color-theme-2);
265292
}
266293
267-
input:focus {
268-
outline: none;
269-
}
270-
271-
[aria-selected='true'] {
294+
.selected {
272295
outline: 2px solid var(--color-theme-1);
273296
}
274297
275-
input:not(:disabled)::selection {
276-
background: transparent;
277-
color: var(--color-theme-1);
278-
}
279-
280298
.controls {
281299
text-align: center;
282300
justify-content: center;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { readable } from 'svelte/store';
2+
import { browser } from '$app/environment';
3+
4+
const reduced_motion_query = '(prefers-reduced-motion: reduce)';
5+
6+
const get_initial_motion_preference = () => {
7+
if (!browser) return false;
8+
return window.matchMedia(reduced_motion_query).matches;
9+
};
10+
11+
export const reduced_motion = readable(get_initial_motion_preference(), (set) => {
12+
if (browser) {
13+
/**
14+
* @param {MediaQueryListEvent} event
15+
*/
16+
const set_reduced_motion = (event: MediaQueryListEvent) => {
17+
set(event.matches);
18+
};
19+
const media_query_list = window.matchMedia(reduced_motion_query);
20+
media_query_list.addEventListener('change', set_reduced_motion);
21+
22+
return () => {
23+
media_query_list.removeEventListener('change', set_reduced_motion);
24+
};
25+
}
26+
});

0 commit comments

Comments
 (0)