Skip to content
fayeuiStatic / 0.1.0
LLMs
System / Static edition

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.

PreferenceDefaultExplicit choice
Focus indicatorsAutomatic browser :focus-visible behaviorShow the same indicator on every focus, including pointer focus
Interaction hintsShow authored guidance on keyboard navigationShow it on every focus, including pointer focus
ContrastFollow system preferencesIncrease text and boundary contrast
MotionFollow system preferencesReduce 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

'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

'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

'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.

Live examples

Try the controls below, then read or copy the complete example to adapt it for your application.

Adaptive accessibility preferences

Accessibility

Focus guidance and floating hints

Use Tab to explore. Optional hints follow the focused control; labels and form help stay visible.

This name is visible only in this preview.

Preview volume

40
Preview rating

Focus: auto; hints: auto; contrast: system.

A local automatic scope

Read the complete example
'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>;
}