# Accessibility and input behavior

Automatic focus guidance, optional keyboard hints, system preferences and explicit user controls.

## Automatic presentation, continuous accessibility

Faye UI keeps accessible names, labels, errors, screen-reader semantics and keyboard operation available continuously. The presentation preferences below control focus indicators, optional interaction hints, contrast and motion.

| Preference | Default | Explicit choice |
|---|---|---|
| Focus indicators | Automatic browser `:focus-visible` behavior | Show the same indicator on every focus, including pointer focus |
| Interaction hints | Show authored guidance on keyboard navigation | Show it on every focus, including pointer focus |
| Contrast | Follow system preferences | Increase text and boundary contrast |
| Motion | Follow system preferences | Reduce nonessential movement |

Text fields can receive a visible focus indicator after a pointer click: browsers treat text entry as keyboard interaction. Automatic hints specifically follow navigation keys such as Tab and the arrow keys. Ordinary typing or pointer hover does not enable the new hints. System contrast and motion preferences act independently; they do not turn on every other preference.

## Provider and scoped preferences

```tsx
'use client';
import { FayeUIProvider, ThemeScope } from '@fayegram-ai/faye-ui/provider/provider';
import { Button } from '@fayegram-ai/faye-ui/controls/button';

export function AccessibleApplication() {
  return <FayeUIProvider
    accessibility={{ focus: 'always', hints: 'auto', contrast: 'system' }}
    motion="system"
  >
    <Button>Continue</Button>
    <ThemeScope accessibility={{ focus: 'auto' }}>
      <Button>Use automatic focus here</Button>
    </ThemeScope>
  </FayeUIProvider>;
}
```

Omitted properties inherit. Explicit `auto` or `system` values restore automatic behavior inside a scope. Accessibility preferences remain inherited through isolated visual themes and are carried into dialogs, menus and other portals. The read-only `useAccessibilityPreferences()` hook returns the effective scoped configuration. Applications own updates and persistence. `useInputModality()` reports document-local navigation modality (`unknown`, `keyboard` or `pointer`); it does not detect a screen reader.

Use `ThemeScope` to override one control or a region. With `applyToDocument={false}`, use a `ThemeScope` around the region to supply its DOM presentation, just as for scoped theme colors. The existing `motion="system"` and `motion="reduced"` properties remain supported. Forced system colors retain priority, and a system request to reduce motion is respected.

## Optional floating interaction hints

```tsx
'use client';
import { InteractionHint } from '@fayegram-ai/faye-ui/overlays/interaction-hint';
import { Button } from '@fayegram-ai/faye-ui/controls/button';

export function SaveAction() {
  return <InteractionHint content="Press Enter or Space to save this collection.">
    <Button>Save collection</Button>
  </InteractionHint>;
}
```

Supply one control that forwards its ref and event/ARIA props. The hint adds no focus stop or wrapper to the layout. Use brief, noninteractive content. It opens immediately for eligible focus, stays within the viewport, and closes on blur, Escape, disabling or unmounting. Escape keeps it dismissed until focus leaves and returns. Reduced-motion preferences remove its fade.

`hints="always"` means every focused control with an authored hint, not every hint at once. Existing tooltips keep their hover and focus behavior. Essential field descriptions, validation messages and accessible labels belong on their controls and must remain available independently of an optional hint.

## Optional preference controls

```tsx
'use client';
import { useState } from 'react';
import { FayeUIProvider } from '@fayegram-ai/faye-ui/provider/provider';
import { AccessibilityPreferences, type AccessibilityPreferencesValue } from '@fayegram-ai/faye-ui/patterns/accessibility-preferences';

export function PreferenceExample() {
  const [value, setValue] = useState<AccessibilityPreferencesValue>({
    focus: 'auto', hints: 'auto', contrast: 'system', motion: 'system'
  });
  const { motion, ...accessibility } = value;
  return <FayeUIProvider accessibility={accessibility} motion={motion}>
    <AccessibilityPreferences value={value} onValueChange={setValue}/>
  </FayeUIProvider>;
}
```

The controls are optional and remain inside the single Faye UI package. They use the provider's message catalogs, support label overrides, and leave persistence to the application. The documentation's header **Theme → Accessibility** section saves explicit choices locally. Its reset action resets these four preferences while preserving palette, density and direction.

## Application responsibilities and verification

Keep labels visible, provide meaningful task structure, and distinguish disabled and read-only states. Use the existing dialog and menu components for their keyboard and focus behavior. Drag or swipe interactions need appropriate alternatives. No browser signal reliably establishes whether a screen reader is active; Faye UI does not attempt that detection.

WCAG 2.2 AA remains the target, not a completed certification. Automated checks and direct visual/interaction inspection cover different facts. Manual screen-reader, physical-device and linguistic reviews retain their recorded limitations. See [verification](/docs/verification/).

## Executable example source

