Skip to content

fix(webapp): basic accessibility improvements - #1236

Open
acoseac wants to merge 1 commit into
desec-io:mainfrom
acoseac:a11y/basic-improvements
Open

fix(webapp): basic accessibility improvements#1236
acoseac wants to merge 1 commit into
desec-io:mainfrom
acoseac:a11y/basic-improvements

Conversation

@acoseac

@acoseac acoseac commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Closes #472

This picks up the three items in #472. It builds on #890 by @xyhhx, which got a review but went quiet and no longer merges after the Vuetify 3 migration. Following that review, accessible names are added only where there is no visible text, rather than duplicating text the accessible name is already computed from — a <v-btn aria-label="Log In">Log In</v-btn> gains nothing and can hurt voice control.

Accessible names

  • The mobile navigation toggle in App.vue had no name (it shows as a bare button with an img in the accessibility tree). It now has aria-label plus aria-expanded, since it toggles the drawer.
  • The create button in the CRUD lists (domains, tokens) is icon-only and unnamed; it now uses the list's own headlines.create, so it reads as "Create New Domain" rather than nothing.
  • Two dialog close controls were bare <v-icon> elements carrying a click handler. Those are not focusable and not exposed as controls at all, so the dialogs could not be closed by keyboard via that affordance. They now use the same v-btn icon variant="text" aria-label="Close dialog" pattern already used by the close control in TOTPVerifyDialog.vue.

Worth noting what is not here: the show/hide password toggles get Vuetify's generated name, "Password appended action". That is poor but not missing, and fixing it properly means replacing five @click:append-inner handlers with labelled buttons in the append-inner slot. Happy to do it in a follow-up if you want it.

Headings

The page titles were <v-toolbar-title>, which renders a <div>. The login page, for example, had no heading element at all, so there was nothing to navigate by. Vuetify's tag prop fixes this with no visual change:

  • 10 page titles become <h1>; the two on the donate page become <h2>, since that page already has an <h1>
  • the four numbered step titles on the domain setup page become <h2>

/custom-setup/{domain} went from zero headings to:

H1 Setup Instructions for example.com
  H2 Configure your DNS records
  H2 Delegate your domain
  H2 Enable DNSSEC
  H2 Find Help
  H2 Keep deSEC Going

This is what @robin24 suggested in the issue thread.

Progress bar

@nils-wisiol asked why it was irritating and couldn't see it visually. The reason isn't visual. VProgressLinear renders aria-hidden="false" whenever it is active, so on every single request an unnamed progressbar enters the accessibility tree and leaves again. It carries no information a screen reader user can act on — the outcome is announced by the result or the error alert — so it is marked decorative.

That keeps the bar exactly as it is for sighted users, rather than removing it as was floated. If you'd rather it went away entirely, or want a spinner instead as @peterthomassen suggested, say so and I'll change it.

Verification

Checked in a browser against the accessibility tree and the DOM, not just by reading source:

  • login page before: no heading, unnamed nav button. After: H1 Log In, nav button named with aria-expanded
  • progress bar A/B with work_count driven directly: on main it is aria-hidden="false" while working; with this branch it stays "true". Visual state (height: 0, no --active class) is identical on both, so this changes assistive-technology exposure and nothing else
  • donate page outline verified as H1 → H2, no unnamed buttons
  • npm run build and npm test (4 passed) pass

Two caveats I'd rather state than paper over. The CRUD create/destroy dialogs and DomainSetupDialog are behind login, so I verified those by source and by matching the existing TOTPVerifyDialog pattern, not visually. And where a heading replaced an inline <span> inside a v-card-title (which is display: block), I kept d-inline on the heading so the layout is byte-identical rather than adding flex — semantics change, appearance does not.

Unrelated, noticed while working: npm run lint fails on main too, because the glob src/**/*.{vue,js,json} matches src/modules/qrcode.vue, which is a directory. Left alone here.

Accessible names: the mobile navigation toggle and the create button in
the CRUD lists are icon-only and were exposed to assistive technology
without a name. Two dialog close controls were bare <v-icon> elements
carrying a click handler, so they were neither focusable nor operable by
keyboard; they now use the same button pattern as the close control in
TOTPVerifyDialog.

Headings: page titles rendered as <div>, so views such as the login page
had no heading at all and screen reader users had nothing to navigate by.
Toolbar titles now render as <h1> through Vuetify's tag prop, and the
step titles on the domain setup page as <h2>. Neither changes appearance.

Progress bar: it appears on every request and, while active, was exposed
to assistive technology as an unnamed progressbar, which is the reported
irritation. It is decorative, so it is now hidden from the accessibility
tree; the visual behaviour is unchanged.

Follows the review on desec-io#890 by adding accessible names only where there is
no visible text, rather than duplicating text the accessible name is
already computed from.

Closes desec-io#472
@peterthomassen
peterthomassen force-pushed the a11y/basic-improvements branch from e100fbf to b9f4a8f Compare August 18, 2026 17:21

@peterthomassen peterthomassen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice contribution, thank you! Just one comment; pls let me know if you'd like to take a stab at it or rather not this time. Thanks!

(rebased)

Comment on lines +15 to +22
<v-btn
icon
variant="text"
aria-label="Close dialog"
@click.stop="close"
>
<v-icon :icon="mdiClose" />
</v-btn>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, this button now is focused when the dialog opens first:

Image

Clicking next to it removes the focus. Can you please make sure it isn't focused right away?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now realize this also happens for other dialog (like the 2FA setup email notice from TOTPVerifyDialog.vue). It's not required for this PR, but if you have a solution idea, that'd be appreciated!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a look at this one. It reproduces only when creating a domain: the create dialog closes as the setup dialog opens, so the element that had focus disappears, and Vuetify moves focus to the first focusable child, which is the close button. Opening the same dialog from the info icon on a domain row doesn't do it.

I tried some obvious one-liners, making the card itself focusable via tabindex or autofocus. Both stop the close button getting focus, but focus then lands on the + button behind the dialog, because the closing create dialog restores focus to its own activator. From there, tabbing walks straight into the table behind the overlay. That seems like a worse trade than the focus ring.

So maybe best to leave it as is here, and treating initial focus in dialogs as its own issue, including TOTPVerifyDialog since it has the same shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A11y: basic accessibility improvements

2 participants