From the June 2026 accessibility audit. IMPORTANT: this group's adversarial verification step did not run (spend limit), so each finding needs verification against current code before fixing. (The cat-icon decorative-by-default and cat-button loading-state findings from this group already landed in #34.)
Clear button falls back to meaningless accessible name "Action" [serious]
Location: src/controls/search-bar.vue:8
search-bar.vue:8-9 enables the clickable right icon (icon-right-clickable) but never passes icon-right-aria-label, so the clear button takes cat-input's fallback name 'Action' (input.vue:29 :aria-label="iconRightAriaLabel || 'Action'"). A screen-reader user tabbing through the search bar hears 'Action, button' with no indication it clears the query.
Recommendation: In search-bar.vue add icon-right-aria-label="Clear search" to the cat-input binding. Separately, consider changing input.vue's fallback from the meaningless 'Action' to requiring the label (warn in dev) so future call sites cannot ship an unnamed control.
Keyboard focus is lost when the clear button is activated [moderate]
Location: src/controls/search-bar.vue:58
clearSearch (search-bar.vue:58-60) emits null, which makes modelValue falsy, which removes the iconRight (search-bar.vue:8 :icon-right="modelValue ? 'close-circle' : undefined"), unmounting the very button that has keyboard focus. Focus falls back to <body>, stranding keyboard and screen-reader users.
Recommendation: Give the cat-input a template ref and call its exposed focus() (input.vue:197-201) inside clearSearch after emitting, so focus returns to the search input — which is also where a user wants to be after clearing.
No typed way to give the search input an accessible name; demos rely on placeholder only [moderate]
Location: src/controls/search-bar.vue:28
cat-search-bar's Props (search-bar.vue:28-40) declare only modelValue and placeholder — there is no ariaLabel passthrough to cat-input (which supports it, input.vue:79). At runtime aria-label fallthrough happens to be absorbed as cat-input's prop, but strictTemplates consumers get a type error, and every playground usage (e.g. playground/app/pages/controls/search-bar.vue:12,53,92) labels the input by placeholder alone — placeholder text disappears once a value is typed and is a weak name source.
Recommendation: Declare an ariaLabel?: string prop on cat-search-bar (defaulting to undefined) and bind it :aria-label="ariaLabel" on the cat-input, mirroring the documented convention in button.vue:147-151. Update playground demos to pass it (e.g. aria-label="Search products").
Toggle state exposed only via a state-describing label, with no aria-pressed; icon not hidden [moderate]
Location: src/controls/theme-toggle.vue:3
The button text is {{ isDark ? 'Dark' : 'Light' }} Mode (theme-toggle.vue:9), i.e. the label names the CURRENT state, and there is no aria-pressed. A screen-reader user hears 'Dark Mode, button' and cannot tell whether pressing it enables dark mode or whether dark mode is already on (pressing it actually turns dark mode OFF — the inverse of the Play/Pause convention where the label names the action). Additionally the raw icon markup (theme-toggle.vue:6-8 <span class="icon"><i class="mdi ...">) has no aria-hidden, so the MDI glyph can leak into the name.
Recommendation: Either (a) keep a fixed label 'Dark mode' and bind :aria-pressed="isDark" (requires the ariaPressed prop on cat-button, see separate finding), or (b) keep the changing label but make it action-phrased: Switch to {{ isDark ? 'light' : 'dark' }} mode. In both cases add aria-hidden="true" to the icon span at theme-toggle.vue:6.
Copy success/failure is never announced [moderate]
Location: src/controls/safelink.vue:52
clipboard() (safelink.vue:52-67) writes to the clipboard and emits a copy event, but the component renders no status feedback of its own — visually or via a live region — and failures only go to console.error (safelink.vue:54,64). A screen-reader user who activates 'Copy to clipboard' gets no confirmation anything happened unless every consumer remembers to wire the copy event to a toast.
Recommendation: Render a visually-hidden <span role="status"> from mount inside the component (consistent with the datepicker's role=status approach) and set its text to 'Copied to clipboard' on success / 'Copy failed' on the catch branch, clearing after a few seconds. Keep emitting copy for consumers who want richer UI.
All instances share identical action names with no context of what they act on [moderate]
Location: src/controls/safelink.vue:11
The copy button is always aria-label="Copy to clipboard" (safelink.vue:11) and the link always aria-label="Open URL in new tab" (safelink.vue:22). safelink is typically rendered many times per page (URL lists/tables — see the playground page's multiple instances), so a screen-reader elements list shows N indistinguishable 'Copy to clipboard' buttons and N 'Open URL in new tab' links, and the link's name doesn't include its destination.
Recommendation: Make the labels computed to include the subject, e.g. :aria-label="Copy ${text || sanitizedUrl} to clipboard" and :aria-label="Open ${sanitizedUrl} in new tab" (or use aria-labelledby pointing at the .cat-safelink-desc div plus a static suffix).
Search input is type=text with no searchbox role or search landmark affordance [minor]
Location: src/controls/search-bar.vue:6
search-bar.vue:6 hardcodes type="text", so the input exposes a plain textbox role even though the component is semantically a search field; there is also no <search>/role="search" wrapper, so screen-reader landmark navigation cannot jump to it. cat-input already supports type="search" (input.vue:67).
Recommendation: Change the binding to type="search" (gives the implicit searchbox role and native clear behavior in some browsers). Optionally wrap the cat-input in a <search> element inside the component, or document that page-level consumers should place it inside one — only if a single search per page context is expected; otherwise leave landmarks to consumers.
No typed props for toggle/disclosure ARIA states (ariaPressed, ariaExpanded, ariaHaspopup, ariaControls) [minor]
Location: src/controls/button.vue:147
cat-button declares only ariaLabel and title (button.vue:152-159). There is no typed way to build a toggle button (aria-pressed) or popup trigger (aria-expanded/aria-haspopup/aria-controls) with cat-button under strictTemplates — the datepicker had to use a raw <button> for its toggle (datepicker.vue:37-44), and cat-theme-toggle cannot adopt aria-pressed. Runtime fallthrough via v-bind="$attrs" (button.vue:10) works but fails consumer typechecks, steering consumers away from the shared focus-visible styling at button.vue:250-261.
Recommendation: Add optional ariaPressed?: boolean, ariaExpanded?: boolean, ariaHaspopup?: 'dialog' | 'menu' | 'listbox' | 'grid' | 'tree' | boolean, and ariaControls?: string props bound onto the native button (omitting when undefined), following the existing ariaLabel pattern.
Filed from the catenary widget accessibility audit session, 2026-06-10 (context: interline-io/calact-network-analysis-tool#390).
From the June 2026 accessibility audit. IMPORTANT: this group's adversarial verification step did not run (spend limit), so each finding needs verification against current code before fixing. (The cat-icon decorative-by-default and cat-button loading-state findings from this group already landed in #34.)
Clear button falls back to meaningless accessible name "Action" [serious]
Location:
src/controls/search-bar.vue:8search-bar.vue:8-9 enables the clickable right icon (
icon-right-clickable) but never passesicon-right-aria-label, so the clear button takes cat-input's fallback name 'Action' (input.vue:29:aria-label="iconRightAriaLabel || 'Action'"). A screen-reader user tabbing through the search bar hears 'Action, button' with no indication it clears the query.Recommendation: In search-bar.vue add
icon-right-aria-label="Clear search"to the cat-input binding. Separately, consider changing input.vue's fallback from the meaningless 'Action' to requiring the label (warn in dev) so future call sites cannot ship an unnamed control.Keyboard focus is lost when the clear button is activated [moderate]
Location:
src/controls/search-bar.vue:58clearSearch(search-bar.vue:58-60) emits null, which makesmodelValuefalsy, which removes the iconRight (search-bar.vue:8:icon-right="modelValue ? 'close-circle' : undefined"), unmounting the very button that has keyboard focus. Focus falls back to<body>, stranding keyboard and screen-reader users.Recommendation: Give the cat-input a template ref and call its exposed
focus()(input.vue:197-201) insideclearSearchafter emitting, so focus returns to the search input — which is also where a user wants to be after clearing.No typed way to give the search input an accessible name; demos rely on placeholder only [moderate]
Location:
src/controls/search-bar.vue:28cat-search-bar's Props (search-bar.vue:28-40) declare only
modelValueandplaceholder— there is noariaLabelpassthrough to cat-input (which supports it, input.vue:79). At runtimearia-labelfallthrough happens to be absorbed as cat-input's prop, but strictTemplates consumers get a type error, and every playground usage (e.g. playground/app/pages/controls/search-bar.vue:12,53,92) labels the input by placeholder alone — placeholder text disappears once a value is typed and is a weak name source.Recommendation: Declare an
ariaLabel?: stringprop on cat-search-bar (defaulting to undefined) and bind it:aria-label="ariaLabel"on the cat-input, mirroring the documented convention in button.vue:147-151. Update playground demos to pass it (e.g.aria-label="Search products").Toggle state exposed only via a state-describing label, with no aria-pressed; icon not hidden [moderate]
Location:
src/controls/theme-toggle.vue:3The button text is
{{ isDark ? 'Dark' : 'Light' }} Mode(theme-toggle.vue:9), i.e. the label names the CURRENT state, and there is noaria-pressed. A screen-reader user hears 'Dark Mode, button' and cannot tell whether pressing it enables dark mode or whether dark mode is already on (pressing it actually turns dark mode OFF — the inverse of the Play/Pause convention where the label names the action). Additionally the raw icon markup (theme-toggle.vue:6-8<span class="icon"><i class="mdi ...">) has no aria-hidden, so the MDI glyph can leak into the name.Recommendation: Either (a) keep a fixed label 'Dark mode' and bind
:aria-pressed="isDark"(requires the ariaPressed prop on cat-button, see separate finding), or (b) keep the changing label but make it action-phrased:Switch to {{ isDark ? 'light' : 'dark' }} mode. In both cases addaria-hidden="true"to the icon span at theme-toggle.vue:6.Copy success/failure is never announced [moderate]
Location:
src/controls/safelink.vue:52clipboard()(safelink.vue:52-67) writes to the clipboard and emits acopyevent, but the component renders no status feedback of its own — visually or via a live region — and failures only go to console.error (safelink.vue:54,64). A screen-reader user who activates 'Copy to clipboard' gets no confirmation anything happened unless every consumer remembers to wire thecopyevent to a toast.Recommendation: Render a visually-hidden
<span role="status">from mount inside the component (consistent with the datepicker's role=status approach) and set its text to 'Copied to clipboard' on success / 'Copy failed' on the catch branch, clearing after a few seconds. Keep emittingcopyfor consumers who want richer UI.All instances share identical action names with no context of what they act on [moderate]
Location:
src/controls/safelink.vue:11The copy button is always
aria-label="Copy to clipboard"(safelink.vue:11) and the link alwaysaria-label="Open URL in new tab"(safelink.vue:22). safelink is typically rendered many times per page (URL lists/tables — see the playground page's multiple instances), so a screen-reader elements list shows N indistinguishable 'Copy to clipboard' buttons and N 'Open URL in new tab' links, and the link's name doesn't include its destination.Recommendation: Make the labels computed to include the subject, e.g.
:aria-label="Copy ${text || sanitizedUrl} to clipboard"and:aria-label="Open ${sanitizedUrl} in new tab"(or usearia-labelledbypointing at the.cat-safelink-descdiv plus a static suffix).Search input is type=text with no searchbox role or search landmark affordance [minor]
Location:
src/controls/search-bar.vue:6search-bar.vue:6 hardcodes
type="text", so the input exposes a plain textbox role even though the component is semantically a search field; there is also no<search>/role="search"wrapper, so screen-reader landmark navigation cannot jump to it. cat-input already supportstype="search"(input.vue:67).Recommendation: Change the binding to
type="search"(gives the implicitsearchboxrole and native clear behavior in some browsers). Optionally wrap the cat-input in a<search>element inside the component, or document that page-level consumers should place it inside one — only if a single search per page context is expected; otherwise leave landmarks to consumers.No typed props for toggle/disclosure ARIA states (ariaPressed, ariaExpanded, ariaHaspopup, ariaControls) [minor]
Location:
src/controls/button.vue:147cat-button declares only
ariaLabelandtitle(button.vue:152-159). There is no typed way to build a toggle button (aria-pressed) or popup trigger (aria-expanded/aria-haspopup/aria-controls) with cat-button under strictTemplates — the datepicker had to use a raw<button>for its toggle (datepicker.vue:37-44), and cat-theme-toggle cannot adopt aria-pressed. Runtime fallthrough viav-bind="$attrs"(button.vue:10) works but fails consumer typechecks, steering consumers away from the shared focus-visible styling at button.vue:250-261.Recommendation: Add optional
ariaPressed?: boolean,ariaExpanded?: boolean,ariaHaspopup?: 'dialog' | 'menu' | 'listbox' | 'grid' | 'tree' | boolean, andariaControls?: stringprops bound onto the native button (omitting when undefined), following the existing ariaLabel pattern.Filed from the catenary widget accessibility audit session, 2026-06-10 (context: interline-io/calact-network-analysis-tool#390).