Repository navigation
fix(ui): web accessibility fixes and axe gate in Storybook tests - #543
Merged
Merged
Conversation
Registry scripts (inline-component-source, generate-component-metadata, check-registry-integrity, verify-preview-stories) and the component page now find a @vllnt/ui component folder by name through apps/registry/lib/component-directory.ts, which searches the atomic level folders (atoms/molecules/organisms/templates) and still accepts today's flat layout. The Storybook scripts list component folders through the same kind of helper in packages/ui/scripts/lib/component-source.ts. The shim sibling-import rewrite now also matches `../../<level>/<name>/<name>` at any `../` depth, and the ESLint per-component overrides use name-based globs that match at any depth. Registry output is unchanged (public/r, registry/default, registry.json and component-metadata.json diff clean). Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Move every @vllnt/ui component folder into
packages/ui/src/components/{atoms,molecules,organisms,templates}/<name>/
(196 atoms, 70 molecules, 46 organisms, 1 template). The level is the higher
of the import-derived minimum and a conceptual promotion: self-contained
section-level widgets (full charts, maps and scaled timelines, pan/zoom
workspaces and viewers, data grids and trees) are organisms.
Relative imports (lib, .storybook, __tests__, sibling components, vi.mock
paths, the barrel and the top-level visual fixtures) follow the new depth;
ESLint re-sorted the affected import lines. src/components/index.ts keeps
identical export names and order. Committed visual baselines move with their
tests, the tsup exclude follows canvas-foundation-demo, and
conversation-thread.mdx pins its current docs ID with an explicit
<Meta title>. The registry and Storybook folder helpers drop the flat-layout
fallback, with a vitest test that every registry item resolves.
Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Brings in #536 (relative-import-free shims). The static sibling, dynamic import() and same-folder shim rewrites are all level-aware: dynamic imports share the `(?:../)+(?:<level>/)?<name>(?:/<name>)?` sibling path with static ones, and collectPublicExports keeps following src/index.ts -> components/index.ts (FlowCanvas included). The #536 edits to flow-diagram/index.ts land on organisms/flow-diagram/index.ts. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Add scripts/check-atomic-levels.mjs, a dependency-free check for a component package: every component folder sits in atoms/, molecules/, organisms/ or templates/; atoms import no component, molecules only atoms, organisms atoms/molecules/organisms, templates anything but templates; no cycles, no duplicate names, no barrel imports. Static imports, re-exports, import() and require() are read with a comment/string-aware lexer; stories, tests, visual fixtures, MDX and __tests__ are exempt. `--base <dir>` marks a directory below atoms (for the native package's primitives later). node --test fixtures cover a clean tree and every violation. `pnpm check:atomic` (turbo -> packages/ui) runs it, and the CI quality job runs the fixtures plus the check. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
COMPONENTS.md documents the {level}/{name} folder layout, the import rules
per level, how to choose a level (import-derived minimum plus the organism
promotion heuristic) and how to move a component between levels.
CONTRIBUTING, ARCHITECTURE, the registry README and docs page (en/fr), the
vllnt-ui skill and the agent gate lists (AGENTS.md, R6, PR playbook) point at
the new layout and `pnpm check:atomic`.
Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
…-a11y The branch was based on the unsquashed atomic-design branch, whose tree matches #537. Main's remaining delta (#538, #540) is applied on top. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Every story now runs axe-core (WCAG 2.0/2.1/2.2 A + AA tags) in the light and dark theme after render and fails on any violation. Page-scaffold rules (region, landmark-one-main, page-has-heading-one, bypass, document-title, html-has-lang) are off because a story canvas is a fragment. A story can only opt a rule out through parameters.a11y.config.rules with a reason. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
- Name every control and popup: Combobox/MultiSelect triggers take aria-label/aria-labelledby/id; Combobox, MultiSelect, DatePicker, DateRangePicker, TimePicker and ColorPicker name their popover dialog (new optional popoverLabel); date pickers open with focus on the selected day; progress bars, slider thumb, number-input steppers, video play button and data-table filter get names. - Replace aria-label on generic elements with sr-only text or a real role (animated/scramble/typewriter/spinning/text-reveal text, snap guides as img, selection halo as group, curriculum, map backdrop image). - Keyboard-reachable scroll regions (AI artifact content, activity strip, conversation log, Gantt chart, parallel timeline, ScrollArea viewport). - Fix nested interactive content (CanvasView is a focusable region, not a button; CommentPin, FileUpload label dropzone, geography quiz map group), choropleth regions are keyboard buttons outside aria-hidden, valid dl/ol/landmark structure (KeyConcept, ChronologicalTimeline, Globe3D, PrimarySourceViewer), 24px targets (RunTimeline, SearchField, Tour), Pagination as a nav of single links with aria-current. - GanttChart today caption on a 10% destructive tint (AA). - eslint: allow tabIndex on region/log/tabpanel roles and sections, and key handlers on the canvas section, since WCAG 2.1.1 requires scrollable regions to be focusable. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
- Shared roving-tabindex helper (lib/roving-focus): Tabs, AnimatedTabs and ViewSwitcher (Arrow/Home/End, aria-controls/aria-labelledby wiring), ThemeSwitcher, Rating and WorkspaceSwitcher radio groups, ListBox and the TimePicker columns (one tab stop, focus on the selected option, PageUp/ PageDown) and TreeView aria-activedescendant on stable item ids. - ModelSelector: search box inside the cmdk root so arrows and Enter reach the list. - CompletionDialog on the Radix dialog primitive (focus trap, Escape, focus return); shortcuts act inside the dialog only, Enter no longer always confirms, empty shortcut strings turn them off (WCAG 2.1.4). - Focus returns to the opener for CommandDialog, SearchDialog and ShareDialog (lib/use-return-focus); Drawer moves focus into its content (autoFocus defaults to true); AnimatedTooltip closes on Escape; Toast announces as a status/alert live region; CommandSeparator no longer puts role=separator inside the listbox; cmdk disabled styling matches data-disabled=true only. - ContextMenuTrigger documents the Shift+F10 keyboard path; real demos for the Command, ContextMenu and Drawer stories. - eslint: no-autofocus ignores component props (vaul autoFocus). Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
- styles.css: global reduced-motion safety net that collapses animations and transitions to their end state; data-motion="essential" opts out. - Move animation names out of inline styles (Marquee, Meteors, Particles, Sparkles, LiquidGlass, ShineBorder, BorderBeam) so motion-reduce:animate-none can stop them. - Guard infinite pulse/ping animations (AlertPulse, HandoffBeacon, LiveFeed, ObjectInspector, PresenceSyncIndicator, Skeleton, StateBadgeOverlay, SeverityBadge); Spinner keeps turning, slower, as essential loading feedback. - Marquee and TickerTape: keyboard-operable pause / play button (WCAG 2.2.2, new optional pauseControl + labels props), pause while focus is inside, stop under reduced motion. Marquee keeps the fade mask off the control. TickerTape change badges use emerald/rose-700 for AA contrast. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
…olours - Darken light-mode palette text on tints (emerald/rose/green/red-700, amber/orange/sky-800) in CandlestickChart, Watchlist, SparklineGrid, MarketTreemap, TransactionList, StatCard, OrderBook, Exercise, SeverityBadge (+ dark: variants), EraComparison, HistoricTimeline, StateBadgeOverlay and Comparison; white text sits on -700 fills (FollowMode, InteractiveTimeline, SeverityBadge solid, CommentPin unread badge, StepByStep). - Collaborator colours stay legible whatever the user picks: chips darken the colour with color-mix behind white text (LiveCursor, SelectionPresence) and ThreadBubble mixes the author colour toward the foreground. - Opacity-dimmed text uses the AA muted token (ObjectHandle, PropertySection, HistoricalFigureCard, LiveCursor status); PlaybackGhost fades its frame and keeps the label readable. - CodeBlock colours tokens from theme-aware --vllnt-code-* variables (AA in both themes, no JS theme detection, no Prism theme chunk); CodePlayground line numbers use the muted token and are aria-hidden. - FlowDiagram nodes take card / card-foreground colours via the xyflow node variables in both colour modes. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Give CodePlayground, HorizontalScrollRow, ShareSection and ProfileSection stories their required content so the headings and links they render are not empty (story IDs and names unchanged). Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Switch themes by waiting for the class on <html> instead of a render event, cap the animation settle at 1s, and give the test-runner a 60s per-story timeout for the two axe passes. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Switch themes by waiting for the class on <html> instead of a render event, cap the animation settle at 1s, and give the test-runner a 60s per-story timeout for the two axe passes. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
The embed previews for Combobox, MultiSelect, Select, Slider, InputOTP, NumberInput and InlineInput now pass an accessible name, and the ContextMenu demo trigger is focusable so Shift+F10 opens it. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Mixing in oklch toward black or a neutral token interpolated the hue toward 0deg (blue chips turned purple). Oklab darkens or lightens the same colour without rotating it; contrast is unchanged. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
…orts Registry shims rewrite ../../lib/* to "@vllnt/ui", but moveRovingFocus, useReturnFocus, focusCalendarDay and the checklist helpers used by progress-tracker were not public exports, so shadcn installs failed with TS2305. Export them, share collectPublicExports in lib/public-exports, and make registry:integrity fail when a shim imports a name the package does not export (14 failures before, 0 after). Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
The Radix-based CompletionDialog is page-modal by default (focus trap,
scroll lock, rest of the page inert). modal={false} keeps the page
interactive like the earlier container overlay.
Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
no-autofocus ignoreNonDOM applies to drawer only, the section key handlers to canvas-view only, tabIndex on log/region/section to the scroll containers and tabpanel to tabs; the global rules are back to their previous settings. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
Drop the document-wide reduced-motion rule from the shipped styles.css: it also froze animations the consuming app owns. Every library animation now carries its own motion-reduce guard; the seven spinning loaders are marked data-motion="essential" and only slow down. A Playwright CT test proves a consumer element keeps its animation under prefers-reduced-motion (it fails with the old global rule) while library pulses stop. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
The code palette failed 4.5:1 on 13 of 24 preset/mode backgrounds (e.g. nord dark comments 4.05). Express the --vllnt-code-* tokens as OKLCH channels and retune their lightness so each passes on the default theme and every preset in light and dark; theme-contrast.test.ts now checks the code tokens against each preset background (12 combos fail with the previous values). Presets and custom themes can still override the variables. Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
…scape - ScrollArea: the viewport is a tab stop only while its content overflows; aria-label / aria-labelledby name it as a region. - TabsContent: tabIndex=0 only when the panel holds no focusable content (re-checked with a MutationObserver). - TreeView: Home / End, ArrowRight to the first child of an open branch, ArrowLeft to the parent, and the active row scrolls into view. - CommentPin, PresenceStack, LiveCursor: darken collaborator colours with color-mix(in oklab) and fall back to theme tokens. - moveRovingFocus: arrows can leave a focused aria-disabled item. - AnimatedTooltip: Escape listens only while open, in the capture phase, and stops propagation so a parent dialog stays open. - Localizable names: Pagination labels, AIArtifact labels.content, ModelSelector listLabel, DataTableFilter ariaLabel, VideoEmbed playLabel (English defaults unchanged). Export PaginationLabels. Part of #535 Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
…de stories - AnimatedText: under prefers-reduced-motion the terminal, matrix and decipher variants show the final text at once (no JS typing or scramble), as the docs already promised. Tracks the media query with useSyncExternalStore. - ContentIntro / TutorialComplete HeadingOverride stories use h1 + h2 so the outline no longer skips a level. - NavigationMenu: real-browser test that ArrowDown on an open trigger moves focus into the content (Radix already does this; the earlier audit probe did not recognise the content as a popup). - Pagination: a label passed as undefined keeps its English default. Part of #535 Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
- CHANGELOG (root and packages/ui): the axe gate, the new optional props and exports, the registry release requirement, and the behaviour changes (Drawer autoFocus, CompletionDialog modal, Marquee/TickerTape pause button and its position, Pagination structure, CodeBlock colour variables, CommandSeparator, reduced motion, Tabs/ScrollArea tab stops, TreeView keys). - Registry NavigationMenu preview: the links had no href, so they were not focusable and ArrowDown on an open trigger had nowhere to go. Part of #535 Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
The combobox role requires aria-controls. Radix added it at runtime, but the trigger now sets it itself and the popover content takes the same id (the MultiSelect pattern), so static checks (react-doctor in CI) see it too. Part of #535 Claude-Session: https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc
bntvllnt
marked this pull request as ready for review
October 1, 2026 18:29
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the real web accessibility defects found by the #535 audit (axe on every story and registry embed in both themes, keyboard/APG sampling, reduced-motion sampling) and makes Storybook enforce it: the test-runner now runs axe-core (WCAG 2.0/2.1/2.2 A + AA) on every story in the light and dark theme and fails on any violation.
Part of #535
Builds on #537 (atomic layout) and #538 (token contrast), both merged; main (through #541/#542) is merged in, so this is no longer stacked.
What changed
.storybook/test-runner.jsinjects axe-core (already a devDependency) after each story renders, checks light + dark, fails on violations; page-scaffold rules off; per-story exceptions only viaparameters.a11y.config.rules[{ id, enabled: false, reason }](reason required)canvas-snapguides--*aria-prohibited-attr in both themes); full CLI run ona4323f28: 317 suites, 770 of 770 stories pass in light + darkaria-label/aria-labelledby/idforwarding, new optionalpopoverLabel(sensible English default), labels on steppers/filters/progress bars, slider name on the thumb,PopoverContentTSDoc; localizable names: Paginationlabels, AIArtifactLabelscontent, ModelSelectorlistLabel, DataTableFilterariaLabel, VideoEmbedplayLabel(English defaults unchanged)aria-labelon generic elements (AnimatedText, ScrambleText, Typewriter, SpinningText, TextReveal, SnapGuides, SelectionHalo, Curriculum, Map2D image); unfocusable scroll regions (AIArtifact, BottomActivityStrip, ConversationThread, GanttChart, ParallelTimeline, ScrollArea); nested interactive (CanvasViewrole=button, CommentPin, FileUpload, GeographyQuizMap); ChoroplethMap focusables insidearia-hidden; KeyConceptdl; ChronologicalTimelineli; Globe3D duplicate landmark/ids; PrimarySourceViewer nestedaside; 24px targets (RunTimeline, SearchField, Tour); Paginationa > buttonaria-hiddenvisuals or real roles (img/group);tabIndex=0+ region names + focus rings; the ScrollArea viewport is a tab stop only while it scrolls and a named region when givenaria-label/aria-labelledby; TabsContent is a tab stop only when it holds no focusable content; CanvasView is a labelled focusable region (R15: not a button); FileUpload dropzone is a<label>around the native input; choropleth regions are keyboard buttons (aria-pressed, Enter/Space); validdl/li; unique ids;sectionregion; 24px hit areas; Pagination is anavof single links witharia-current; Combobox setsaria-controlsitselfmoveRovingFocus, now exported), Arrow/Home/End(/PageUp/PageDown),aria-controlswiring, single tab stop on the selected option; arrows can leave a focusedaria-disableditem; TreeViewaria-activedescendantplus Home/End, ArrowRight to the first child, ArrowLeft to the parent and scroll-into-view; ModelSelector search inside the cmdk root; CompletionDialog on the Radix dialog primitive (trap, Escape, focus return; shortcuts scoped to the dialog, disable with""per WCAG 2.1.4; Enter no longer always confirms; newmodalprop); focus returns to the opener (useReturnFocus, now exported); Drawer focuses its content; AnimatedTooltip handles Escape only while open, in the capture phase, and stops propagation so a parent dialog stays open; Toast is a status/alert live region; date pickers open on the selected day; ContextMenuTrigger documents Shift+F10 and the demo trigger is focusablerun-kb.mjs) before → after belowanimationoverrodemotion-reduce:animate-none(Marquee, Meteors, Particles, Sparkles, LiquidGlass, ShineBorder, BorderBeam); unguarded infinite pulse/ping/spin; Marquee/TickerTape hover-only pause (WCAG 2.2.2); AnimatedText JS typing/scramble ignored reduced motionmotion-reduce:guards (no global rule, so consumer elements are never touched); the 7 inline loaders (ChainOfThought, PromptInput, Toaster, AgentActivity, AIChatInput, NewsletterSignup, FlowControls) and Spinner aredata-motion="essential"and spin at 1.5s under reduced motion; Marquee/TickerTape get a keyboard pause/play button (new optionalpauseControl,labels), pause while focus is inside; AnimatedText shows the final text at once under reduced motionreduced-motion.visual.tsxunderreducedMotion: reduce: a consumer element keeps its own animation and transition (red with a global*rule), Skeleton stops, Spinner slows; real-browser check: AnimatedText decipher shows the final string at 0 ms and +400 msopacity-90, cmdkdata-[disabled]dimming every itemcolor-mix(in oklab, …)so any user colour keeps its hue and stays AA, theme tokens when no colour is given, muted token instead of opacity, CodeBlock colours from--vllnt-code-*OKLCH variables (no JS theme detection, no theme chunk), xyflow node colours from card tokens,data-[disabled=true]theme-contrast.test.tschecks every CodeBlock token at 4.5:1 on the background for all 24 preset × mode combinations (12 fail with the previous values); vitest where class-testablenext/imagestub exportedLink, heading-override stories skipped a level.storybook/next-image-stub.ts; ContentIntro/TutorialComplete HeadingOverride stories use h1 + h2; registry NavigationMenu demo links have anhrefstorybook:verifygreen; story IDs unchanged (registry:verify-previews: 313 previews resolve, 770 stories); axe incl. best-practice on the HeadingOverride stories: 0Non-obvious roles (R15): CanvasView
region+aria-roledescription="canvas"(WAI-ARIA landmark, notbutton, cf. PR #139); SnapGuidesimg, SelectionHalo/GeographyQuizMap svggroup; choropleth regionsbutton+aria-pressed(APG toggle button); TimePicker columnslistbox/optionwith roving tabindex (APG listbox); TreeViewaria-activedescendant(APG tree view); Toaststatus/alert(APG alert, ARIA live regions).CodeBlock palette across presets. The review asked for per-preset code colours where needed. Presets change the surrounding tokens (background etc.) but not
--vllnt-code-*, so one light and one dark set of defaults is checked against every preset's own background. Those defaults pass 4.5:1 in all 24 combinations, so no per-preset overrides were needed; the test fails if a future preset breaks that.Review follow-ups (independent review of this PR)
@vllnt/uidoes not exportmoveRovingFocus(+RovingFocusOptions,RovingOrientation),useReturnFocus(+ReturnFocusHandlers),focusCalendarDay,CHECKLIST_PROGRESS_EVENT,parseChecklistStorageValue. Newregistry:integritycheck: every named@vllnt/uiimport in every shim must be a public export (sharedcollectPublicExports). 14 shims fail before, OK after. Stale comment ininline-component-source.tsfixed86704802modal?: boolean(defaulttrue) mapped to Radixmodal7edf3274files:overrides (drawer, the six scroll containers, tabs, canvas-view)96c7942c*reduced-motion net could alter consumer elementsdata-motion="essential"; CT test with a consumer element01c262b1theme-contrast.test.ts, CSS comment corrected103e76deMarqueeLabels,TickerTapeLabelsexported;overflow-hiddenrestored on the Marquee root9448e0392195c36dautoFocusundocumentedf87051d72195c36dcolor-mix(in oklab, …)in CommentPin, PresenceStack, LiveCursor2195c36dlabels/*Labelprops with English defaults (Pagination, AIArtifactContent, ModelSelector, DataTable filter, VideoEmbed)2195c36d,a4323f28aria-disableditem2195c36dstopPropagation; tested inside a Radix Dialog (first Escape closes the tooltip only)2195c36dhref, so nothing in it was focusable. Demo fixed; CT test + Storybook real-browser checka4323f28,f87051d7useSyncExternalStoreon the media query)a4323f28a4323f28react-doctorerror (combobox role withoutaria-controls)aria-controlsitself, MultiSelect pattern76db9f1fAxe before → after
Audit scripts from #535 (
run-axe2.mjs: axe 4.11, WCAG A/AA 2.0–2.2 plus best-practice, page-scaffold rules off, both themes). Counts are violating nodes.ae65aab5; 152 of the WCAG nodes were token contrast, fixed by #538)33dc3bbe); the 4 best-practice nodes (heading order) are now fixed: 0 on re-scan of those stories008a9291)a4323f28The 2 remaining WCAG nodes in the bulk scan are AnimatedTabs measured mid-layout while the scan switches stories inside one page; the same stories pass with a fresh page load (
axe-story.mjs) and in the gate.Keyboard (
run-kb.mjs, embeds + Storybook) before → after:data-activearia-activedescendant, Home/End, Left/Right)<body>after Escapehref; Storybook story: ArrowDown focuses "Installation"Reduced motion (
run-motion.mjs, embeds withreducedMotion: reduce): embeds still moving 18 → 2, infinite animations 17 → 1 (the remaining infinite one is the Spinner, kept as essential feedback at a slower speed). AnimatedText no longer scrambles under reduced motion.API (additive only)
api.mjssnapshot ofdist/index.d.ts, current main (818a3994) vs this branch: 0 exports removed, 11 added, 0 members removed. 47 exports differ in type text: 43 add optional props or only change destructuring text; 4 (cn,GeoPosition,THEME_PRESETS,ChoroplethColorScale) differ only in generated symbol ids (run noise).moveRovingFocus,RovingFocusOptions,RovingOrientation,useReturnFocus,ReturnFocusHandlers,focusCalendarDay,CHECKLIST_PROGRESS_EVENT,parseChecklistStorageValue,MarqueeLabels,TickerTapeLabels,PaginationLabelspopoverLabel?: stringon Combobox, MultiSelect, DatePicker, DateRangePicker, TimePicker, ColorPickeraria-label?,aria-labelledby?,aria-describedby?,id?aria-label?,aria-labelledby?incrementLabel?,decrementLabel?pauseControl?: boolean,labels?: { pause?; play? }tabIndex?: numberregion?: stringmodal?: booleanlabels?: PaginationLabels; AIArtifactLabels:content?; ModelSelector:listLabel?; DataTableFilter:ariaLabel?; VideoEmbed:playLabel?useEscapeKeyoptions:capture?,stopPropagation?autoFocusdefaults totrue), ScrollArea, Slider (name props go to the thumb), WorkspaceSwitcher, mdxComponentsBehaviour changes (also in
CHANGELOG.mdandpackages/ui/CHANGELOG.md,[Unreleased])autoFocusdefaults totrue;autoFocus={false}restores vaul's default).modal(defaulttrue) maps to Radixmodal;modal={false}leaves the page behind it interactive. Shortcuts work only while focus is inside the dialog,""turns one off, and Enter no longer confirms from any focused element.pauseControl={false}opts out). Position: it overlays the top-right corner of the Marquee (right-1 top-1) and the right edge, vertically centred, of the TickerTape (right-2 top-1/2); it hides under reduced motion, when nothing moves. The Marquee root clips its content (overflow-hidden) as before.navand pages are single links styled as buttons,aria-current="page"on the current one.--vllnt-code-*CSS variables (OKLCH channels) instead of JS theme detection.aria-hiddendivider.data-motion="essential"spin slower; nothing outside the library is affected.Exceptions
None: no story disables an axe rule. (The gate supports reasoned exceptions; the list is empty.)
Gates
Run on this branch. UI code is final at
76db9f1f; the full axe CLI run is ona4323f28, after which only the changelogs, the registry NavigationMenu demo and the Comboboxaria-controlschanged (CI's Storybook Tests job re-runs the gate on HEAD).pnpm -F @vllnt/ui lint: pass (its only report is the local, uncommittedtest-runner-jest.config.jsthat points the runner at a local Chromium); changed files re-linted after the last commit: 0 problems;pnpm -F @vllnt/ui-registry lint: passpnpm -F @vllnt/ui exec tsc --noEmit --project tsconfig.build.json: passpnpm build: 6/6 taskspnpm test:once: ui-core, registry (99 tests) and native pass; ui: 1502 tests in 320 files pass (vitest run --maxWorkers=4). With full parallelism on a heavily loaded machine (load ~40), one timing-sensitive test timed out per run (MultiSelect first render at 5s, TutorialMDXfindByRoleat 1s); both pass alone and in the 4-worker runpnpm check:atomic,pnpm -F @vllnt/ui size(13 budgets ok),check:dist-imports: passpnpm -F @vllnt/ui-registry registry:build, thenregistry:integrity(with the new shim-import check): OK, 313 items; the registry build leaves no diff--maxWorkers=3 --testTimeout=60000, same timeout as CI)--diff origin/main --fail-on error): 0 errors (was 1: the Comboboxaria-controls)*rule; the code-token contrast test fails 12 cases with the previous colours). Not run red: the Pagination "label passed as undefined" test, and the Comboboxaria-controlsassertion (Radix already set it at runtime; that change is for static analysis)Lint config (
packages/ui/eslint.config.js): global rules unchanged from main;files:overrides only: drawer (no-autofocusignores the vaulautoFocusprop), the six scroll containers (no-noninteractive-tabindexallowslog/regionand<section>), tabs (tabpanel), canvas-view (the<section>may handle key events). No eslint-disable, ts-ignore orany.Remaining (not fixed here)
--update-snapshots.dc45faf9carries a copy of the previous commit's message by mistake; the PR is squash-merged.https://claude.ai/code/session_01TY3D3WivybufQN49bAnPjc