Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Renders a dismissible banner for announcements and alerts.

## Accessibility

- Uses `role="banner"` for proper landmark identification
- Supports keyboard dismissal and focus management
- Provides `aria-label` support for screen readers
- The action (when `actionLabel` or `actionIcon` is provided) is rendered
as a real `<button>`, so it's focusable, reachable by Tab, and activated
by Enter/Space — not as a `<Text>` with an `onClick` handler.
- The action button exposes a focus ring via `:focus-visible`.
- Decorative `leadingIcon` and `actionIcon` are marked `aria-hidden="true"`
so they don't add noise to screen readers.
13 changes: 8 additions & 5 deletions apps/www/src/content/docs/components/button/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ The button component accepts optional leading and/or trailing icons.

## Accessibility

- Uses native `button` element with proper ARIA attributes
- Supports keyboard activation with Enter and Space keys
- Disabled state is communicated via `aria-disabled` attribute
- Loading state prevents interaction and announces status to screen readers
- Respects motion preferences: button loader rotation is enabled only when `prefers-reduced-motion: no-preference`
- Uses the native `<button>` element so keyboard activation with Enter and
Space works out of the box.
- Disabled state is communicated via the native `disabled` attribute.
- Loading state sets `aria-busy="true"` on the button so screen readers
announce the busy state. The internal spinner is rendered with
`aria-hidden="true"` to avoid double-announcing.
- Respects motion preferences: button loader rotation is enabled only when
`prefers-reduced-motion: no-preference`.
8 changes: 5 additions & 3 deletions apps/www/src/content/docs/components/container/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Containers can be aligned to the left, center, or right of their parent element.

The Container component is designed with accessibility in mind:

- Uses `role="region"` to mark it as a landmark for screen readers
- Supports labeling via `aria-label` or `aria-labelledby`
- Helps organize content into meaningful sections
- Applies `role="region"` automatically when an `aria-label` or
`aria-labelledby` is provided, exposing the container as a landmark.
- Without a label, no role is applied — Container stays a generic `<div>`
to avoid emitting unlabeled regions that clutter assistive-tech output.
- Pass `role` explicitly to override (e.g. `role="main"`).
14 changes: 9 additions & 5 deletions apps/www/src/content/docs/components/drawer/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ The Drawer can slide in from different sides of the screen. Swipe-to-dismiss is

## Accessibility

- Uses `role="dialog"` with `aria-modal="true"`
- Focus is trapped within the drawer and restored on close
- Supports dismissal with Escape key and swipe gestures
- Title is announced via `aria-labelledby`
- Respects motion preferences: drawer slide motion is enabled only when `prefers-reduced-motion: no-preference`
- Uses `role="dialog"` with `aria-modal="true"`.
- Focus is trapped within the drawer and restored on close.
- Supports dismissal with Escape key and swipe gestures.
- Default `aria-label` is `"Drawer"`. Pass `aria-label` or `aria-labelledby`
on `Drawer.Content` to give the dialog a meaningful name.
- Close button label defaults to `"Close"`. Override with the `closeLabel`
prop for localisation or context-specific copy (e.g. `"Close settings"`).
- Respects motion preferences: drawer slide motion is enabled only when
`prefers-reduced-motion: no-preference`.

