Skip to main content

Accessibility

The web client aims at WCAG 2.2 level AA (design note 4.14 item 4). This document records the audit, what is checked automatically, the manual screen reader walkthrough, the known gaps and a template for the accessibility statement that operators may need to publish.

The audit fixed problems without redesigning the app: the layout and the palette stay as they are, only the tokens and the few styles that failed a contrast or size criterion were nudged (see “Design tokens” below).

LayerWhatWhere
Playwright + axe-core@axe-core/playwright scans the main screens with the tags wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22a, wcag22aa and best-practice. Serious and critical WCAG violations fail the test; moderate and minor findings and best-practice-only rules are logged and attached as axe-findings JSON. Runs on chromium, mobile-android and tablet-chromium as part of the normal Playwright run.e2e/tests/a11y.spec.ts
Playwright keyboard testFirst Tab shows the skip link, Enter moves to the message list; the create-channel dialog keeps Tab inside, closes with Escape and gives the focus back; the emoji picker opens from the keyboard, takes the focus and returns it; the media viewer keeps Tab inside; Tab leaves the whiteboard canvas (no trap); new messages of others are announced and the setting mutes them; <html lang> follows the locale.e2e/tests/a11y.spec.ts
Vitest (static)Every icon-only <button> has aria-label, aria-labelledby or title; every <img> has alt; every range input has a label and aria-valuetext; every role="dialog" is labelled; no aria-label on a plain span/div/p (ARIA prohibits names on generic elements); text fields, selects and text areas draw their border with line-strong, whose contrast against bg-0…bg-3 (read from styles.css) is at least 3:1 (WCAG 1.4.11).web/src/test/a11yStatic.test.ts
Vitest (behaviour)Dialog focus trap, Escape and focus return; the in-app confirmation (labelled, Cancel focused, Escape cancels, focus return) and the menu/popover dismissal (Escape, focus back on the button); field hint and error association; tab arrow keys; the feed and its announcements; the poll calendar (date grid: one tab stop, arrow keys, Home/End, Page Up/Down, Enter/Space, disabled past days) and the poll composer (labelled options, keyboard reordering with focus kept, errors described at their fields); message components (named buttons in reading order, disabled ones kept in the tab order with aria-disabled, the pending state as aria-busy, the select menu as a listbox with aria-activedescendant, arrow keys, Escape and the focus returned) and the application’s form (a labelled dialog with labelled fields, focus trap, Escape, errors described at their fields).web/src/components/a11y.test.tsx, web/src/stores/accessibility.test.ts, web/src/polls/Calendar.test.tsx, web/src/polls/PollComposer.test.tsx, web/src/messageComponents/*.test.tsx

Screens scanned (each on phone, tablet and desktop): sign-in, sign-in with an error, registration, forgot password, the e-mail confirmation page (without a link and with an invalid one), invite; a text channel with messages (formatting, a reply, a code block, an image attachment, a link), the navigation drawer (phone), the composer with the emoji picker, the composer with the formatting bar, the styled input and open mention suggestions, a message context menu, the media viewer, the member list, a modal dialog (create channel), the search panel with results, a files channel with entries, all room settings tabs, all user settings tabs, Discover, Home; friends with an incoming request, a direct message, the whiteboard, a joined voice channel (fake microphone and camera) muted and unmuted, the voice bar and account bar; the room header menu, the account menu (also a node admin’s) and the settings navigation with the admin link; every admin tab; the banner and the encryption page of a device that lost its keys (“Sign in again”). e2e/tests/files-e2ee.spec.ts scans an end-to-end encrypted files channel with entries, the “Your files” panel of a locked device (checkboxes named by kind, size and date; the delete buttons name their entry) and the channel deletion dialog with its file facts (the keep option is a labelled checkbox).

The feature specs scan their own views with the same rule (WCAG 2.2 A/AA tags; serious and critical violations fail), on the desktop and the phone project unless noted:

SpecViews scanned
rail-folders.spec.tsthe room rail with a folder collapsed and expanded
social-voice.spec.tsthe room card on the rail (desktop), the room’s long-press menu with the card head (phone), the friends list with a friend in voice
friend-invite.spec.tsthe friend menu with the open “Invite to room” submenu
channel-access-marks.spec.tsthe channel list with the private and restricted marks, the channel editor
view-as-role.spec.tsthe role preview’s banner, a previewed read-only channel page (the disabled composer with its reason)
permissions-layered.spec.tsthe channel and category permission editors
upload-policy.spec.tsthe role’s file types, the channel and category editors with their file type blocks, the download warning dialog
files-e2ee.spec.tsan end-to-end encrypted files channel with entries and its breadcrumbs, “Your files” of a locked device, the deletion dialog
history-restore.spec.tsthe requester’s banner, Room settings → History requests
age.spec.tsthe registration form, the room settings, the invite page, a locked channel, the date of birth dialog
threads.spec.tsa forum post open in the thread panel, the forum’s post list
polls.spec.tsthe poll composer (choice and date poll with the calendar), a choice poll with results and voters, a date poll with the answer grid
components.spec.tsa bot’s message with buttons, a select menu and a link button (closed and open list), the application’s form with a field error
reports.spec.tsa report’s detail with its action row
voice-stream-join.spec.tsthe voice stage with a stream placeholder (“Join stream”) and with a joined stream
whiteboard-voice.spec.tsthe voice stage with a whiteboard placeholder (“Join whiteboard”), a joined whiteboard, the presenter’s preview with the “Whiteboard is open” bar, and a whiteboard next to a focused stream
voice-ux.spec.tsthe voice stage with a participant muted for you (marker, header count) and while talking muted (the pulsing microphone button, the notice)
desktop-voice.spec.tsthe Tauri app’s Settings → Voice & video and a joined voice channel with the native engine (axe-core inside the app’s WebKitGTK webview)
apps/desktop-electron/e2e/smoke.spec.ts, electron-smoke.spec.tsthe Linux app’s screen-share picker window (axe-core inside the Electron shell; smoke.spec.ts fails on any WCAG violation, electron-smoke.spec.ts on serious and critical ones)

Run it alone (with E2E_NODE_BIN or E2E_NODE_IMAGE set, README.md “End-to-end tests”): cd e2e && npx playwright test tests/a11y.spec.ts --project chromium --project mobile-android --project tablet-chromium --workers=2. A11Y_REPORT_DIR=<dir> writes every finding as JSON, A11Y_REPORT_ONLY=1 turns the gate off (for a baseline).

Counts are axe nodes summed over the three projects (about 51 scans per project before, 52 after: the whiteboard was added to the scan and the files channel got entries). “Scans” is the number of screen scans with the finding.

RuleImpactNodes (scans)WhereFix
aria-required-childrencritical3 (3)Files channel: the empty file list had role="listbox" without optionsThe list is a list box with options; the inner open and menu affordances are no longer nested controls (see nested-interactive below)
color-contrastserious265 (50)Muted text fg-2 (#7c8494, 4.29:1 on the content background, 3.78:1 on inputs and hover), offline members (secondary text at 60 % opacity, 4.08:1)fg-2 → #8a92a2 (≥ 4.5:1 on bg-0…bg-3), offline rows at 70 % opacity
scrollable-region-focusableserious25 (23)Code blocks in messages, the settings and admin content areas, the emoji picker section barCode blocks tabindex="0"; settings/admin content area focusable; the emoji section bar has one tab stop with arrow keys
target-sizeserious21 (4)Tablet: check boxes squeezed by long labels; inputs squeezed to 26 px because the tablet layout put the main content into the 312 px navigation columnCheck boxes never shrink; the tablet layout keeps the navigation (rail and channels) in its column again (AppShell: the drawer is display: contents only from xl)
aria-prohibited-attrserious12 (6)aria-label on plain spans: voice flags, speaking glyph, lock icons, unread badgesrole="img" on icon badges and counters (the label is their text alternative); loading placeholders role="status"
link-in-text-blockserious2 (2)“Set up now” link in the security settings (color only)Underlined links inside text (security settings, encryption banners)
heading-ordermoderate127 (127)Settings sections were h3 under the page h1; member list h3/h4Sections h2; member list h2/h3; DM members h2
region, landmark-one-mainmoderate105 (33)Sign-in, registration, password reset, invite and add-application pages without a main landmarkrole="main" on those page containers
page-has-heading-onemoderate9 (9)Sign-in and registration had no h1The logo is the h1 (alt text “tavrn”)
aria-allowed-roleminor25 (25)The composer’s text area has role="combobox" (slash command palette)Kept on purpose (see known gaps)

Found during the fixes (screens not reached by the baseline scan) and fixed as well:

RuleImpactWhereFix
nested-interactiveseriousFiles: each option of the file list contained an open button and a menu buttonThose became pointer shortcuts (aria-hidden spans); keyboard: Enter opens, the context menu key or Shift+F10 opens the menu
aria-required-childrencriticalAn empty conversation: the feed had no articlesThe feed role is set while messages exist
button-namecriticalWhiteboard: Excalidraw’s menu button has an icon onlyThe client names it (“Canvas menu”)
skip-linkmoderateThe skip link pointed to #messages on pages without messagesIt targets #main-content and prefers the message list when there is one
color-contrastseriousWhite initials of picture-less avatars and room icons on bright hues (greens, yellows, cyans: down to 3.2:1)avatarBackground(): the same hue, darker only where 40 % lightness misses 4.5:1
RuleImpactNodes (scans)Note
serious / critical–0The gate fails on any
aria-allowed-roleminor34 (34)Composer textarea with role="combobox", see known gaps
regionmoderate3 (3)The context menu layer is rendered into body outside the landmarks (a popup)
landmark-contentinfo-is-top-levelmoderate2 (2)Inside Excalidraw (third party)

Before: 3 critical + 325 serious + 241 moderate + 25 minor nodes. After: 0 critical + 0 serious + 5 moderate + 34 minor.

RuleImpactWhereFix
target-sizeseriousFiles: the text-only breadcrumb buttons rendered below 24 CSS px with the fonts of the CI runner (WCAG 2.5.8)A minimum size of 24 × 24 px independent of the font (min-h-6 min-w-6 in files/FilesView.tsx); files-e2ee.spec.ts scans the breadcrumbs
(layout)–The emoji picker grew after its data had loaded and left the viewport above the composer (WCAG 1.4.10)A fixed panel size bounded by the viewport (components/EmojiPicker.tsx); the grid scrolls inside

What was built (checklist of design note 4.14 item 4)

Section titled “What was built (checklist of design note 4.14 item 4)”
  • Focus: one :focus-visible ring (2 px accent, offset 2 px) for every control, never removed; drawers, menus and dialogs move the focus in and back out.
  • Skip link “Skip to messages” / “Skip to main content”, visible on focus (.skip-link); main#main-content and the message list #messages take the focus. Screen-reader-only text uses Tailwind’s sr-only.
  • Dialogs (components/ui.tsx Dialog, the media viewer, the profile card): role="dialog", aria-modal="true", labelled by their title, focus trap (lib/focusTrap.ts), Escape, focus return; Escape closes only the topmost dialog. Questions before an action (deleting a message, leaving a group, closing a poll, …) use the in-app confirmation (components/Confirm.tsx, never the browser’s window.confirm): the shared Dialog, Cancel first with the focus, the confirming button red for deletions.
  • Popovers and menus close the same way everywhere (lib/dismiss.ts useDismiss): a press outside (pointer, touch or pen), Escape with the focus back on the button that opened them (only the innermost open one closes, and a dialog underneath stays open), and for fixed popovers scrolling or resizing.
  • Menus: context menus and the ”⋯” menus (ContextMenu) with role="menu", roving tabindex, arrow keys, Home/End, submenus, Shift+F10 and the context menu key; the presence menu got the same keyboard model and returns the focus to its button on Escape. Items that show a state (notification level, mute, roles, folder colour, a thread’s archive and lock, …) are menuitemcheckbox with aria-checked; choosing one keeps the menu open, so the new state is announced on the focused item, like the voice quick menus (e2e/tests/context-menus.spec.ts). The room header of the sidebar is one menu button inside the sidebar’s h2 (aria-haspopup="menu", aria-expanded; Enter or Space opens the room menu with its first item focused); its accessible name is the room name, the node of a remote room and the number of open reports.
  • Icons in the sidebar bars are SVG (2 px stroke, currentColor, components/icons.tsx) on 40 px targets; the voice bar’s padlock sits in the signal bars and their text alternative names encryption and quality (“Encrypted · connection good”), so neither depends on colour.
  • Room rail folders (components/RoomRail.tsx): rooms stay links in a list; a folder is a disclosure button (aria-expanded, aria-controls to the nested list of its rooms) named by its name, room count, mention or unread count and “contains the open room” (the folder colour is decoration only). ↑/↓, Home/End move through the rail, →/← expand, enter, leave and collapse folders, Enter/Space toggle, the context-menu key or Shift+F10 open the menus. Every drag-and-drop action is a menu entry too (WCAG 2.5.7). Badges and the drop indicator use danger-solid (3.2:1 on bg-0) and accent (6.97:1); targets stay 48 × 48 px. e2e/tests/rail-folders.spec.ts scans the rail with axe with a folder collapsed and expanded.
  • Channel order (pages/settings/ChannelTree.tsx, Room settings → Channels): the drag handles are named images (“Drag general to reorder”), every drag has real buttons as its equivalent (WCAG 2.5.7): “Move … up” / “Move … down” per channel and per category (24 × 24 px, 44 × 44 px on touch screens), the category dropdown of the channel editor for moves between categories. An arrow button keeps the focus while its row moves; during a save the tree is aria-busy and the buttons aria-disabled. The drop target’s ring uses accent; the dragged row is dimmed, not only coloured. e2e/tests/channel-order.spec.ts scans the tree with axe.
  • Room card and voice badges (components/RoomHoverCard.tsx, components/RoomRail.tsx): content on hover follows WCAG 1.4.13 and the client’s rule “no hover-only functionality”: the card opens on mouse hover after 300 ms and at once on keyboard focus (not on the focus of a click or tap), is role="tooltip" referenced by the room link’s aria-describedby while open, stays while the pointer moves onto it (hoverable), stays until the pointer leaves both or the focus leaves the room (persistent) and closes with Escape wherever the focus is (dismissible). It holds no controls. On touch the same content is the non-interactive head of the room’s long-press menu (a header menu entry: no stop of the arrow keys, no list roles inside the menu). Voice participants are listed with their names as text (avatars are decorative). The speaker and monitor badges are glyphs on ok/warn (7.65:1 and 9.24:1 against bg-0, glyph in bg-0 on them), the unread count badge is bg-0 text on fg-1 (9.7:1); their meaning is in the room’s accessible name (“12 unread, 3 in voice, someone is streaming”), never in colour alone. The friends list’s voice line is text (ok on bg-1, 7.12:1) with a decorative speaker icon; every friend has a ”⋯” menu button (aria-haspopup="menu", 40 px) besides right-click and long press. e2e/tests/social-voice.spec.ts scans the card (desktop), the room menu with the card head (phone) and the friends list with axe.
  • Invite to room (lib/menus.ts inviteToRoomEntry, friend and member menus): a submenu like the others (aria-haspopup="menu", → or Enter opens it, ← closes it, the room names as menu items; rooms of other nodes carry their domain as visible text in the item’s name). The result is a toast (status, or alert for a refusal), so it is announced. e2e/tests/friend-invite.spec.ts scans the friend menu with the open submenu with axe on the desktop and the phone project.
  • Joining a room and the room’s documents (components/JoinRoomDialog.tsx, components/RoomDocuments.tsx, pages/settings/RoomLegalTab.tsx): the join dialog is the shared modal Dialog (labelled “Join <room>”, focus trapped, Escape cancels, the first control takes the focus); the rules are a labelled, focusable scroll region and “I accept the rules” a labelled checkbox that enables Join. The privacy notice and the legal notice are disclosure buttons (aria-expanded, aria-controls, a decorative arrow, 40 px high) that open the document as a labelled, focusable region with its date as text. The settings tab’s editors are labelled fields with their limits as hints. e2e/tests/room-legal.spec.ts scans the editor, the join dialog, the member’s document dialog and the read-only tab with axe; web/src/components/a11y.test.tsx checks names, focus and the disclosure.
  • History restore after a key reset (components/HistoryRestoreBanner.tsx, pages/settings/HistoryRequestsTab.tsx): the requester’s banner is a labelled region with real buttons and a labelled message field; each request on the settings page is a labelled section with a definition list for the dates, the channels as a list and the identity check as a checkbox that enables “Approve” (no information by colour alone: partial coverage is spelled out). Warnings use the warn tokens on warn/10, like the other notices. e2e/tests/history-restore.spec.ts scans the banner and the page with axe.
  • File types and the download warning (pages/settings/RolesTab.tsx, ChannelsTab.tsx, components/DownloadWarningDialog.tsx): the role’s file types are labelled checkboxes in a fieldset (“Not set” is a checkbox of its own; while it is on, the list shows the inherited value disabled); the override block uses the same radio groups as the permission bits (named per type, the inherited value as accessible description, the symbols ✓/–/✗ never the only cue) and announces the result for the subject in a polite live region. The download warning is the shared modal Dialog (labelled by its title, focus trapped, Escape cancels) whose first button, Cancel, takes the focus. e2e/tests/upload-policy.spec.ts scans the role section, the channel and category editors and the dialog with axe on the desktop and the phone project.
  • Tabs: the sign-in/registration switch is a tab list with arrow keys and a tab panel (lib/a11y.ts tabListKeyDown); the settings and admin sections are navigation links with aria-current="page".
  • Pickers: the emoji picker is a dialog with a menu grid (arrow keys, Escape, focus return) and one tab stop for its section bar; it has a fixed size bounded by the viewport (at most 384 px high) and opens below or above its button, so it never reaches outside the viewport; the composer is a combobox for slash commands and mention suggestions (aria-controls, aria-activedescendant; the focus stays in the text box, 44 px rows above the input); search results are a list box driven from the search field.
  • The composer’s formatting bar is a labelled toolbar with one tab stop (arrow keys, Home, End), named buttons with their shortcuts in the tooltip and aria-keyshortcuts, and the “Formatting” and “styled input” toggles carry aria-pressed. The styled input’s highlight layer duplicates the text and is aria-hidden; screen readers read the textarea, whose own text stays the input (web/src/components/a11y.test.tsx, e2e/tests/a11y.spec.ts).
  • The Linux app’s screen-share picker (apps/desktop-electron/src/main/picker.ts, src/preload/picker.ts): a window of its own, outside the client, with a heading, the screens and the windows as two lists labelled by their headings, and one button per source named “Share &lt;name>” (the thumbnail is decorative); the first source has the focus, Escape and Cancel refuse, the focus ring is 3 px in the accent colour, targets are at least 44 px, it follows the light and dark theme and reduced motion, and its texts are English or German after the system’s language. The smoke tests of the Electron shell scan it with axe.
  • Message list: role="feed" with article rows (aria-posinset, aria-setsize, aria-busy while a page loads); new messages of others in the open conversation are announced in a polite live region (“New message from …”), muted with Settings → Notifications → Screen reader (device-local, stores/accessibility.ts).
  • Forms: every control has a label; Field links its hint and, while shown, the form’s error (errorId → aria-describedby, aria-invalid); errors are role="alert".
  • Sliders carry aria-valuetext with the unit (volumes, thresholds, audio progress); the microphone level is a role="meter" with a value text; the speaking bars are an image named “speaking”.
  • Toasts: role="status" (errors role="alert") inside a polite live region.
  • Icon buttons and badges: names on every icon-only button (translated), role="img" with a label on icon badges and unread counters, decorative avatars and icons alt=""/aria-hidden.
  • Channel access marks (AccessMark in components/Badges.tsx, icons NoEntryIcon and GroupOffIcon in components/icons.tsx): 16 px SVG glyphs in fg-2 (5.62:1 on the sidebar bg-1, 4.46:1 on the selection tint, above the 3:1 for graphics), each a role="img" named by its text with the same text as tooltip (“Visible only to certain roles”, “Joining only for certain roles”, “You can see this channel but not join it”, “Read only for you”). They are part of the channel link’s name (the voice row’s aria-label repeats them), so the meaning never depends on colour or the glyph. The dimmed name of a restricted channel uses fg-2 (4.5:1 or more on bg-1 and its hover) and returns to fg-0 as the open channel; the lock stays the encryption mark only. The channel editor’s line uses a decorative glyph next to the text. e2e/tests/channel-access-marks.spec.ts scans the channel list and the channel editor with axe on the desktop and the phone project.
  • Role preview (“View the room as a role”, components/RolePreviewBanner.tsx): the banner is a labelled region whose text (“Viewing as <role> · joined <date>”) is a polite live status, so starting the preview is announced; “What is simulated” is a disclosure button (aria-expanded, aria-controls) for the approximations (no hover-only tooltip), “Exit preview” a plain button, both at least 32 px high. The rights lists of voice and files channels say “allowed” / “not allowed” in words next to the decorative ✓/✗; the refused write is a status toast with a title and a reason. e2e/tests/view-as-role.spec.ts scans the banner and a previewed channel page with axe on the desktop and the phone project.
  • Spoilers (lib/markdown.ts): a hidden ||spoiler|| is a role="button" with a tab stop and the name “Spoiler, press to reveal”; Enter, Space or a click reveals it, after which role, tab stop and name are removed and the text reads as normal text. It has the focus ring, and the colour change is not animated under reduced motion. Spoiler attachments are covered by a real button (“Spoiler, click to reveal”).
  • Time stamps: <time datetime> with the full date and time as title.
  • Motion: a global prefers-reduced-motion rule ends every remaining animation and transition (drawers, pulses, the speaking glow, the composer growth, cue indicators); the speaking ring stays static.
  • Language: <html lang> follows the chosen locale (set at start and on every change).
  • Headings: one h1 per page (the channel or page title, the logo on sign-in), sections h2.
  • Date of birth and age gates (age.v1): the date of birth is a native date input with a visible label, a hint and an error text in its accessible description (aria-invalid while wrong); the blocking dialog is a labelled modal with the focus inside; the “18+” badge has the name “Adults only”; the locked channel is a labelled section with a heading. e2e/tests/age.spec.ts runs axe (WCAG A/AA, serious and critical fail) on the registration form, the room settings, the invite page, the locked channel and the dialog, on the desktop and the phone project.

Measured in web/src/styles.css (WCAG relative luminance). The client has one theme (dark); there is no light theme to measure.

TokenOnRatioStatus
fg-0 #f3f4f6bg-0…bg-317.2 … 12.9ok
fg-1 #b4bac6bg-0…bg-39.7 … 7.3ok
fg-2 #7c8494 → #8a92a2bg-0…bg-35.03 … 3.78 → 6.04 … 4.54nudged
accent #33aea6bg-0…bg-36.97 … 5.24ok
accent-text on accent6.28ok
white on danger-solid5.97ok
ok, warn, dangerbg-0…bg-3≥ 5.05ok
line #2f3542 (dividers, decorative)bg-11.43ok (not needed to identify anything)
line-strong #737b8c (borders of text fields, selects, text areas, the composer)bg-0…bg-34.45 … 3.34added (WCAG 1.4.11)
initials on hsl(hue 45% 40%)3.2 … 7.6 → ≥ 4.5bright hues darker
mention pill: accent-hover #40b9b1 on accent-soft over bg-0…bg-36.39 … 4.67ok (tavrn-web#122)
own / @everyone mention pill: fg-0 on 22 % warn over bg-0…bg-3; its warn border against the pill and the row11.2 … 8.2; ≥ 3.7ok (tavrn-web#122)

Script (run before a release; NVDA + Firefox or Chrome on Windows, VoiceOver + Safari on macOS/iOS, TalkBack + Chrome on Android, Orca + Firefox on Linux):

  1. Sign in: open /login. The page title, the heading “tavrn”, the tab list “Sign in or register” (two tabs, arrow keys switch) and the fields with their labels are announced. Submit a wrong password: the error is announced at once and read again as the description of the user name and password fields. Complete the second factor step (code field labelled “6-digit code”).
  2. Chat: press Tab once: “Skip to messages, link”; Enter lands in the message feed (“Messages, feed”); arrow keys / the virtual cursor read article by article (author, time, text; the full date is the time’s title). Have a second account send a message: it is announced once (“New message from …: …”). Turn the announcement off under Settings → Notifications → Screen reader and check it stays silent. Open a message’s menu with Shift+F10: “Message menu, menu”, arrow keys move, Escape returns to the message. Open the emoji picker from the composer’s emoji button: focus lands on the first emoji, Escape returns to the button. Open an image: “Image viewer”, Tab stays inside, Escape closes.
  3. Voice: join a voice channel from the channel list (link “Voice channel …”). The controls announce their state (“Mute, toggle button, not pressed”); the speaking participant’s tile reads “speaking”; muted participants read “Muted”. In Settings → Voice & video the volume sliders read “100 %” and the microphone meter reads its level in dB.

Results: the walkthrough could not be run in this environment (headless Linux without Orca, NVDA or VoiceOver). The automated checks cover the names, roles, states and live regions the script relies on; the walkthrough must be run by a person before claiming conformance.

  • The composer’s textarea has role="combobox" for the slash command palette; ARIA in HTML does not list it as allowed on textarea (axe aria-allowed-role, minor). Screen readers handle it well; kept on purpose.
  • The whiteboard canvas (Excalidraw) is a drawing surface: its shapes are not accessible to screen readers beyond the text list the client shows; Excalidraw’s own footer landmark is nested (moderate).
  • The context menu layer is rendered outside the landmarks (moderate region), as popups usually are.
  • Captions: not applicable (no prerecorded media in the app; live voice is not captioned).
  • Voice tiles show video without audio description (live content, exempt at AA).
  • No screen reader walkthrough has been run yet (see above).

Accessibility statement (template for operators)

Section titled “Accessibility statement (template for operators)”

Template. Operators that fall under the EU Web Accessibility Directive (public sector bodies) or the German Barrierefreiheitsstärkungsgesetz (BFSG, for certain consumer services from 28 June 2025) must publish an accessibility statement. Adapt the bracketed parts, check them against your deployment and your legal obligations, and link the statement from your legal pages. This is not legal advice.


Accessibility statement for [service name] ([https://your-node.example])

[Operator name] is committed to making [service name] accessible. This service runs tavrn [version], whose web client is designed to meet the Web Content Accessibility Guidelines (WCAG) 2.2 at level AA, which corresponds to the European standard EN 301 549 for web content.

Conformance status. The service is [fully / partially] compliant with WCAG 2.2 level AA. [Partially:] The content listed below is not yet accessible.

Non-accessible content.

  • The whiteboard is a drawing surface; drawings are not available as text beyond the written labels shown next to the board (WCAG 1.1.1).
  • Live voice and video conversations are not captioned (WCAG 1.2.4, live content).
  • [Content added by the operator, e.g. custom legal pages, room content posted by users, third-party embeds.]

Preparation of this statement. This statement was prepared on [date]. It is based on [a self-assessment / an assessment by a third party] using automated tests (axe-core) on phone, tablet and desktop layouts and [manual testing with screen readers]. It was last reviewed on [date].

Feedback and contact. If you notice barriers or need content in an accessible format, contact us: [e-mail], [phone], [postal address]. We aim to respond within [two weeks].

Enforcement procedure. [EU public sector: name and contact of the national enforcement body, e.g. in Germany the Schlichtungsstelle nach § 16 BGG.] [BFSG: the market surveillance authority responsible for your federal state (Marktüberwachungsbehörde der Länder for accessibility).]


This page is generated from tavrn-web/docs/ACCESSIBILITY.md (Apache-2.0) when the website is built; its links were adapted for this site.