Minesweeper: Keyboard Navigation and Accessibility Minesweeper: Keyboard Navigation and Accessibility

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.

Demo

Controls

Navigation

Actions

The Hard Problems

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.

Communicating Tile States

The UI uses ARIA labels to achieve clear and accessible communication of tile states:

Tip: Keep aria labels short and clear. Screen reader users move fast, they need quick, relevant info, not extra words.

Dynamic announcements

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.`;

Wrong tab order inside the grid

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.

CSS Grid ARIA structure

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.

Focus traps and invisible “lost” focus

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.

Additional Recommendations

Here are some extra tips, along with how the library's UI components help you out of the box.

Color and Visual Contrast

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.

Reduced Motion and Animations

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.

Accessible Focus Outline

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.

Conclusion

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.

Further Reading

Back to Main Page