Source targets WCAG 2.1 AA. This page documents the approach, color policy, keyboard support, and per-component ARIA implementation.
Target
WCAG 2.1 AA for all interactive components. This covers contrast ratios, keyboard operability, focus management, and programmatic name. Components that only render content (Badge, Tag without href) have no interactivity requirements, though color usage still follows the non-reliance rule.
Principles
Semantic HTML firstComponents use native elements wherever possible. Tabs use button elements with role="tab", links use a, interactive chips use button. This gives correct baseline behavior without extra ARIA.
Color is not the only signalNo component relies on color alone to convey state or meaning. Badge palette changes are always paired with a text label. Selected state in Chip is communicated via aria-pressed in addition to visual change.
Visible focusEvery interactive element shows a focus ring when navigated by keyboard. Focus rings use :focus-visible so they do not appear on mouse click. Focus is never suppressed.
Accessible by defaultComponents do not require extra props to be accessible. aria-pressed, aria-selected, and role attributes are applied internally based on component state.
Known considerations
These are areas where the component provides the structure but the consuming app carries responsibility.
External linksLink and FeatureCard with external open a new tab. The visual arrow icon indicates this, but screen readers do not automatically announce the behavior. Add visible "(opens in new tab)" text or an aria-label on critical links.
Badge semanticsBadge renders a plain span. If the badge conveys live status (e.g. an unread count that updates), the consuming app should add aria-live="polite" on a wrapping element.
FeatureCard link textWhen FeatureCard renders as a link, the accessible name comes from the card's text content. The title should be descriptive enough to make sense out of context. Avoid generic titles like "Learn more".
Disabled stateDisabled Chips are not keyboard-reachable. WCAG 2.1 does not require disabled components to meet contrast ratios (exception in 1.4.3). Disabled FeatureCard renders a non-interactive div.
Contrast policy
Text token contrast targets against their expected backgrounds:
TokenTargetNotes
--source-color-textAA+Primary text. Meets AA for normal and large text against all surface tokens.
--source-color-text-secondaryAASecondary text. Meets AA for normal text against background and surface tokens.
--source-color-text-labelLarge onlyFor supplementary labels at larger sizes. Use only at 18px+ or 14px+ bold.
--source-color-text-disabledExemptIntentionally low-contrast. WCAG 1.4.3 exempts disabled states from contrast requirements.
Dark mode
Semantic tokens are individually tuned for dark mode. Each token maps to a different primitive stop in dark mode, chosen to maintain the same contrast intent. Primitives that were dark in light mode become light in dark mode, and vice versa. The palette page shows both light and dark resolved values for all semantic tokens.
Accent themes
Accent tokens use different primitive stops depending on the theme hue. Orange and yellow themes use deeper stops in light mode (-700/-800 instead of -600) because those hues reach AA contrast at higher lightness values. The system compensates automatically when you switch theme — you do not need to adjust tokens manually.
Non-reliance on color
Every component that uses palette color also communicates state through text, shape, or ARIA. Chip selection uses aria-pressed. Tab selection uses aria-selected. Badge palette is always paired with a visible text label. No component uses color as the only indicator of state.
Focus management
All interactive components follow these focus rules:
VisibilityFocus rings appear on keyboard navigation. CSS :focus-visible suppresses them on mouse click. A 2px inset outline in the primary text color is the default style across all components.
SuppressionFocus is never removed programmatically. Components do not call .blur() or outline: none unconditionally.
Tab orderComponents follow DOM order. No tabindex manipulation except in the Tabs component, where tab buttons receive focus management as defined by the ARIA Authoring Practices.
Disabled elementsDisabled Chips and disabled FeatureCards are removed from the tab order. They are not reachable by keyboard.
Keyboard support by component
ComponentKeysBehavior
LinkTab, EnterStandard anchor behavior. Tab to focus, Enter to follow.
Tag (link)Tab, EnterSame as Link when href is set. No keyboard interaction when static.
ChipTab, Space, EnterTab to focus, Space or Enter to toggle. Disabled chips are not reachable.
FeatureCard (link)Tab, EnterStandard anchor behavior when href is set. Not focusable when disabled.
TabsTabMoves focus into the tab bar, then to the active panel.
TabsArrow Left / RightMoves focus between tab buttons and activates the focused tab. Wraps at both ends.
TabsHome / EndMoves focus and activates the first or last tab.
TabsSpace / EnterActivates the focused tab.
ARIA by component
Each row shows what the component renders, what ARIA attributes it applies, and what the consuming app must provide.
Badge
Elementspan
RoleNone. Inherits from context.
ARIA (built-in)None.
App responsibilityAlways pair palette with a text label. If the badge content changes dynamically (e.g. unread count), wrap it in an element with aria-live="polite".
Tag
Elementspan (static) / a (with href)
RoleLink when href is set (implicit from a element).
ARIA (built-in)None additional.
App responsibilityLink text should describe the destination clearly.
Chip
Elementbutton
RoleButton (implicit from button element).
ARIA (built-in)aria-pressed="true|false"
App responsibilityChip label should describe what it toggles or filters. When used as a filter group, consider wrapping in a fieldset with legend to provide group context.
App responsibilityLink text should make sense out of context. For external links that open a new tab, add visible "(opens in new tab)" when the destination is critical or when the surrounding context does not make this clear.
FeatureCard
Elementdiv (static) / a (with href)
RoleLink when href is set (implicit from a element).
ARIA (built-in)None additional.
App responsibilityTitle should be descriptive. The entire card is the link, so "Click here" or "View" as a title makes the accessible name meaningless.
Tabs
Elementsdiv (tablist), button (tab), div (tabpanel)
Rolesrole="tablist", role="tab"
ARIA (built-in)aria-selected="true|false" on each tab. hidden attribute on inactive panels.
PatternFollows the WAI-ARIA 1.2 Tabs pattern. Inactive panels use the hidden attribute, removing them from the accessibility tree entirely rather than just hiding them visually.
App responsibilityTab labels should describe the content of the panel. If the tab panel contains dynamic content, the panel element can receive aria-live as needed.
Button
Elementbutton (default) / a (with href)
ARIA (built-in)aria-busy when loading. type="button" by default.
App responsibilityIcon-only buttons require ariaLabel. Danger actions must be paired with a cancel path. Labels should describe outcomes, not mechanisms.
Input
Elementinput with label
ARIA (built-in)aria-invalid when error is set. aria-describedby links to hint/error text.
App responsibilityAlways provide a visible label. Set autocomplete for common fields. Error text should describe how to fix the problem, not just that one exists.
Textarea
Elementtextarea with label
ARIA (built-in)aria-invalid when error is set. aria-describedby links to hint/error text.
App responsibilitySet a meaningful label. If the field has a character limit, expose it as hint text before the limit is reached.
Select
Elementselect with label
ARIA (built-in)aria-invalid when error is set. Native select behavior for keyboard and screen readers.
App responsibilityInclude a default empty option when no value is required by default. Group long option lists with optgroup.
Checkbox
Elementinput[type="checkbox"] with label
ARIA (built-in)Native checkbox semantics. aria-invalid on error.
App responsibilityGroup related checkboxes in a fieldset with legend. Label text must describe the specific option being toggled.
Radio
Elementinput[type="radio"] with label
ARIA (built-in)Native radio semantics with shared name group.
App responsibilityAlways wrap in a fieldset with legend to name the group. All options in a group must share the same name attribute.
Switch
Elementbutton
ARIA (built-in)role="switch" with aria-checked="true|false".
App responsibilityProvide a label that names the setting being toggled. Label should describe the feature, not the switch state ("Email notifications", not "On/Off").
Modal
Elementdialog (native)
ARIA (built-in)Native dialog role. Focus trapped inside when open. Esc closes via cancel event.
App responsibilityReturn focus to the trigger element after modal closes. Title should describe the task, not just say "Dialog". Avoid stacking modals.
Accordion
Elementsdetails / summary (native disclosure)
ARIA (built-in)Native open/closed state via details element. aria-disabled on disabled triggers.
App responsibilityTrigger labels must be self-explanatory without expanding. Mandatory actions should not live inside collapsed panels.
Tooltip
Elementdiv with role="tooltip"
ARIA (built-in)role="tooltip". Trigger receives aria-describedby pointing to the tooltip element.
App responsibilityNever put required information only in a tooltip — touch users cannot access it. Tooltip content should be supplementary, not critical.
Avatar
Elementspan (non-interactive)
ARIA (built-in)Fallback icon is aria-hidden. Image gets alt from the alt prop.
App responsibilityProvide meaningful alt text for identity photos. If the avatar is purely decorative (e.g. in a group list that already names the person), use an empty alt.
Breadcrumb
Elementsnav > ol > li > a
ARIA (built-in)aria-label="Breadcrumb" on nav. aria-current="page" on the current item. Separators are aria-hidden.
App responsibilityThe last item should reflect the current page title. Do not make the current item a link.