Accessible Names
Interactive controls need an accessible name. Text children usually provide one automatically.
import { Button } from "@boreal-ui/core";
export function SubmitAction() {
return <Button type="submit">Submit request</Button>;
}Boreal UI components are designed to provide accessible defaults, but the consuming app still controls the final experience. Use this guide when composing Boreal components into forms, pages, navigation, overlays, dashboards, and application workflows.
These defaults work best when consumers provide meaningful labels, descriptions, and state values.
The application remains responsible for the complete flow and final content quality.
Interactive controls need an accessible name. Text children usually provide one automatically.
import { Button } from "@boreal-ui/core";
export function SubmitAction() {
return <Button type="submit">Submit request</Button>;
}Icon-only controls need an explicit label.
import { IconButton } from "@boreal-ui/core";
function CloseIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M6.2 4.8 12 10.6l5.8-5.8 1.4 1.4L13.4 12l5.8 5.8-1.4 1.4L12 13.4l-5.8 5.8-1.4-1.4 5.8-5.8-5.8-5.8 1.4-1.4Z"
fill="currentColor"
/>
</svg>
);
}
export function CloseAction() {
return <IconButton icon={CloseIcon} aria-label="Close dialog" />;
}Use aria-labelledby when a visible element outside the control should provide the name.
<h2 id="billing-actions-title">Billing actions</h2>
<Button aria-labelledby="billing-actions-title">Open</Button>Prefer visible labels. Do not rely on placeholder text as the only label.
import { TextInput } from "@boreal-ui/core";
export function EmailField() {
return (
<>
<TextInput
id="email"
name="email"
type="email"
label="Email"
aria-describedby="email-help"
autoComplete="email"
required
/>
<p id="email-help">Use the address attached to your account.</p>
</>
);
}Pass the component's error or invalid state props where available so assistive technology receives the same state as sighted users.
<>
<TextInput
id="project-name"
name="projectName"
label="Project name"
state="error"
aria-invalid
aria-describedby="project-name-error"
/>
<p id="project-name-error">Project name is required.</p>
</>Use Button for actions and href for navigation.
<Button type="button" onClick={saveDraft}>
Save draft
</Button>
<Button href="/settings">Open settings</Button>When a button opens or controls another element, expose that relationship.
<Button
aria-controls="project-menu"
aria-expanded={menuOpen}
aria-haspopup="menu"
onClick={() => setMenuOpen((open) => !open)}
>
Project actions
</Button>Give DataTable a caption, aria-label, or aria-labelledby.
import { DataTable } from "@boreal-ui/core";
<DataTable
caption="Invoices"
columns={columns}
data={rows}
rowKey={(row) => row.id}
/>;Use hideCaption when the table needs a semantic caption but the page already has a visible heading.
<h2 id="invoice-table-heading">Invoices</h2>
<DataTable
aria-labelledby="invoice-table-heading"
caption="Invoices"
hideCaption
columns={columns}
data={rows}
/>;For interactive rows, provide labels that explain the action.
<DataTable
columns={columns}
data={rows}
onRowClick={(row) => openInvoice(row.id)}
getRowAriaLabel={(row) => `Open invoice ${row.id}`}
/>;Modal should have a visible title, aria-label, or aria-labelledby. Add aria-describedby when supporting text explains the decision.
import { Button, Modal } from "@boreal-ui/core";
export function DeleteProjectDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
return (
<Modal
open={open}
onClose={onClose}
title="Delete project"
aria-describedby="delete-project-description"
closeButtonAriaLabel="Close delete project dialog"
>
<div>
<p id="delete-project-description">
This removes the project and its saved dashboard settings.
</p>
<Button state="warning">Delete project</Button>
</div>
</Modal>
);
}Use loading props and busy/live-region props where available.
<Button loading loadingLabel="Saving changes" aria-live="polite">
Save changes
</Button>Pair visual loading states with text that explains the state.
<DataTable
aria-label="Search results"
columns={columns}
data={rows}
loading={isLoading}
loadingMessage="Loading search results"
emptyMessage="No matching results"
/>;Verify text, borders, focus indicators, success/error/warning states, outline variants, and glass surfaces against their backgrounds.
import { ThemeProvider } from "@boreal-ui/core";
import type { ColorScheme } from "@boreal-ui/types";
const schemes: ColorScheme[] = [
{
name: "Brand Night",
primaryColor: "#4f46e5",
secondaryColor: "#06b6d4",
tertiaryColor: "#a855f7",
quaternaryColor: "#22c55e",
backgroundColor: "#0f172a",
forceTextColor: "#ffffff",
},
];
<ThemeProvider customSchemes={schemes} initialSchemeName="Brand Night">
<App />
</ThemeProvider>;If you override --focus-outline-color, make sure it is visible on light, dark, glass, outlined, and themed surfaces.
.app-action:focus-visible {
outline: 2px solid var(--focus-outline-color);
outline-offset: 2px;
}Use Testing Library role and name queries first.
import { render, screen } from "@testing-library/react";
import { Button } from "@boreal-ui/core";
it("renders an accessible submit button", () => {
render(<Button type="submit">Submit request</Button>);
expect(
screen.getByRole("button", { name: /submit request/i }),
).toBeInTheDocument();
});Use jest-axe for automated accessibility checks.
import { render } from "@testing-library/react";
import { axe } from "jest-axe";
import { TextInput } from "@boreal-ui/core";
it("has no accessibility violations", async () => {
const { container } = render(<TextInput label="Email" name="email" />);
expect(await axe(container)).toHaveNoViolations();
});For grouped fields, use FormGroup, fieldsets, headings, or helper text so related controls have context.
Avoid using disabled controls as the only way to explain what happened. Pair disabled states with visible helper text when the reason is not obvious.
For Dropdown, PopOver, Tooltip, Tabs, Accordion, and CommandPalette, prefer the component's public props for labels and state. Avoid adding custom roles to wrapper elements unless the component API asks for them.
Also test these complete states and workflows before shipping.
Before shipping a page that uses Boreal UI: