Skip to content
Open
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
14 changes: 10 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,13 @@ 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 `requiredText` is passed (e.g.
`requiredText="(required)"` or `requiredText="*"`), that text is appended
next to the label. No indicator is rendered by default. 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`.
8 changes: 8 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,14 @@ export interface LabelProps {
*/
optionalText?: string;

/**
* Text rendered next to the label when `required={true}`. No indicator is
* rendered if this is omitted — preserving apsara's existing behaviour of
* not surfacing a required marker by default. Pass any non-empty string
* (e.g. `"(required)"`, `"*"`) to opt in.
*/
requiredText?: string;

/** 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 forwards `aria-level` (1–6, defaults
to 3) to the underlying heading so the 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 set as `aria-level` on the underlying heading element.
* @default 3
*/
'aria-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
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"`. Pass
`aria-label="…"` to override with 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"
*/
'aria-label'?: string;
}
8 changes: 5 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,8 @@ 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"`.
- Supports `aria-label` and `aria-labelledby` for table identification.
8 changes: 7 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,13 @@ 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 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this as per design to put a focus-visible state here? If not let's take it up with the focus-visible milestone

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix Stylelint keyword-case violation on Line 56.

currentColor should be currentcolor to satisfy the configured value-keyword-case rule.

Proposed fix
-.action-btn:focus-visible {
-  outline: 2px solid currentColor;
+.action-btn:focus-visible {
+  outline: 2px solid currentcolor;
   outline-offset: 2px;
   border-radius: var(--rs-radius-1);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
outline: 2px solid currentColor;
.action-btn:focus-visible {
outline: 2px solid currentcolor;
outline-offset: 2px;
border-radius: var(--rs-radius-1);
}
🧰 Tools
🪛 Stylelint (17.11.0)

[error] 56-56: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/announcement-bar/announcement-bar.module.css` at
line 56, Change the CSS keyword value to lowercase to satisfy the stylelint
value-keyword-case rule: in announcement-bar.module.css replace the outline
property value "currentColor" with "currentcolor" (the rule applies where
outline: 2px solid currentColor; is defined) so the keyword is all lowercase and
the lint violation is resolved.

outline-offset: 2px;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use variable here

border-radius: var(--rs-radius-1);
}
Loading
Loading