directus on GitHub

directus/directus

Reviewed against Rams quality heuristics — accessibility, hierarchy, color, typography, spacing, components, motion, and UX.

30 files reviewed·May 13, 2026

Elevated

Accessibility needs attention.

6issues
Critical & Serious

Top fix

Add accessible names to all icon-only buttons across states.

See the fix

Verdict

Token discipline is real but accessibility is an afterthought — icon-only buttons, unlabeled controls, and sub-11px text appear across core chat UI components. The submit button's complete opacity to assistive tech is the single sharpest risk in an otherwise structurally tidy codebase.

Files Rams reviewed

app/src/ai/components/ai-context-card.vue

app/src/ai/components/ai-context-menu.vue

app/src/ai/components/ai-context-menu/context-menu-item.vue

app/src/ai/components/ai-conversation.vue

app/src/ai/components/ai-input-submit.vue

app/src/ai/components/ai-input.vue

app/src/ai/components/ai-magic-button.vue

app/src/ai/components/ai-message-list.vue

app/src/ai/components/ai-message.vue

app/src/ai/components/ai-model-selector.vue

app/src/ai/components/ai-pending-context.vue

app/src/ai/components/ai-prompt-variables-modal.vue

app/src/ai/components/ai-settings-menu.vue

app/src/ai/components/ai-sidebar-detail.vue

app/src/ai/components/ai-textarea.vue

app/src/ai/components/logos/claude.vue

app/src/ai/components/logos/openai.vue

app/src/ai/components/parts/ai-ask-user-summary.vue

app/src/ai/components/parts/ai-message-file-group.vue

app/src/ai/components/parts/ai-message-file.vue

app/src/ai/components/parts/ai-message-reasoning.vue

app/src/ai/components/parts/ai-message-text.vue

app/src/ai/components/parts/ai-message-tool.vue

app/src/ai/components/parts/ai-tool-call-card.vue

app/src/components/transition/bounce.vue

app/src/components/transition/dialog.vue

app/src/components/v-avatar.vue

app/src/components/v-badge.vue

app/src/components/v-banner.vue

app/src/components/v-breadcrumb.vue

87/100

Accessibility

1 critical5 serious
Accessibility·app/src/ai/components/ai-input-submit.vue:50Critical

Icon-only button has no accessible name for any state

The `<VButton>` renders with only a `<VIcon>` child and no `aria-label`. All three states — submit (`arrow_upward`), stop (`stop_circle`), and retry (`replay`) — are icon-only with no text alternative.

Why it matters

Screen readers announce the button as an unlabelled button or just 'button', making all three action states invisible to assistive tech. This fails WCAG 4.1.2.

Fix

Every icon-only button must have a dynamic aria-label that names the current action.

<VButton
	:disabled="isDisabled"
	class="submit-button"
	x-small
	:danger="props.status === 'error'"
	icon
	@click="handleClick"
>
	<VIcon :name="icon" />
</VButton>
<VButton
	:disabled="isDisabled"
	:aria-label="ariaLabel"
	class="submit-button"
	x-small
	:danger="props.status === 'error'"
	icon
	@click="handleClick"
>
	<VIcon :name="icon" />
</VButton>
Accessibility·app/src/ai/components/ai-context-menu.vue:184Serious

Back button in list header has no accessible name

The `VButton` with `icon` and `x-small` props in `.list-header` renders only `<VIcon name="arrow_back" small />` with no `aria-label` or tooltip: ``` <VButton x-small icon secondary @click="closeList"> <VIcon name="arrow_back" small /> </VButton> ``` Screen readers announce this as an unlabeled button.

Why it matters

Users on assistive tech get no indication this button navigates back — keyboard and screen-reader users lose orientation inside the menu.

Fix

Every icon-only button must have an aria-label that names its action.

<VButton x-small icon secondary @click="closeList">
					<VIcon name="arrow_back" small />
				</VButton>
<VButton x-small icon secondary :aria-label="t('back')" @click="closeList">
					<VIcon name="arrow_back" small />
				</VButton>
Accessibility·app/src/ai/components/ai-conversation.vue:113Serious

Scroll-to-bottom button has no accessible name for screen readers

The `scroll-to-bottom-btn` `VButton` uses `icon` and `rounded` props with only a `VIcon` child — no visible text, no `aria-label`. Screen readers will announce this as an unlabeled button.

Why it matters

Users navigating by keyboard or assistive tech hear 'button' with no context. They can't tell this control scrolls to the latest message, which disrupts navigation in a chat UI where following the conversation is the primary task.

Fix

Icon-only buttons must carry an aria-label that describes the action, not the icon.

