Let's build an accessible Minesweeper game using HTML and Typescript.
Minesweeper is a classic logic game where you uncover tiles to avoid hidden mines and flag suspicious spots, aiming to clear the board without triggering any bombs. Building it for the web is a fun way to showcase real-world accessibility patterns.
This post will dive into the accessibility techniques that make Minesweeper fully playable with just a keyboard and friendly for screen readers, using my web component library.
I’ll call out which components are involved, how ARIA attributes are put to use, and how keyboard navigation is handled throughout the experience.
up
, down
, left
, right
): Navigate between tiles on the grid.Home
: Jump to the first tile in the first column.End
: Jump to the last tile in the last column.Ctrl + Home
: Move to the first tile in the current column.Ctrl + End
: Move to the last tile in the current column.r
key: Restart the current game.f
key: Toggle the flag on the currently selected tile.Enter
or Spacebar
: Reveal the currently selected tile (uncover its content).A Minesweeper grid can’t just be a bunch of buttons and call itself accessible. Every interaction, every tile state, needs to be clearly exposed and predictable. Focus can’t get lost. Screen readers need the right cues, at just the right time.
The UI uses ARIA labels to achieve clear and accessible communication of tile states:
aria-label="Unrevealed tile"
, so screen readers know the state and that the tile can be interacted with.aria-label="Flag"
when flagged, and revert to "Unrevealed tile" when unflagged.aria-label
to that number, for example, aria-label="2"
.aria-label="Empty Tile"
.aria-label="Bomb!"
.Tip: Keep aria labels short and clear. Screen reader users move fast, they need quick, relevant info, not extra words.
To ensure that all updates are announced when the game ends (win or lose), we use the <c-aria-live>
component. This makes the game status clear to both sighted users and screen reader users, as it is not reliably announced otherwise.
<c-aria-live>
The <c-aria-live>
component is an accessible live region for screen readers. It is visually hidden, keeping the layout clean. We use it to announce status changes, such as when you win or hit a bomb, without shifting focus away from the game. The assertive
attribute ensures screen readers announce updates immediately.
When the game ends, we update the status message with either a victory or game over notice, along with your time and a tip to restart.
status.textContent = hasWon
? `You won! Game Completed in ${timer.count} seconds. Press R to restart game`
: `Bomb! Game Over. Your time was ${timer.count} seconds. Press R to restart.`;
By default, every tile on the grid can be tabbed to, which makes for a painfully long and frustrating tab order. Keyboard and screen reader users have to tab through every single cell just to get across the board. To fix this, we use the <c-navigation-grid>
component.
<c-navigation-grid>
The c-navigation-grid
component handles keyboard events and ensures every tile is accessible by arrow keys. The selector
attribute of the grid defines a CSS selector string that determines which elements within the grid are considered for keyboard navigation.
The grid also manages focus so that only one of its children is tabbable (tabIndex=0
) at a time, while all others have tabIndex=-1
to prevent accidental tabbing between items. It keeps track of which tile is currently active. Whenever focus moves, whether by keyboard, mouse, or when the grid’s children change, the component updates which tile is focusable preventing focus from getting lost in the grid.
The grid uses the ARIA grid
role, and each tile uses the gridcell
role. For screen readers to announce cell positions correctly, every group of tiles in a row should also be wrapped inside an element with the row
role. While this wrapper isn't needed visually for the CSS grid layout, it's essential for accessibility.
<c-row>
Each c-row
uses the row
role and provides a display: contents
container so it doesn't affect the grid layout. This setup helps assistive technologies understand the grid structure, allowing them to read the tiles as a spreadsheet rather than just a group of buttons.
Screen readers sometimes lose the current focus when a tile is interacted with or when the game ends. This leads to confusing jumps or a complete loss of focus (nothing is read at all).
We solve this by programmatically resetting focus after each action that could disrupt it, like opening a tile, flagging, or changing the game state. Whenever elements are removed or swapped, we immediately focus the most relevant tile or button. This keeps keyboard and screen reader users grounded in the game, avoiding dead ends or lost context.
Here are some extra tips, along with how the library's UI components help you out of the box.
Don’t rely on color alone to convey information. While the tiles use color to show state, each one also has labels and icons as backup, so color-blind users don’t get left out. Every color in the demo sticks to WCAG contrast guidelines.
If you use animations or flashing effects, respect user preferences for reduced motion (via CSS prefers-reduced-motion: reduce
) to prevent issues for users with vestibular disorders.
Ensure that the currently focused control always has a visible outline. This is critical for low-vision users and for keyboard navigation. The components used in the demo show a clear, high-contrast focus outline.
Building accessible applications isn't just about adding ARIA roles and keyboard handlers, it's about thoughtfully guiding every user's experience, ensuring feedback, context, and control at every step.
If you find a bug or notice anything I missed, please let me know.