```tsx
'use client';
import { useState } from 'react';
import { ThemeScope, useAccessibilityPreferences, useVisualScope } from '@fayegram-ai/faye-ui/provider/provider';
import { AccessibilityPreferences, type AccessibilityPreferencesValue } from '@fayegram-ai/faye-ui/patterns/accessibility-preferences';
import { InteractionHint } from '@fayegram-ai/faye-ui/overlays/interaction-hint';
import { Dialog } from '@fayegram-ai/faye-ui/overlays/dialog';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
import { Input } from '@fayegram-ai/faye-ui/controls/input';
import { ControlLabel } from '@fayegram-ai/faye-ui/controls/field-layout';
import { Field } from '@fayegram-ai/faye-ui/controls/field';
import { Radio } from '@fayegram-ai/faye-ui/controls/radio';
import { RadioGroup } from '@fayegram-ai/faye-ui/controls/radio-group';
import { Slider } from '@fayegram-ai/faye-ui/controls/slider';
import { Rating } from '@fayegram-ai/faye-ui/controls/rating';
import { Text } from '@fayegram-ai/faye-ui/core/text';
import { Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { Section } from '../example';

const responsiveAction = { maxInlineSize: '100%', whiteSpace: 'normal' } as const;

function Controls() {
    const preferences = useAccessibilityPreferences();
    return <Stack gap="4">
        <Text as="p" size="sm" tone="muted">Use Tab to explore. Optional hints follow the focused control; labels and form help stay visible.</Text>
        <Inline wrap><InteractionHint content="Press Enter or Space to activate this action."><Button style={responsiveAction} data-testid="assistance-action">Save a place</Button></InteractionHint><InteractionHint content="This action cannot currently be used."><Button style={responsiveAction} disabled>Unavailable action</Button></InteractionHint><Button style={responsiveAction}>Next action</Button></Inline>
        <Field.Root><Field.Label>Collection name</Field.Label><InteractionHint content="Type a name, then press Tab to continue."><Input defaultValue="Weekend ideas" data-testid="assistance-input"/></InteractionHint><Field.Description>This name is visible only in this preview.</Field.Description></Field.Root>
        <RadioGroup defaultValue="weekly" aria-label="Update frequency"><ControlLabel label="Weekly updates">{attrs => <Radio.Root {...attrs} value="weekly"><Radio.Indicator/></Radio.Root>}</ControlLabel><ControlLabel label="Monthly updates">{attrs => <Radio.Root {...attrs} value="monthly"><Radio.Indicator/></Radio.Root>}</ControlLabel></RadioGroup>
        <InteractionHint content="Use arrow keys to adjust; Home and End jump to either limit."><Slider.Root defaultValue={40} aria-label="Preview volume"><Text as="p" size="sm">Preview volume</Text><Slider.Control><Slider.Track><Slider.Indicator/><Slider.Thumb aria-label="Preview volume"/></Slider.Track></Slider.Control><Slider.Value/></Slider.Root></InteractionHint>
        <Rating label="Preview rating" defaultValue={3} clearable/>
        <Dialog.Root><Dialog.Trigger render={<Button style={responsiveAction}/>}>Open assistance dialog</Dialog.Trigger><Dialog.Portal><Dialog.Backdrop/><Dialog.Popup><Dialog.Header><Dialog.Title>Keep your collection</Dialog.Title><Dialog.Close aria-label="Close assistance dialog"/></Dialog.Header><Dialog.Body><Text as="p">The dialog uses the same accessibility preferences.</Text><InteractionHint content="Press Enter to keep this collection."><Button style={responsiveAction} data-testid="assistance-dialog-action">Keep collection</Button></InteractionHint></Dialog.Body></Dialog.Popup></Dialog.Portal></Dialog.Root>
        <Text as="p" size="sm" data-testid="assistance-status">Focus: {preferences.focus}; hints: {preferences.hints}; contrast: {preferences.contrast}.</Text>
    </Stack>;
}
export default function AccessibilityExample() {
    const inherited = useAccessibilityPreferences(), scope = useVisualScope();
    const [override, setValue] = useState<AccessibilityPreferencesValue>();
    const value = override ?? { ...inherited, motion: scope.motion };
    const { motion, ...accessibility } = value;
    return <ThemeScope accessibility={accessibility} motion={motion} data-testid="assistance-preview"><Section title="Adaptive accessibility preferences"><AccessibilityPreferences value={value} onValueChange={setValue}/></Section><Section title="Focus guidance and floating hints"><Controls/></Section><Section title="A local automatic scope"><ThemeScope accessibility={{ focus: 'auto', hints: 'auto', contrast: 'system' }} data-testid="assistance-automatic"><InteractionHint content="This hint follows keyboard navigation."><Button style={responsiveAction} data-testid="assistance-automatic-action">Automatic guidance</Button></InteractionHint></ThemeScope></Section></ThemeScope>;
}
```