6 changes: 6 additions & 0 deletions apps/www/src/content/docs/components/drawer/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export interface DrawerContentProps {
/** Whether to show the close button. */
showCloseButton?: boolean;

/**
* Accessible label for the close button. Override for localisation.
* @default "Close"
*/
closeLabel?: string;

/** Props to pass to the backdrop/overlay component. */
overlayProps?: {
className?: string;
Expand Down
7 changes: 4 additions & 3 deletions apps/www/src/content/docs/components/image/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ The Image component follows accessibility best practices:

### ARIA Attributes

- `role="img"`: Explicitly defines the image role for assistive technologies
- `aria-label`: Provides description matching alt text for screen readers
- `alt`: Required alternative text description for screen readers
- `alt` is the accessible name. Set it to a meaningful description, or `""`
for decorative images (which marks the element as presentational).
- No redundant `role="img"` or duplicated `aria-label` is emitted — the
native `<img>` element already provides those semantics.

### Performance

Expand Down
13 changes: 9 additions & 4 deletions apps/www/src/content/docs/components/label/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ Override the text via `optionalText`.

## Accessibility

- Renders a semantic HTML `<label>` element by default
- Supports programmatic association with form controls via `htmlFor`
- Maintains WCAG compliant color contrast ratios
- Shows a pointer cursor when associated with a control via `htmlFor`
- Renders a semantic HTML `<label>` element by default.
- Supports programmatic association with form controls via `htmlFor`.
- When `required={false}`, an `(optional)` indicator is appended
(customise the text via `optionalText`).
- When `required={true}` and `showRequiredIndicator` is set, a
`(required)` indicator is appended (customise via `requiredText`). The
indicator is real text, so screen readers announce it.
- Maintains WCAG compliant color contrast ratios.
- Shows a pointer cursor when associated with a control via `htmlFor`.
14 changes: 14 additions & 0 deletions apps/www/src/content/docs/components/label/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ export interface LabelProps {
*/
optionalText?: string;

/**
* Text rendered next to the label when `required={true}` and
* `showRequiredIndicator` is enabled.
* @defaultValue "(required)"
*/
requiredText?: string;

/**
* When `true` and `required={true}`, renders a `(required)` indicator next
* to the label. Disabled by default to preserve existing visual layouts.
* @defaultValue false
*/
showRequiredIndicator?: boolean;

/** Associates the label with a form control using its ID. */
htmlFor?: string;

Expand Down
16 changes: 10 additions & 6 deletions apps/www/src/content/docs/components/link/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,13 @@ import { Link as RouterLink } from "react-router-dom";

The Link component follows accessibility best practices:

- Uses native `<a>` elements with proper `role="link"`
- External links include `target="_blank"` and `rel="noopener noreferrer"`
- External links have aria-labels indicating they open in new tabs
- Download links include appropriate aria-labels
- Maintains color contrast ratios for all variants
- Hover feedback uses an opacity transition
- Renders a native `<a>` by default — no explicit `role="link"` is set
because the native element already provides it. If you swap the element
via `render` to something non-anchor (e.g. `<button>`), it will be
announced according to that element's own semantics.
- External links include `target="_blank"` and `rel="noopener noreferrer"`.
- External and download links auto-generate an `aria-label` when `children`
is a string (e.g. `"Docs (opens in new tab)"`). For non-string children,
pass `aria-label` yourself so screen readers get a clean label.
- Maintains color contrast ratios for all variants.
- Hover feedback uses an opacity transition.
12 changes: 9 additions & 3 deletions apps/www/src/content/docs/components/list/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ Renders the value text within a list item.

The List component implements proper ARIA attributes for accessibility:

- List has `role="list"` and `aria-label`
- List.Item has `role="listitem"`
- List.Header has `role="heading"` and `aria-level={3}`
- List renders a `<ul>` with explicit `role="list"`. The role is kept
even though the element is native, because Safari/VoiceOver drop the
implicit list role when `list-style: none` is applied.
- List relies on the native `<li>` semantics for items — no redundant
`role="listitem"` is set.
- No generic default `aria-label` is added. Pass `aria-label` when the
list needs an accessible name distinct from surrounding context.
- List.Header has `role="heading"` and exposes a configurable `level` prop
(1–6, defaults to 3) so the heading rank matches the document outline.
6 changes: 6 additions & 0 deletions apps/www/src/content/docs/components/list/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export interface ListHeaderProps {
/** Content to be displayed in the header. */
children?: React.ReactNode;

/**
* Heading level (`aria-level`) for the header.
* @default 3
*/
level?: 1 | 2 | 3 | 4 | 5 | 6;

/** Additional CSS class names. */
className?: string;
}
Expand Down
9 changes: 6 additions & 3 deletions apps/www/src/content/docs/components/scroll-area/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Control when the scrollbar appears using the `type` prop.

## Accessibility

- Scrollable region is keyboard accessible
- Scrollbar elements are hidden from screen readers with `aria-hidden`
- Supports standard scrolling keyboard shortcuts
- Scrollable region is keyboard accessible.
- Scrollbar elements are hidden from screen readers with `aria-hidden`.
- Supports standard scrolling keyboard shortcuts.
- Pass `aria-label` or `aria-labelledby` to label the scrollable content.
When provided, the viewport is exposed as a labelled `region` landmark
so screen-reader users can navigate to it.
8 changes: 5 additions & 3 deletions apps/www/src/content/docs/components/separator/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Separator can be rendered in both horizontal and vertical orientations.

## Accessibility

- Uses `role="separator"` for proper semantic meaning
- Supports `aria-orientation` for horizontal and vertical variants
- Decorative separators use `role="none"` to hide from screen readers
- Uses `role="separator"` and `aria-orientation` by default so the divider
is announced and oriented correctly.
- Pass `decorative` when the separator is purely visual (for example
between icons in a toolbar). It then renders with `role="presentation"`
and `aria-hidden="true"` so assistive tech skips it.
8 changes: 8 additions & 0 deletions apps/www/src/content/docs/components/separator/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export interface SeparatorProps {
*/
orientation?: 'horizontal' | 'vertical';

/**
* When `true`, the separator is treated as purely decorative
* (`role="presentation"`, `aria-hidden`). Use for visual dividers that
* don't convey structure.
* @default false
*/
decorative?: boolean;

/** Additional CSS class names. */
className?: string;

Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/sidebar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ The Sidebar implements the following accessibility features:

- `role="navigation"` for the main sidebar
- `role="banner"` for the header
- `role="list"` for item containers (Main groups, Footer) and `role="listitem"` for navigation items
- `role="list"` for item containers (Main groups, Footer); navigation items rely on the native semantics of their underlying element (`<a>` by default) instead of an explicit `role="listitem"`
- `aria-expanded` to indicate sidebar state
- `aria-current="page"` for active items
- `aria-disabled="true"` for disabled items
Expand Down
9 changes: 6 additions & 3 deletions apps/www/src/content/docs/components/sidepanel/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ The SidePanel can be positioned on either the left or right side of the screen.

## Accessibility

- Uses `role="complementary"` for proper landmark identification
- Supports keyboard navigation and focus management
- Panel state is communicated to screen readers
- Renders as a native `<aside>` — assistive tech announces it as the
`complementary` landmark automatically.
- `SidePanel.Header` renders the `title` as a semantic `<h2>` and assigns
it an auto-generated id (override via `titleId`). Pair it with
`aria-labelledby` on the `<aside>` to label the landmark.
- Supports keyboard navigation and focus management of nested controls.
7 changes: 7 additions & 0 deletions apps/www/src/content/docs/components/sidepanel/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ export interface SidePanelHeaderProps {
* Array of action elements to display in the header.
*/
actions?: React.ReactNode[];

/**
* Optional id assigned to the title heading. Useful when wiring up
* `aria-labelledby` on the parent `<aside>` (or any other landmark).
* A unique id is generated automatically when omitted.
*/
titleId?: string;
}
12 changes: 7 additions & 5 deletions apps/www/src/content/docs/components/skeleton/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ All props available on the Skeleton component can be passed to the Provider and

The Skeleton component follows accessibility best practices:

- Uses semantic HTML elements
- Provides appropriate ARIA attributes
- Maintains sufficient color contrast
- Respects motion preferences: skeleton shimmer is enabled only when `prefers-reduced-motion: no-preference`
- Supports both block and inline layouts
- The placeholder container is `aria-hidden="true"` so screen readers
skip the decorative shapes. Announce the loading state on a real status
region (e.g. a `<Spinner>` or `aria-live` text) elsewhere in the UI.
- Maintains sufficient color contrast.
- Respects motion preferences: skeleton shimmer is enabled only when
`prefers-reduced-motion: no-preference`.
- Supports both block and inline layouts.
13 changes: 10 additions & 3 deletions apps/www/src/content/docs/components/spinner/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ The Spinner component offers 6 color values. `default` prop sets the color to `c

The Spinner component includes appropriate ARIA attributes for accessibility:

- `role="status"`: Indicates that the element is a status indicator.
- `aria-hidden="true"`: Hides the spinner from screen readers, as it's a visual indicator only.
- Respects motion preferences: spinner rotation is enabled only when `prefers-reduced-motion: no-preference`
- `role="status"` and `aria-live="polite"`: assistive tech announces the
spinner as a polite live region when it appears.
- `aria-label`: announced label, defaults to `"Loading"`. Override with the
`ariaLabel` prop (or the standard `aria-label` attribute) for
context-specific text like `"Saving changes"`.
- Pass `aria-hidden="true"` for purely decorative spinners (e.g. inside a
button that already has `aria-busy`). The status role and label are
removed automatically so the two attributes don't conflict.
- Respects motion preferences: spinner rotation is enabled only when
`prefers-reduced-motion: no-preference`.
7 changes: 7 additions & 0 deletions apps/www/src/content/docs/components/spinner/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ export interface SpinnerProps {

/** Additional CSS class names. */
className?: string;

/**
* Accessible label announced to screen readers. Ignored when
* `aria-hidden="true"` is passed.
* @default "Loading"
*/
ariaLabel?: string;
}
12 changes: 9 additions & 3 deletions apps/www/src/content/docs/components/table/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ Represents a section heading, grouping different rows of the table.

## Accessibility

- Uses semantic `table`, `thead`, `tbody`, `tr`, `th`, and `td` elements
- Supports `aria-label` for table identification
- Header cells use `scope` attribute for row/column association
- Uses semantic `table`, `thead`, `tbody`, `tr`, `th`, and `td` elements.
- `Table.Head` applies `scope="col"` by default — override to `"row"`,
`"colgroup"`, or `"rowgroup"` for non-standard layouts. `SectionHeader`
uses `scope="colgroup"`.
- Use `Table.Caption` as the first child of `Table` to give the table an
accessible name visible to all users (recommended over `aria-label`
when the description is short and benefits everyone).
- Supports `aria-label` and `aria-labelledby` for table identification
when a visible caption isn't appropriate.
16 changes: 15 additions & 1 deletion apps/www/src/content/docs/components/table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,21 @@ export interface TableRowProps {
className?: string;
}

export interface TableHeadProps {}
export interface TableHeadProps {
/**
* Associates the header cell with rows or columns it labels.
* @default "col"
*/
scope?: 'col' | 'row' | 'colgroup' | 'rowgroup';
}

export interface TableCaptionProps {
/** Caption content describing the purpose of the table. */
children?: React.ReactNode;

/** Additional CSS class names. */
className?: string;
}

export interface TableCellProps {
/** Additional CSS class names. */
Expand Down
8 changes: 7 additions & 1 deletion apps/www/src/content/docs/components/tooltip/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,10 @@ Show the arrow by setting `showArrow={true}` on the Content component:

## Accessibility

- Respects motion preferences: tooltip entry motion is enabled only when `prefers-reduced-motion: no-preference`
- The trigger is wired to the popup via `aria-describedby` automatically.
- When `Tooltip.Content` receives a string `children`, that string is the
accessible description. For ReactNode content (icons, mixed markup),
pass `aria-label` (or `aria-labelledby`) on `Tooltip.Content` so screen
readers still announce a meaningful name.
- Respects motion preferences: tooltip entry motion is enabled only when
`prefers-reduced-motion: no-preference`.
13 changes: 13 additions & 0 deletions apps/www/src/content/docs/components/tooltip/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ export interface TooltipContentProps {
* Additional CSS class names
*/
className?: string;

/**
* Accessible label for the tooltip popup. Use when `children` is a
* ReactNode (icons, custom markup) that wouldn't expose a readable name
* on its own.
*/
'aria-label'?: string;

/**
* ID of an element that labels the tooltip popup. Alternative to
* `aria-label` when the labelling text lives elsewhere in the DOM.
*/
'aria-labelledby'?: string;
}

export interface TooltipProviderProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('AnnouncementBar', () => {

it('applies correct action button styles', () => {
render(<AnnouncementBar text='Test' actionLabel='Action' />);
const actionBtn = screen.getByText('Action');
const actionBtn = screen.getByRole('button', { name: 'Action' });
expect(actionBtn).toHaveClass(styles['action-btn']);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,19 @@
}

.action-btn {
display: flex;
display: inline-flex;
align-items: center;
gap: var(--rs-space-1);
cursor: pointer;
background: transparent;
border: 0;
padding: 0;
font: inherit;
color: inherit;
}

.action-btn:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
border-radius: var(--rs-radius-1);
}
Loading
Loading