UI pattern playground
Accessibility UI patterns playground
Hands-on UI pattern practice for product managers, designers, and developers. Each pattern explains who is affected and what to fix, then lets you compare broken and accessible UI. Developers also get reference code and a live editor to copy into projects. Explore Dev Tools (wcag-kit, Claude Skill, and WCAG Lens) to ask about these patterns without leaving your IDE.
Find and browse UI patterns
Search patterns by name (e.g. "modal", "breadcrumb").
Search patterns by name (e.g. "modal", "breadcrumb").
All 27 patterns
Pick a pattern to jump straight to its broken and accessible demos. Use Jump to pattern or the sidebar to go directly to a section.
Forms & input
Overlays & feedback
Navigation & layout
Content & structure
Controls & interaction
Dropdown Menu
A dropdown is a button that opens a list of options. Without the right code, keyboard users and screen reader users can't open it or navigate the choices.
Understand this pattern
Who is affected
- Keyboard-only users
- Blind and low-vision users
- People with motor disabilities who don't use a mouse
Designer note
If your design shows a styled div or box as the trigger instead of a button, flag it. The trigger must be a real button element.
What breaks
- Cannot be opened or navigated with a keyboard
- Screen readers announce it as a generic group with no context
- No aria-expanded, so assistive tech cannot tell if it is open
- Arrow keys and Escape do nothing
- Violates WCAG 2.1.1 and 4.1.2
What to do instead
- Fully keyboard navigable: Tab, Enter, arrows, and Escape
- Screen reader announces: Select country, collapsed, button
- aria-expanded updates when the menu opens or closes
- aria-selected tracks the highlighted option
- Meets WCAG 2.1.1 and 4.1.2
Impact
Custom dropdowns are one of the most common accessibility failures. Keyboard-only users cannot use a menu that relies on mouse clicks alone. Correct ARIA roles and keyboard handlers take similar effort but work for everyone.
How to test
Tab to the menu, press Enter to open it, use Arrow Down and Arrow Up to move, Enter to select, and Escape to close.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Modal / Dialog
A modal is a popup window that appears over the page. Without the right code, keyboard users get trapped behind it or can't interact with it at all.
Understand this pattern
Who is affected
- Keyboard-only users
- Screen reader users
- People with cognitive disabilities who rely on clear focus
Designer note
Every modal needs a visible close button and must return the user to where they were before the modal opened. Design for the exit, not just the entry.
What breaks
- Tab escapes the modal into the page behind it
- Screen readers can still reach background content
- No role=dialog, so it is not announced as a dialog
- Escape does nothing
- Focus does not return to the trigger on close
What to do instead
- Focus stays inside the modal while it is open
- Screen reader announces the dialog title
- Escape closes the modal and returns focus to the trigger
- Background content is hidden via aria-modal
- Meets WCAG 2.1.2 and 4.1.2
Impact
A modal without a focus trap is a dead end for keyboard users. Once focus moves behind the overlay, there is no clear way back. Dialog primitives such as Radix handle trapping correctly.
How to test
Open the bad modal and press Tab to see focus leave. Open the good modal and confirm Tab cycles inside until you press Escape.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Form Validation
Form validation tells users what went wrong when they submit a form. Without the right code, screen reader users submit a form and hear nothing: they have no idea an error occurred.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- People with colour blindness (if errors use colour only)
Designer note
Never use colour alone to show an error. Always pair it with an icon and text. Every error message must appear next to the field it describes, not just at the top of the page.
What breaks
- Label and input are not linked, screen readers can't associate them
- Error uses red colour only, 8% of men have colour blindness
- No role='alert', screen readers stay completely silent when the error appears
- aria-invalid is missing, screen readers can't tell the field is in an error state
- type='text' misses email-specific validation and the correct mobile keyboard
What to do instead
- htmlFor links label to input, screen reader says 'Email Address, required, edit text'
- Error uses a ⚠️ icon AND text, not colour alone
- role='alert' causes the screen reader to announce the error immediately on submit
- aria-invalid='true' tells screen readers the field contains an error
- type='email' gives mobile users the correct keyboard and enables browser validation
Impact
Form errors are the most common barrier for screen reader users on e-commerce and sign-up flows. If an error isn't programmatically linked to its input and announced via role='alert', a blind user has no idea what went wrong or which field to fix. These ARIA attributes cost nothing to add and make forms usable for everyone.
How to test
Tab into the bad form, submit without an email, and listen, your screen reader says nothing. Do the same in the good form.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Heading Hierarchy
Headings are the invisible table of contents for a page. Screen reader users navigate by jumping between headings: if the structure is broken, they get lost or miss content entirely.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- People with cognitive disabilities who rely on clear structure
Designer note
Don't choose heading levels based on how big you want the text to look. Use CSS for size. Headings should reflect the page structure: one H1, then H2 for sections, H3 for subsections.
What breaks
- Bold or large text used instead of heading elements, missing from the heading list
- Skipped levels (h1 then h4) break the mental model of page structure
- Multiple h1 elements suggest several unrelated pages in one view
- Screen reader users cannot jump by section when headings are wrong
- Violates WCAG 1.3.1 (Info and Relationships) and 2.4.6 (Headings and Labels)
What to do instead
- One h1 describes the main purpose of the page or view
- h2 marks major sections; h3 marks subsections without skipping levels
- Visual size comes from CSS, not from picking arbitrary heading levels
- Heading rotor / landmarks list matches what sighted users see in the outline
- Meets WCAG 1.3.1 and 2.4.6
Impact
Screen reader users often navigate by headings (H key in NVDA/JAWS, rotor in VoiceOver). A broken outline means they hear a flat wall of text or jump to the wrong section. Fixing hierarchy is mostly choosing the right HTML element, not redesigning the UI.
How to test
Open your screen reader heading list (NVDA: Insert+F7, VoiceOver: rotor → Headings). Compare the bad example’s chaotic list to the good example’s h1 → h2 → h3 tree.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Icon Button
An icon button is a clickable control with only an icon and no visible text. Without a label, a screen reader user hears only 'button' with no idea what it does.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- People with cognitive disabilities who don't recognise icons
Designer note
For every icon-only button in your design, ask: what would a screen reader announce? If the answer is just 'button', it needs a label. Consider adding a visible tooltip or text label.
What breaks
- Variant 1: Screen reader only says 'button', zero context for the user
- Variant 2: title is unreliable, many screen readers ignore it by default
- Variant 3: Icon not marked aria-hidden, so it gets announced as 'image'
- Icon-only buttons with no label are also a cognitive accessibility issue
- All three violate WCAG 4.1.2 (Name, Role, Value)
What to do instead
- aria-label gives the button a clear name: 'Delete item, button'
- sr-only hides text visually while keeping it accessible to screen readers
- aria-hidden='true' on the icon stops it being announced separately
- All three good variants produce the same clean announcement
- Choose the variant that fits your design, all three are correct
Impact
Icon buttons are everywhere, toolbars, tables, cards. Without a text alternative, every icon button is a mystery to a screen reader user. Adding aria-label or sr-only text takes five seconds and makes your interface usable for blind and low-vision users.
How to test
Focus each bad button and listen to your screen reader. Then focus the good buttons, notice the difference in what's announced.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Image Alt Text
Alt text is a written description of an image. When a screen reader reaches an image, it reads the alt text aloud. Without it, users hear the filename or nothing at all.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- People on slow connections where images don't load
Designer note
For every image in your design, ask: if this image disappeared, what information would be lost? That's what the alt text should say. Decorative images need no description.
What breaks
- Generic alt like image or logo
- Describes appearance instead of purpose
- Decorative images given verbose descriptions
- Charts missing text alternatives for data
- Linked images missing destination context
What to do instead
- Alt explains what would be lost if the image disappeared
- Linked images describe the action or destination
- Charts include data in text nearby
- Decorative images use alt=""
- Meets WCAG 1.1.1
Impact
Alt text replaces the image for people who cannot see it. Caption-style descriptions often miss the point the image was meant to convey.
How to test
Read the alt strings below and ask whether each one answers what information would be lost.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Color Contrast
Colour contrast is the difference in brightness between text and its background. If the contrast is too low, the text is hard or impossible to read for many users.
Understand this pattern
Who is affected
- People with low vision
- People with colour blindness
- Anyone in bright sunlight or on a low-quality screen
Designer note
Before finalising any colour combination for text, run it through a contrast checker. The minimum ratio is 4.5:1 for normal text and 3:1 for large text (18px bold or 24px regular).
What breaks
- Light gray body text on white (~2.5:1) fails AA for normal text
- Pastel success or link colours chosen for aesthetics, not readability
- Colour used as the only way to show state (still needs sufficient contrast)
- Placeholder and disabled styles mistaken for readable body copy
- Violates WCAG 1.4.3 (Contrast Minimum)
What to do instead
- Body text at roughly 4.5:1 or higher against its background (AA)
- Links and buttons tested as foreground/background pairs, not in isolation
- Semantic colour tokens checked in both light UI states
- Large text (18pt+ or 14pt bold) may use 3:1 where appropriate
- Mini checker validates hex pairs before you ship
Impact
Low contrast is one of the most common WCAG failures on the web. It affects people with low vision, users in bright sunlight, and anyone on a poor monitor. Most fixes are a darker text or background tweak, not a redesign.
How to test
Use the mini checker with your brand colours, or browser DevTools → Accessibility → Contrast. Squint at the bad examples, if the text disappears, the ratio is too low.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Accordion / Disclosure
An accordion is a list of sections that expand and collapse. Without the right code, a screen reader user cannot tell if a section is open or closed, and may not be able to open it at all.
Understand this pattern
Who is affected
- Keyboard-only users
- Blind and low-vision users
- Screen reader users
Designer note
Every accordion trigger must look and behave like a button. Include a visual indicator (chevron or +/-) that changes when the section is open. The state change must be communicated in code, not just visually.
What breaks
- Div trigger cannot be reached or activated by keyboard
- No aria-expanded, screen reader cannot tell if section is open or closed
- No aria-controls, trigger and panel are not linked
- Panel hidden with CSS only, screen readers may still read hidden content
- Violates WCAG 4.1.2
What to do instead
- Button trigger is keyboard focusable and activatable with Enter or Space
- aria-expanded updates from false to true when panel opens
- aria-controls links the button to its panel by id
- hidden attribute prevents screen readers from reading collapsed content
- Meets WCAG 4.1.2
Impact
Accordions are used in FAQs, settings panels, and content-heavy pages. A keyboard user who cannot open an accordion section simply misses that content. The fix is replacing div triggers with button elements and adding two ARIA attributes.
How to test
Tab to the bad accordion trigger and press Enter: nothing happens. Tab to the good accordion trigger and press Enter: it opens. Check that your screen reader announces 'collapsed' and 'expanded' as you toggle.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Tabs
Tabs are a row of buttons that each reveal a different panel of content. Without the right code, a screen reader user hears a confusing list with no context, and keyboard users can't navigate between tabs.
Understand this pattern
Who is affected
- Keyboard-only users
- Blind and low-vision users
- Screen reader users
Designer note
The active tab must be distinguishable by more than colour. Include an underline, border, or shape change. Make sure there is enough visual difference between the selected and unselected states for low-vision users.
What breaks
- Div triggers are not keyboard focusable
- No role=tablist, role=tab, or role=tabpanel, screen readers get no context
- No aria-selected, active tab is not announced
- Arrow keys do nothing, keyboard users cannot move between tabs
- Violates WCAG 2.1.1
What to do instead
- role=tablist wraps the tab row, role=tab on each trigger
- aria-selected=true on active tab, false on others
- Arrow Left and Right move focus between tabs
- Tab key enters the active panel content
- Meets WCAG 2.1.1 and 4.1.2
Impact
Tabs are one of the most common interactive patterns on the web. Broken tab implementations leave keyboard users stuck on the first tab, unable to reach other content. The ARIA pattern is well-documented and straightforward to implement.
How to test
Focus the bad tabs and press Arrow Right: nothing moves. Focus the good tabs and press Arrow Right: focus moves to the next tab. Press Tab to enter the panel content.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Toast / Notifications
A toast is a small notification that appears briefly on screen. Without the right code, it appears visually but is completely silent for screen reader users: they never know it happened.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- People with cognitive disabilities who rely on confirmation messages
Designer note
Every toast or notification in your design must have a text label, not just an icon or colour. Design a dismiss button for toasts that don't auto-dismiss. For auto-dismissing toasts, keep them visible for at least 5 seconds.
What breaks
- No aria-live region, screen readers never announce the message
- No keyboard-accessible dismiss button
- Auto-dismisses with no pause option
- Colour and icon used without text label
- Violates WCAG 4.1.3
What to do instead
- aria-live=polite region announces the message to screen readers without stealing focus
- role=status communicates this is a non-urgent update
- Keyboard-focusable close button with aria-label
- Auto-dismiss pauses on hover and focus
- Meets WCAG 4.1.3
Impact
Toasts confirm actions: item saved, form submitted, error occurred. If a screen reader user never hears the confirmation, they may repeat the action or assume it failed. aria-live is a one-line addition that makes these messages accessible.
How to test
Trigger the bad toast and listen with your screen reader: nothing is announced. Trigger the good toast: the message is read aloud without moving your focus.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Data Table
A data table organises information into rows and columns. Without the right markup, a screen reader user hears a long list of numbers with no idea which column they belong to.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- People with cognitive disabilities who rely on structured data
Designer note
Every column must have a clear header label. If a table has sortable columns, show sort direction in text or ARIA, not only with an arrow icon.
What breaks
- Div-based layout, no semantic table structure
- No column headers, screen readers read cells as a flat disconnected list
- No caption, purpose of the table is not announced
- Sort state shown visually only, not communicated to screen readers
- Violates WCAG 1.3.1
What to do instead
- Semantic table element with caption describing the data
- th elements with scope=col link headers to their columns
- aria-sort announces current sort direction to screen readers
- Sort state change announced via live region
- Meets WCAG 1.3.1
Impact
Data tables are common in dashboards, reports, and admin interfaces. Without proper headers, a screen reader user hears cell after cell with no context. Semantic HTML fixes this entirely.
How to test
Navigate the bad table with your screen reader and notice cells are read with no column name. Navigate the good table and hear each cell announced with its column header.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Focus Management
In a single-page app, clicking a nav link swaps content without a full page reload. Without the right code, a screen reader user clicks a link and hears nothing. They have no idea the content changed.
Understand this pattern
Who is affected
- Blind and low-vision users
- Screen reader users
- Keyboard-only users
Designer note
Add this to acceptance criteria for SPAs: every route change moves focus to the new content and announces the page title. This is a developer task, but PMs and designers should require it.
What breaks
- Focus stays on the nav link after content changes
- No announcement that new content has loaded
- Screen reader user must manually navigate to find new content
- New page title not announced
- Violates WCAG 2.4.3
What to do instead
- Focus moves to the top of the new content on route change
- New page title announced via aria-live region
- Focus target uses tabIndex=-1 for programmatic focus
- Screen reader user knows immediately that navigation occurred
- Meets WCAG 2.4.3
Impact
React, Vue, and Angular apps swap content without full page reloads. This is invisible to sighted users but breaks the experience for screen reader users completely. A few lines of focus management code fix this.
How to test
Click a nav item in the bad example with your screen reader on and notice nothing is announced. Click in the good example: the new page title is announced and focus moves to the new content.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Combobox (List Autocomplete)
A combobox is a text field that shows suggestions as you type. Without ARIA, the suggestion list is invisible to screen readers and unreachable by keyboard.
Understand this pattern
Who is affected
- Blind users relying on screen readers to navigate suggestions
- Keyboard-only users who cannot click options with a mouse
- People with motor disabilities using switch access or voice control
- Users with cognitive disabilities who need clear state announcements
Designer note
Link the input and suggestion list in code. Use a visible label, not only a placeholder. Highlight the active suggestion visually and in ARIA.
What breaks
- Input has no role - treated as a plain text field by screen readers
- No aria-expanded - screen readers cannot detect the list opening
- Options are divs - keyboard cannot reach them at all
- No aria-activedescendant - active option is never announced
- No label - screen readers only read the placeholder, which disappears
What to do instead
- role="combobox" on the input - screen readers know the pattern
- aria-expanded updates live - list open/close state is announced
- role="listbox" and role="option" - list is fully keyboard navigable
- aria-activedescendant tracks the focused option - announced on each move
- Explicit label - the field is always named even after typing begins
Impact
Comboboxes appear in search fields, address forms, and tag inputs across almost every web application. Without ARIA, blind and keyboard users cannot access suggestions.
How to test
Tab to the input and type a letter. Press Arrow Down - the first option should be announced. Press Enter - the input fills and the list closes. Press Escape - the list closes without selecting. Listen for aria-expanded when the list opens and closes.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Tooltip
A tooltip explains what a control does. If it only appears on hover, keyboard and touch users never see it. If it cannot be dismissed with Escape, it may block other content. WCAG 1.4.13 requires tooltips to be hoverable, dismissible, and persistent.
Understand this pattern
Who is affected
- Keyboard-only users who cannot hover with a mouse
- Touch screen users who have no hover state
- Low-vision users who zoom in and may trigger tooltips accidentally
- Screen reader users who need the tooltip text linked via aria-describedby
- Users with cognitive disabilities who need time to read tooltip content
Designer note
Every tooltip must be reachable without a mouse. Never put essential information only in a tooltip - supplement a visible label or aria-label, not replace it.
What breaks
- Tooltip appears on hover only - keyboard and touch users never see it
- No role="tooltip" - screen readers do not announce it as a tooltip
- No aria-describedby - tooltip text is not associated with the button
- Cannot be dismissed with Escape - may overlap other content
- Closes when pointer leaves the button - cannot be read by hovering over it
What to do instead
- Tooltip appears on both mouseenter and focus - reachable without a mouse
- role="tooltip" and aria-describedby - announced as supplementary description
- Escape dismisses the tooltip while keeping focus on the button
- Pointer can move onto the tooltip without it closing (hoverable)
- Tooltip persists until Escape or focus leaves - enough time to read it
Impact
Tooltips on icon buttons and truncated text are everywhere. Hover-only tooltips hide supplementary information from keyboard and touch users. WCAG 1.4.13's hoverable, dismissible, and persistent requirements are easy to miss.
How to test
Tab to a button - tooltip appears on focus. Press Escape - tooltip closes, focus stays. Hover the button, move onto the tooltip - it stays open. Move away - it closes. Confirm aria-label (name) and aria-describedby (description) on the button.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Date Picker
A date picker opens a calendar to choose a date. If days are divs, keyboard users cannot reach them. Screen readers hear numbers without month or year context.
Understand this pattern
Who is affected
- Keyboard-only users who cannot click calendar days with a mouse
- Blind and low-vision users who need readable date announcements
- People with motor disabilities using switch access or voice control
- Screen reader users who need role="grid" to understand the structure
- Mobile users who may rely on keyboard-accessible alternatives
Designer note
Show a visible focus ring on the focused day. Mark the selected date with more than colour alone. Label Prev/Next month controls clearly.
What breaks
- Calendar days are divs - Tab cannot reach any day
- No arrow key navigation - keyboard users are completely stuck
- Day cells have no aria-label - screen readers announce only numbers
- No role="grid" - the calendar structure is invisible to screen readers
- Escape does not close the calendar - no keyboard exit
What to do instead
- role="grid" with role="gridcell" and role="columnheader" - structure announced
- Each day button has a full aria-label - screen readers read the complete date
- Arrow keys navigate days in all four directions - full keyboard access
- Escape closes the calendar and returns focus to the trigger
- aria-pressed marks the selected date - announced by screen readers
Impact
Date pickers appear on booking and scheduling flows. An inaccessible calendar blocks keyboard and screen reader users from choosing a date unless a plain labelled text field is offered instead.
How to test
Tab to the calendar trigger and press Enter. Arrow Right moves one day forward; Arrow Down moves one week. Press Enter to select - the input updates and the calendar closes. Open again and press Escape - focus returns to the trigger.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Carousel
A carousel rotates through slides. If it auto-plays without a pause control, many users cannot read the content. If Previous and Next are not real buttons, keyboard users cannot operate the carousel at all.
Understand this pattern
Who is affected
- Keyboard-only users who cannot click div controls
- Screen reader users who need slide changes announced
- Users with vestibular disorders affected by motion
- Users with cognitive disabilities who need time to read each slide
- Touch users when hover-only controls hide functionality
Designer note
Put Pause first in the tab order. Pause auto-rotation when any carousel control is focused. Do not rely on colour-only dots - mark the current slide with aria-current.
What breaks
- Auto-rotation cannot be paused - fails WCAG 2.2.2 Pause, Stop, Hide
- Previous/Next are divs - Tab cannot reach them
- No carousel label - screen readers get no region context
- Slide changes are not announced
- Arrow keys do nothing - no keyboard navigation
What to do instead
- role="region" and aria-roledescription="carousel" - context is clear
- Pause/Play button stops auto-rotation; focus inside the carousel also pauses
- aria-live="off" while playing, "polite" when paused - avoids interrupting screen readers
- Real buttons for Previous, Next, and slide dots - full keyboard access
- Slide groups use aria-label Slide N of 4 - position is announced
Impact
Marketing carousels often auto-rotate and hide controls from keyboards. Users miss content entirely or cannot stop movement long enough to read a slide.
How to test
Tab to Pause - rotation should stop. Press Arrow Right - next slide appears and ScreenReaderOutput updates. Press Space on Pause to resume. Confirm aria-live is off during auto-play and polite when paused.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Progress Indicator
A progress indicator shows which step you are on in a form. If step changes are visual only, screen reader users do not know they moved forward or how much is left.
Understand this pattern
Who is affected
- Blind and low-vision users who rely on screen reader announcements
- Screen reader users completing multi-step flows
- People with cognitive disabilities who need clear step context
- Keyboard users who need programmatic progress state
Designer note
Mark the current step with more than colour - use aria-current and a visible border or weight change. Pair a progressbar with a polite live region that announces each step.
What breaks
- Progress bar is a styled div - no role, valuemin, valuemax, or valuenow
- Current step shown by colour only - fails use of colour criterion
- No aria-live region - step changes are silent
- Step count is visual text only - not exposed to assistive tech
- Violates WCAG 4.1.3 Status Messages
What to do instead
- role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax
- aria-live polite status region announces each step change
- aria-current="step" on the active step in the list
- Active step uses border and weight - not colour alone
- Submit on the final step with a clear success announcement
Impact
Checkout, onboarding, and account creation flows use step indicators. Silent step changes leave screen reader users unsure whether Next worked or which fields to complete next.
How to test
Press Next on the good demo and listen: Step 2 of 3 should be announced. Inspect the progressbar - aria-valuenow should match the step. Press Submit on step 3 and confirm the success message is announced.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Pagination
Pagination lets users move between pages of search results. If controls are divs, keyboard users cannot reach them. If the current page is colour only, many users cannot tell which page is active.
Understand this pattern
Who is affected
- Keyboard-only users who cannot click div page numbers
- Blind and low-vision users who need page context in control names
- Screen reader users who need to hear when results change
- People with low vision who cannot rely on colour alone for the current page
Designer note
Use a nav landmark with an accessible name. Label Previous and Next - do not rely on arrow symbols alone. Mark the current page with aria-current and a visible border or weight change.
What breaks
- Pagination is a plain div - no navigation landmark
- Page controls are divs - Tab cannot reach them
- Current page shown by colour only - no aria-current
- Previous and Next are symbols only - no accessible name
- No announcement when the page changes
What to do instead
- nav with aria-label="Search results pagination" - landmark and context
- Real buttons for Previous, Next, and each page number
- aria-current="page" on the active page button
- Active page uses border and weight - not colour alone
- aria-live region announces Page X of 7; results region updates politely
Impact
Search and catalogue pages depend on pagination. Inaccessible controls block keyboard users from reaching other results pages, and silent page changes leave screen reader users on stale content.
How to test
Tab through the good demo - every control should be reachable. On page 1, Previous should be disabled. Press Page 3 - listen for Page 3 of 7 and confirm results update. Inspect aria-current on the active page button.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Drag and Drop
Kanban boards and sortable lists often rely on dragging. If there is no other way to move items, keyboard and voice users cannot use the feature at all.
Understand this pattern
Who is affected
- Keyboard-only users who cannot drag cards
- Screen reader users who need labelled columns and move feedback
- People using voice control or switch access
- Users with motor disabilities who cannot perform precise drag gestures
Designer note
Keep drag for mouse users, but add a Move menu (or similar) on every draggable item. Label columns and announce when items change position.
What breaks
- Dragging is the only way to move cards - no keyboard alternative
- Columns have no role or label - no board context for screen readers
- Cards lack position labels - no “in To Do” context
- aria-grabbed / aria-dropeffect without a working AT path - historical dead ends
- No announcement when a card moves
What to do instead
- role="region" aria-label="Task board" - clear landmark
- Column groups with dynamic labels like "To Do - 2 tasks"
- Move button opens a keyboard menu - same outcome as drag
- Escape closes the menu without moving the card
- Screen reader hears “Write tests moved to In Progress”; focus returns to Move
Impact
Task boards and file uploads often assume dragging. Teams that ship drag-only UIs exclude keyboard users and fail WCAG 2.5.7 unless an equivalent control exists.
How to test
Tab to a Move button, press Enter, choose a destination, and confirm the card moves with an announcement. Press Escape with the menu open - the card should stay put. Drag with a mouse on the good demo - both paths should work.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Radio Group & Checkbox Group
Size and topping choices are common on order forms. If options are only styled divs with naked inputs, screen reader users hear “radio button” with no context about the group or the choice.
Understand this pattern
Who is affected
- Blind and low-vision users who rely on programmatic labels
- Screen reader users who need fieldset/legend for group context
- Keyboard users who need visible focus on each native input
- People with cognitive disabilities who benefit from clear group headings
Designer note
Use a visible legend for every radio or checkbox group. Pair each input with a label - do not rely on colour-filled custom boxes alone.
What breaks
- Group titles are plain paragraphs - not tied to inputs
- No fieldset or legend - group context is missing
- No label elements - only adjacent text beside inputs
- Checkbox visuals can change while the input stays unchecked
- Screen readers announce generic “radio button” with no option name
What to do instead
- fieldset and legend name each group - “Choose your size”, “Add toppings”
- Every input has a label via htmlFor - option names are announced
- Native radio and checkbox inputs - arrow keys and Space work as expected
- Visible focus rings on each control
- ScreenReaderOutput confirms size changes and topping toggles
Impact
Order and survey forms often ship custom-styled controls without labels. Users cannot tell which size or toppings are selected without seeing the screen.
How to test
Tab into the good demo’s radio group - the legend should be announced. Press Arrow Down between radios. Tab to checkboxes and press Space - listen for checked/unchecked. Confirm the bad demo announces no option names.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Search
A search bar filters articles as you type. If results only appear visually - with no label, search landmark, or status announcement - blind users cannot tell whether anything matched.
Understand this pattern
Who is affected
- Blind users who rely on live regions and proper roles for dynamic results
- Keyboard users who need arrow keys and Enter to pick a result
- Screen reader users who need the search button and field named in code
- People with cognitive disabilities who benefit from explicit result counts
Designer note
Pair the field with a visible label, not only a placeholder. Announce result counts in a polite live region. Use a real list structure or listbox/options so results are navigable.
What breaks
- Input has no label - only a placeholder that disappears when typing
- No role="search" landmark - the search region is not identifiable
- Results in plain divs - no listbox semantics and no keyboard structure
- Result count updates visually but is never announced
- Search button is icon-only with no accessible name
What to do instead
- role="search" wraps the field, button, and results
- Label "Search articles" is always available via htmlFor
- role="listbox" and role="option" with aria-controls and aria-activedescendant
- aria-live="polite" status announces match counts and empty states
- aria-label="Search" on the button; icon marked aria-hidden="true"
Impact
Search is the fastest way to find content on docs, shops, and support sites. Silent filters leave screen reader users guessing whether the page changed or the query failed.
How to test
Tab to the good demo input - hear "Search articles". Type a few letters - the live region should announce how many results matched. Press Arrow Down to enter the list, Enter to select, Escape to clear. Try a query with no matches and confirm "No results for …" is announced.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Infinite Scroll / Load More
Article feeds often load more rows when you scroll. If new items appear silently and focus never moves, blind and keyboard users miss content with no way to trigger or discover it.
Understand this pattern
Who is affected
- Blind users who need aria-live announcements when lists grow
- Keyboard users who cannot trigger scroll-only loading
- Screen reader users who lose their place when DOM updates are unannounced
- People with motor disabilities who need a visible loading state and button target
Designer note
Prefer an explicit Load more button over scroll-only fetching. Announce loading, success counts, and end-of-list states. Keep focus predictable - do not yank focus to new rows automatically.
What breaks
- More items load on scroll with no status message - screen readers stay silent
- No Load more button - keyboard users cannot load intentionally
- Focus stays put while the DOM grows - no path to new articles
- No loading indicator - users cannot tell a fetch is in progress
- End of list is never announced when everything is loaded
What to do instead
- section aria-label="Articles" with a semantic ul/li list
- aria-live="polite" on the list plus a dedicated status region
- Load more button with aria-label - disabled with "Loading..." while busy
- aria-busy="true" on the list and spinner while fetching
- Announces counts ("4 more articles loaded…") and "All 12 articles loaded." at the end
Impact
Infinite scroll is common on news, social, and catalog pages. Silent appends strand keyboard users and hide new content from assistive technology unless you offer an accessible alternative.
How to test
Tab to Load more in the good demo and press Enter - hear "Loading more articles", then the loaded count. Confirm focus remains on the button. Tab through new article links. Load until the button is replaced and "All 12 articles loaded." is announced. In the bad demo, scroll the list and note the silence.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
File Upload
Profile photo uploads often rely on drag-and-drop alone. If the drop zone is a plain div and feedback is visual only, keyboard and screen reader users cannot upload or hear whether it worked.
Understand this pattern
Who is affected
- Keyboard-only users who cannot reach drag-only drop zones
- Blind users who need live region announcements for file selection and errors
- Screen reader users who need the control named as a button or labelled input
- People using voice control who rely on accessible names for upload targets
Designer note
Use a label-wrapped file input or an explicit button that opens the picker. Never rely on display:none for the only path to upload. Announce file names, progress, success, and errors.
What breaks
- Drop zone is a div - not in the tab order and not announced as interactive
- File input is hidden with no associated label via htmlFor
- Drag-and-drop only - no keyboard equivalent to open the picker
- Success and error states are colour/text only - no live region
- Selected file name is visual only - screen readers are not told
What to do instead
- Label wraps sr-only input - entire zone is clickable and named
- Drag-and-drop on the drop zone with keyboard access via the file input
- aria-live="polite" announces selection, success, and error messages
- accept="image/*" with clear aria-label on the file input
Impact
Forms for jobs, healthcare, and account settings routinely require document or photo uploads. Inaccessible upload widgets block completion or leave users unsure whether a file was accepted.
How to test
Tab to the good drop zone - confirm a visible focus ring. Press Enter and choose an image - hear the file name in the live region. Upload and listen for success; use Simulate error to hear the failure message. In the bad demo, Tab past the zone and note silent feedback.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Skeleton / Loading State
Profile cards often show grey skeleton blocks while data loads. If those blocks have no status message and the swap to real content is silent, blind users cannot tell whether the page is still loading or already updated.
Understand this pattern
Who is affected
- Blind users who rely on aria-live regions for loading and completion
- Screen reader users who need spinners and skeletons excluded or labelled
- Keyboard users who need a named Reload control to retest async content
- People with cognitive disabilities who benefit from explicit busy states
Designer note
Mark the loading container aria-busy and announce start and end states. Keep skeleton shapes aria-hidden. Do not leave spinners unlabelled.
What breaks
- Skeleton blocks are plain divs - no role, label, or busy state
- aria-busy is missing while content fetches
- Loaded content appears with no status message
- Spinner icon has no accessible name
- Transition from skeleton to content is invisible to assistive technology
What to do instead
- aria-live="polite" and aria-atomic="true" on the profile container
- aria-busy toggles true during load and false when the profile is ready
- Skeleton placeholders are aria-hidden - only the status text is announced
- Announces "Loading profile, please wait." then the loaded profile summary
- Reload button has aria-label="Reload profile" to replay the sequence
Impact
Dashboards, social feeds, and account pages use skeleton UI everywhere. Without status messages, users assume the page is broken or miss new content entirely.
How to test
Press Reload in the good demo - hear "Loading profile, please wait." Inspect aria-busy in DevTools (true, then false after ~2 seconds). Confirm skeleton shapes are aria-hidden. Hear the loaded announcement with name and role. Compare the bad demo where Reload produces silence.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Alert Dialog
Deleting an account is irreversible. A generic dialog with focus on Delete and no warning association puts screen reader and keyboard users one keypress away from a mistake.
Understand this pattern
Who is affected
- Blind users who need alertdialog urgency and aria-describedby on the warning
- Keyboard users who must not land on the destructive button by default
- Screen reader users who rely on clear cancel vs confirm announcements
- People with motor disabilities who use Escape expecting cancel, not confirm
Designer note
Use alertdialog for destructive confirmations, not dialog. Point aria-describedby at the warning text. Default focus to Cancel. Trap Tab between only the dialog actions.
What breaks
- role="dialog" instead of role="alertdialog" - urgency is not conveyed
- aria-describedby missing - warning text is not tied to the dialog
- Focus defaults to Delete - destructive action is the first keypress
- No live region when the dialog opens - appearance may be silent
- Escape triggers delete instead of cancel - keyboard trap with harm
What to do instead
- role="alertdialog" with aria-labelledby and aria-describedby
- aria-haspopup on trigger - relationship to the confirmation
- Focus moves to Cancel on open - safe default for keyboard users
- Tab cycles only between Cancel and the labelled confirm button
- Escape cancels; assertive live region announces "Account deleted." on confirm
Impact
Account deletion, billing cancellations, and data wipes use confirmation dialogs daily. Wrong roles and focus order cause irreversible errors that users cannot see coming.
How to test
Open the good demo - confirm focus is on Cancel and role="alertdialog" in DevTools. Tab between the two buttons only. Press Escape - dialog closes, focus returns, cancellation is announced. Confirm delete and hear the assertive success message. Compare the bad demo where focus starts on Delete and Escape deletes.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
Breadcrumb
On a product page, a trail like Home › Electronics › Laptops › MacBook Pro 14" shows location. If separators are announced and nothing is marked current, screen reader users hear noise without context.
Understand this pattern
Who is affected
- Blind users who need nav landmarks and aria-current for orientation
- Screen reader users bothered by separator characters read as content
- Keyboard users who benefit from a predictable list of ancestor links
- People with cognitive disabilities who rely on clear hierarchy cues
Designer note
Use nav with aria-label="Breadcrumb", an ol/li structure, and aria-hidden separators. Mark the current page with aria-current="page". This pattern targets WCAG 2.4.8 (AAA) and 2.4.1 (AA) - valuable even when AAA is not required.
What breaks
- Plain div wrapper - no breadcrumb landmark
- Inline › characters - announced as content between links
- Current page is not marked - no aria-current="page"
- Links lack breadcrumb context - only isolated link names
- No accessible name on the navigation container
What to do instead
- nav with aria-label="Breadcrumb" - landmark and purpose
- ol and li - order of ancestors is preserved
- Separators use aria-hidden="true" - not read aloud
- aria-current="page" on the current item - "current page" in screen readers
- ScreenReaderOutput shows the full trail with current page called out
Impact
E-commerce and docs sites use breadcrumbs on deep pages. Incorrect markup adds verbosity without helping users understand location, especially on product and article detail views.
How to test
Tab through the good demo links - separators should not appear in the screen reader output panel. Inspect nav aria-label and ol in DevTools. Confirm aria-current="page" on MacBook Pro 14". Compare the bad demo where › characters are read between links.
Try it: broken vs accessible
Compare the two versions before you look at code.
Broken example. This broken example is interactive so you can try keyboard and mouse. It is hidden from assistive technology - use the accessible example for screen reader testing.
Accessible example
Loading interactive demo…
For designers on your team
Designer on your team? Share wcaginpractice.com/learn/for-designers — visual accessibility guidelines with no code required.
Keep learning
Context beyond the code
The playground shows how UI patterns break and how to fix them. The Learn section explains who benefits, Accessibility for Designers covers visual guidelines, CPACC prep maps each pattern to study resources, and Dev Tools (wcag-kit, Claude Skill, and WCAG Lens) bring patterns into your editor.