<VButton icon rounded secondary x-small class="scroll-to-bottom-btn" @click="scrollToBottom('smooth')">
<VButton icon rounded secondary x-small class="scroll-to-bottom-btn" :aria-label="$t('ai.scroll_to_bottom')" @click="scrollToBottom('smooth')">
Accessibility·app/src/ai/components/ai-context-menu/context-menu-item.vue:55Serious

Badge text at 9px on a tinted background likely fails WCAG AA contrast

`.item-badge` renders uppercase text at 0.5625rem (9px) with `color: var(--theme--primary)` on `background-color: var(--theme--primary-background)`. At 9px, WCAG AA requires 4.5:1. Primary-on-primary-background pairs in most design systems sit around 3:1–3.5:1 — sufficient at normal size, insufficient below 12px.

Why it matters

The badge is the only differentiating signal for labeled items (e.g. a "New" or "Beta" indicator). If it fails contrast at its actual rendered size, it communicates nothing to low-vision users.

Fix

Raise the font size to at least 0.75rem or switch to a higher-contrast token pair (e.g. `var(--theme--primary)` on white/neutral background) so the contrast target is met at the rendered size.

padding: 0.125rem 0.3125rem;
font-size: 0.5625rem;
font-weight: 500;
text-transform: uppercase;
background-color: var(--theme--primary-background);
color: var(--theme--primary);
padding: 0.125rem 0.375rem;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
background-color: var(--theme--primary-background);
color: var(--theme--primary);
Accessibility·app/src/ai/components/ai-input.vue:55Serious

Click-to-focus div on input wrapper breaks keyboard and screen reader affordance

The `.input-wrapper` div carries `@click="textareaComponent?.focus()"` and `cursor: text`. A div with a click handler is not keyboard-reachable and is not announced as interactive by screen readers. Users navigating by keyboard or switch access can't activate this affordance.

Why it matters

Keyboard users lose the click-to-focus shortcut entirely. Screen readers announce nothing, so the wrapper is silently inert — onboarding friction compounds for assistive tech users.

Fix

Move the focus-trigger behavior to a visually transparent label element wrapping the content, or keep the div but add role='none' and move focus management to the AiTextarea's own label.

<div class="input-wrapper" @click="textareaComponent?.focus()">
<label class="input-wrapper" @click.self="textareaComponent?.focus()" aria-label="AI prompt input">
Accessibility·app/src/ai/components/ai-context-card.vue:79Serious

Removable card container has no focus style despite being interactive

The outer `.ai-context-card.removable` div handles `@mouseenter` and `@mouseleave` and carries `cursor: pointer` via the removable hover block — but there is no `&:focus-visible` rule on the card itself. The close button inside has correct focus treatment, but the card wrapper does not.

Why it matters

Keyboard users tabbing through a list of chips will land on the close button but have no visible indicator that the card itself is a navigable, interactive surface. This breaks the expected Tab → action flow.

Fix

Add a `&:focus-visible` rule to `.ai-context-card.removable` matching the same outline style used on `.close-button`.

&:hover {
	border-color: var(--theme--border-color-accent);
	background-color: var(--theme--background-normal);
}
&:hover {
	border-color: var(--theme--border-color-accent);
	background-color: var(--theme--background-normal);
}

&:focus-visible {
	outline: 2px solid var(--theme--primary);
	outline-offset: 1px;
}

Hierarchy

✓ No issues found

Typography

✓ No issues found

Color

✓ No issues found

Spacing

✓ No issues found

Components

✓ No issues found

Motion

✓ No issues found

UX

✓ No issues found

Working well

  • The `#scroll-anchor` + `overflow-anchor: auto` technique is the right way to handle auto-scroll in a chat — it lets the browser do the work natively without JavaScript scroll-pinning hacks that fight momentum scrolling and cause jank. Setting `overflow-anchor: none` on all siblings is also correct; it prevents any intermediate element from claiming the anchor position.
  • Passing `canSubmit`, `isProcessing`, and `canStop` as separate computed props to `AiInputSubmit` rather than passing the raw store is clean component design. The submit button's logic is fully encapsulated, which means state changes in the store don't leak visual responsibility into the parent — a pattern that scales well as submission states grow.
  • The `:focus-within` border treatment on `.input-wrapper` — switching to `var(--theme--primary)` — is the right call. It makes the entire composite input (textarea + controls) feel like a single focusable unit, which is the correct affordance for a compound input shell. Most teams either skip this or apply it only to the raw textarea.
  • The `emptyState` computed property consolidates three distinct states (no providers + admin, no providers + user, no messages) into a single object that the template reads once. This is clean composition — it avoids three separate `v-if` blocks in the template and keeps the visual logic in one reviewable place.

Score your own repo with Rams.

Free on public repos. Reviews run on every pull request and post inline fix suggestions.

Review my public repoFree