
Accessibility
- 2 installs
- 21.2k repo stars
- Updated August 5, 2026
- elastic/kibana
accessibility skill documents Accessibility guidance for Kibana.
About
accessibility skill documents Accessibility guidance for Kibana. Use this skill when working with or reviewing EUI components, resolving a11y-related (@elastic/eui) ESLint issues, and ensuring proper use of ARIA attributes, focus management, keyboard interactions, and accessible naming conventions.. name: accessibility disable-model-invocation: true
- Accessibility guidance for Kibana.
- Platform-specific setup patterns for accessibility.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for accessibility versus alternatives.
Accessibility by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,786 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
accessibility capabilities & compatibility
- Capabilities
- accessibility quick start · accessibility when to use guidance · accessibility integration patterns
- Works with
- elasticsearch
- Use cases
- security audit
What accessibility says it does
disable-model-invocation: true
**Writing or refactoring an EUI component.** Open the matching guide from `references/components/index.md` and follow the canonical pattern (props, accessible names, focus, ids).
npx skills add https://github.com/elastic/kibana --skill accessibilityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 21.2k |
| Last updated | August 5, 2026 |
| Repository | elastic/kibana ↗ |
How do I use accessibility correctly?
Accessibility guidance for Kibana. Use this skill when working with or reviewing EUI components, resolving a11y-related (@elastic/eui) ESLint issues, and ensuring proper use of ARIA attributes, focus
Who is it for?
Teams implementing accessibility workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about accessibility, accessibility guidance for kibana. use this skill when working with or reviewing eui compo.
What you get
Working accessibility setup with validated configuration and next steps.
Files
Kibana Accessibility
Accessibility is part of writing the component, not a step that happens after lint complains. Open the matching guide inreferences/components/index.mdbefore writing the JSX, and readreferences/shared_principles.mdfirst.
When to Use
- Writing or refactoring an EUI component. Open the matching guide from
references/components/index.mdand follow the canonical pattern (props, accessible names, focus, ids). - *Fixing an `@elastic/eui/
ESLint error.** Usereferences/eslint.md` to jump from the rule id to the component guide that explains the canonical fix. - General a11y or non-EUI question. Read
references/shared_principles.md.
References
Open only what you need:
- Standards, decision order, accessible naming, i18n, html ids, keyboard/focus, escalation:
references/shared_principles.md - Component guide topic table:
references/components/index.md - ESLint rule id → component guide (with manual-review notes):
references/eslint.md
EUI callouts: EuiCallOut and announceOnMount
Applies to: EuiCallOut
A callout that appears conditionally (validation, async result, toggle, post-submit feedback) is invisible to assistive technology unless you opt into EUI's live-region behavior.
Canonical usage
- Conditional render (
condition && <…>, ternary, branches, early return) → set `announceOnMount` so the callout is announced when it mounts. - Always-mounted, static callout → omit `announceOnMount`; it does not need a live region.
- Conditional but must not announce (rare) → `announceOnMount={false}` and document why in the callsite if non-obvious.
- New user-visible strings (
title, body) → `i18n.translate` (see Localization (i18n) in `../shared_principles.md`).
If `EuiCallOut` uses `{...calloutProps}` and `announceOnMount` is not on the opening tag, merge it at the callsite or in the spread source.
Examples
Conditional
{hasError && (
<EuiCallOut
announceOnMount
title={i18n.translate('form.errorTitle', { defaultMessage: 'Error' })}
color="danger"
>
{errorMessage}
</EuiCallOut>
)}Explicit opt-out
{decorativeCondition && (
<EuiCallOut announceOnMount={false} title="…">
…
</EuiCallOut>
)}Common mistakes
// WRONG — conditional callout without announceOnMount
{hasError && <EuiCallOut title="Error" color="danger" />}
// RIGHT
{hasError && <EuiCallOut announceOnMount title="Error" color="danger" />}
// WRONG — unnecessary on a static callout
<EuiCallOut announceOnMount title="Note" color="primary" />EUI data tables: EuiBasicTable and EuiInMemoryTable
Applies to: EuiBasicTable, EuiInMemoryTable
Tables need a caption exposed to assistive technology so users understand what the grid represents (different from the page <title>). EUI exposes this as `tableCaption`.
Canonical usage
- Pass exactly one `tableCaption` per table instance.
- Caption text describes the dataset or task — “User accounts in this space”, not “Table”.
- If visible nearby text already names the table, you may align caption wording with it; otherwise use `i18n.translate` for new strings (see Localization (i18n) in `../shared_principles.md`).
- If `tableCaption` is supplied through `{...tableProps}`, fix it at the source or merge explicitly at the callsite — never duplicate conflicting captions.
Examples
<EuiBasicTable
tableCaption={i18n.translate('usersList.tableCaption', {
defaultMessage: 'User accounts list',
})}
items={items}
columns={columns}
/>Common mistakes
// WRONG — no caption
<EuiBasicTable items={items} columns={columns} />
// WRONG — generic caption
<EuiBasicTable tableCaption="Table" items={items} columns={columns} />EUI focus and keyboard: interactive controls and tooltip anchors
Applies to: EuiButton, EuiButtonIcon, EuiLink, EuiToolTip
Built-in interactive EUI controls participate in sequential focus navigation (WCAG 2.1.1) automatically — do not interfere with tabIndex. Non-interactive children of EuiToolTip need an explicit tab stop so keyboard users can reveal the tooltip.
Canonical usage
- Interactive EUI controls (
EuiButton,EuiButtonIcon,EuiLink, tabs, …) — never set `tabIndex={-1}`. For conditional disabling, use `disabled` or conditional render.tabIndexis allowed only inside a documented pattern (e.g. roving tabindex) on a component that explicitly opts into it. - `EuiToolTip` anchors — the direct child is the keyboard anchor:
- Already interactive (
EuiButton,EuiButtonIcon,EuiLink, anything withtabIndex/href/onClick) → leave as-is. - Non-interactive (
EuiText,EuiImage,EuiBadgewithoutonClick, plainspan,EuiIcon,EuiHealth,EuiAvatar, …) → add `tabIndex={0}`. - Tooltip `content` and any new accessible name uses `i18n.translate`.
Examples
<EuiButton disabled={isDisabled} onClick={onSave}>
Save
</EuiButton>
<EuiToolTip
content={i18n.translate('myView.infoTooltip', { defaultMessage: 'Info' })}
>
<EuiText tabIndex={0}>Read only</EuiText>
</EuiToolTip>Common mistakes
// WRONG — removes button from tab order
<EuiButton tabIndex={-1} onClick={onSave}>Save</EuiButton>
// WRONG — keyboard cannot reach the tooltip
<EuiToolTip content="Details">
<EuiIcon type="iInCircle" />
</EuiToolTip>
// RIGHT
<EuiToolTip content="Details">
<EuiIcon type="iInCircle" tabIndex={0} />
</EuiToolTip>EUI form layout: EuiFormRow and invalid state
Applies to: EuiFormRow, EuiFieldText, EuiFieldNumber, EuiFilePicker, EuiComboBox, EuiTextArea, EuiSelect
EuiFormRow wires the label, hints, and error to its child control. Assistive technology and visual error styling stay consistent only when the child control’s `isInvalid` matches the row’s `isInvalid`.
Canonical usage
- When `EuiFormRow` has `isInvalid={…}`, the direct child uses the same expression for `isInvalid`.
- Row without `isInvalid` → child does not need it either.
- Typical children:
EuiFieldText,EuiFieldNumber,EuiFilePicker,EuiComboBox,EuiTextArea,EuiSelect,EuiFormControlLayoutDelimited,SingleFieldSelect. - For nested
EuiFormRow, sync the innermost parent–child pair first. - If the child is `{...fieldProps}`, confirm
isInvalidis not already in the spread before adding another. - `isInvalid` is a boolean — no i18n on it. Visible `label` / `error` text uses
i18n.translatewhen added or changed.
Examples
<EuiFormRow label="Name" isInvalid={!!errors.name} error={errors.name}>
<EuiFieldText value={name} onChange={setName} isInvalid={!!errors.name} />
</EuiFormRow>Common mistakes
// WRONG — row marks invalid, child does not
<EuiFormRow label="Name" isInvalid={!!errors.name} error={errors.name}>
<EuiFieldText value={name} onChange={setName} />
</EuiFormRow>
// RIGHT — same expression on both
<EuiFormRow label="Name" isInvalid={!!errors.name} error={errors.name}>
<EuiFieldText value={name} onChange={setName} isInvalid={!!errors.name} />
</EuiFormRow>EUI icons and icon tips: EuiIcon, EuiIconTip, and EuiToolTip
Applies to: EuiIcon, EuiIconTip, EuiToolTip
Each EuiIcon is either decorative or meaningful — pick one and mark it accordingly. When a tooltip wraps a single icon, EuiIconTip is the canonical component.
When to use which
- `EuiIcon` alone — decorative (repeats nearby visible text), or meaningful with its own accessible name.
- `EuiIconTip` — the icon needs a tooltip (help, hint, info). One component, clearer semantics, better defaults than
EuiToolTip+EuiIcon. - `EuiToolTip` + `EuiIcon` — only when
EuiIconTipdoesn't fit: multiple tooltip children, child hasonClick/ handlersEuiIconTipdoesn't support, or the tooltip uses propsEuiIconTipcannot accept. Otherwise migrate toEuiIconTip.
Canonical usage
- Decorative → `aria-hidden={true}`. Do not combine
aria-hiddenwithtabIndex; focusable nodes must be perceivable. - Meaningful → give the icon an accessible name (
aria-labeloraria-labelledby). See Accessible naming, Localization (i18n), and HTML ids in `../shared_principles.md`. - `title` is not a substitute. Native browser tooltip on built-in icon types only — not supported on SVG React components passed as `type`.
- Migrating to `EuiIconTip` — move supported props (
content,position,delay,title,id,aria-label,data-test-subj, icontype/color/size). Do not carrytabIndexover.
Examples
<EuiIcon
type="warning"
color="danger"
aria-label={i18n.translate('myFeature.warningIcon', {
defaultMessage: 'Warning',
})}
/>
<EuiFlexItem>
<EuiIcon type="check" color="success" aria-hidden={true} />
<span>Completed</span>
</EuiFlexItem>
<EuiIconTip
content={i18n.translate('myFeature.helpTip', { defaultMessage: 'Help info' })}
position="right"
type="questionInCircle"
aria-label={i18n.translate('myFeature.helpAria', { defaultMessage: 'Help' })}
/>Common mistakes
// WRONG — meaningful icon without an accessible name
<EuiIcon type="warning" color="danger" />
// WRONG — focusable but hidden from assistive technology
<EuiIcon type="help" tabIndex={0} aria-hidden={true} />
// WRONG — verbose wrapper for a single icon
<EuiToolTip content="Help">
<EuiIcon type="questionInCircle" />
</EuiToolTip>
// RIGHT
<EuiIconTip content="Help" type="questionInCircle" />EUI component guides — canonical accessible usage
Open the guide that matches the component(s) you are writing or refactoring. Read `../shared_principles.md` first; each guide is an extension of those principles.
| Guide | Topic | EUI components |
|---|---|---|
callouts.md | Callouts (announce when conditional) | EuiCallOut |
data_tables.md | Data tables (caption) | EuiBasicTable, EuiInMemoryTable |
focus_and_keyboard.md | Focus & keyboard (tab order, tooltip anchors) | EuiButton, EuiButtonIcon, EuiLink, EuiToolTip |
form_layout.md | Form row + invalid state | EuiFormRow, EuiFieldText, EuiFieldNumber, EuiFilePicker, EuiComboBox, EuiTextArea, EuiSelect |
icons_and_tooltips.md | Icons & icon tips (decorative vs meaningful) | EuiIcon, EuiIconTip, EuiToolTip |
interactive_components.md | Names for interactive controls | EuiBetaBadge, EuiButtonIcon, EuiComboBox, EuiSelect, EuiSuperSelect, EuiPagination, EuiTreeView, EuiBreadcrumbs |
overlays.md | Modals, flyouts, popovers | EuiModal, EuiFlyout, EuiFlyoutResizable, EuiConfirmModal, EuiPopover |
radio_groups.md | Radio groups (name grouping) | EuiRadio, EuiRadioGroup |
tooltip_icon.md | Tooltip on icon button (no duplicate SR text) | EuiToolTip, EuiButtonIcon |
EUI interactive components: accessible names
Applies to: EuiBetaBadge, EuiButtonIcon, EuiComboBox, EuiSelect, EuiSuperSelect, EuiPagination, EuiTreeView, EuiBreadcrumbs
These controls render as interactive elements (buttons, listboxes, pagination, …). Each one needs an accessible name. Read Accessible naming in `../shared_principles.md` for the general rule; this guide adds component-specific notes.
Canonical usage
Walk the naming hierarchy and stop at the first that fits:
1. Existing aria-label / aria-labelledby is correct → leave it. 2. Visible label (EuiFormLabel, EuiTitle, <label>, nearby heading) → wire with `aria-labelledby` + a stable id (see HTML ids in `../shared_principles.md`). Don't duplicate that text into aria-label. 3. No visible label → `aria-label={i18n.translate(...)}`.
Use exactly one mechanism per control — never both aria-label and aria-labelledby.
- `EuiFormRow` already names its direct child — do not add redundant
aria-*to controls inside a form row. - When `EuiToolTip` wraps `EuiButtonIcon` with matching tooltip text, see `tooltip_icon.md` for the duplicate-screen-reader-output pattern.
Examples
const fieldLabelId = useGeneratedHtmlId();
<EuiFormLabel id={fieldLabelId}>
Field (using {bucketAggType} buckets)
</EuiFormLabel>
<EuiComboBox aria-labelledby={fieldLabelId} {...rest} />
<EuiSuperSelect
aria-label={i18n.translate('myView.options.ariaLabel', {
defaultMessage: 'Fancy options',
})}
/>
<EuiPagination
aria-label={i18n.translate('results.pagination', {
defaultMessage: 'Results pagination',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={onPageClick}
/>Common mistakes
// WRONG — both naming mechanisms on the same control
<EuiSelect aria-label="Format" aria-labelledby={labelId} />
// RIGHT — prefer aria-labelledby when a visible label exists
<EuiSelect aria-labelledby={labelId} />
// WRONG — EuiFormRow already supplies the name
<EuiFormRow label="Email">
<EuiFieldText aria-label="Email" />
</EuiFormRow>
// RIGHT
<EuiFormRow label="Email">
<EuiFieldText />
</EuiFormRow>EUI overlays: modals, flyouts, and popovers
Applies to: EuiModal, EuiFlyout, EuiFlyoutResizable, EuiConfirmModal, EuiPopover
Layered UI that traps or shifts focus needs a programmatic accessible name that stays aligned with the visible title.
When to use which
- `EuiModal` — blocking confirmation or form; user must dismiss or complete before continuing.
- `EuiFlyout` / `EuiFlyoutResizable` — non-blocking detail or settings panel, often paired with a list / page selection.
- `EuiConfirmModal` — yes / no or destructive actions; uses the
titleprop API. - `EuiPopover` — contextual menus, filters, or short anchored content; may or may not have a visible title.
Canonical usage
Prefer `aria-labelledby` pointing at the visible title so the spoken name matches what sighted users see.
1. Render a real title inside the overlay (EuiModalTitle, EuiFlyoutTitle, EuiPopoverTitle, EuiTitle, or a heading). 2. Give that title element a stable `id` — use useGeneratedHtmlId() (or htmlIdGenerator() in class components), see HTML ids in `../shared_principles.md`. 3. Set `aria-labelledby` on the overlay container to that id. 4. Reuse one ID variable for both the title and aria-labelledby — never orphan references. 5. `EuiConfirmModal` exposes the title as a prop — wire its rendered DOM through `titleProps={{ id }}` so the id matches aria-labelledby. 6. No suitable visible title (rare for popovers) → use `aria-label` with i18n.translate instead.
Suggested variable names: modalTitleId, flyoutTitleId, confirmModalTitleId, popoverTitleId.
Examples
`EuiModal` / `EuiFlyout` / `EuiFlyoutResizable` — title gets id={...TitleId}, container gets matching aria-labelledby:
const flyoutTitleId = useGeneratedHtmlId();
<EuiFlyout aria-labelledby={flyoutTitleId}>
<EuiFlyoutTitle id={flyoutTitleId}>My title</EuiFlyoutTitle>
</EuiFlyout>`EuiConfirmModal` — aria-labelledby + matching titleProps.id:
const confirmModalTitleId = useGeneratedHtmlId();
return (
<EuiConfirmModal
aria-labelledby={confirmModalTitleId}
title={i18nTexts.modalTitle}
titleProps={{ id: confirmModalTitleId }}
>
<p>{i18nTexts.modalDescription}</p>
</EuiConfirmModal>
);`EuiPopover` — visible title:
const popoverTitleId = useGeneratedHtmlId();
<EuiPopover aria-labelledby={popoverTitleId}>
<EuiPopoverTitle>
<h2 id={popoverTitleId}>Title</h2>
</EuiPopoverTitle>
</EuiPopover>`EuiPopover` — no title, fall back to aria-label:
<EuiPopover
aria-label={i18n.translate('myFeature.filterPopover', {
defaultMessage: 'Filter options',
})}
>
{popoverContent}
</EuiPopover>Common mistakes
// WRONG — aria-label duplicates the visible title as a hidden string
<EuiModal aria-label="Settings">
<EuiModalTitle>Settings</EuiModalTitle>
</EuiModal>
// WRONG — aria-labelledby points at nothing (no titleProps.id wiring)
<EuiConfirmModal aria-labelledby={id} title="Delete?" />
// RIGHT
<EuiConfirmModal aria-labelledby={id} title="Delete?" titleProps={{ id }} />EUI radio groups: EuiRadio and EuiRadioGroup
Applies to: EuiRadio, EuiRadioGroup
Radio buttons are grouped in the accessibility tree by shared `name` values. Without a name, browsers and assistive technology cannot treat options as one exclusive set.
Canonical usage
- Every `EuiRadio` and `EuiRadioGroup` has a `name`.
- Options that belong together share the same
name; distinct groups in one view use different names. nameis a programmatic token — do not wrap it ini18n(see Localization (i18n) in `../shared_principles.md`). Visible `label` text does usei18n.translatewhen added or changed.- Naming: `camelCase` from field, section, or state (
paymentMethod,alertSeverity). Avoidradio1,group1,options. If the context is genuinely unknown,optionGroupis an acceptable last resort — still better than omittingname. - For
{...groupProps}, verifynamein the spread source before adding another.
Examples
<EuiRadio
name="paymentMethod"
label={i18n.translate('payment.creditCard', { defaultMessage: 'Credit Card' })}
checked={selected === 'credit'}
onChange={setSelected}
/><EuiRadioGroup
name="alertSeverity"
options={severityOptions}
idSelected={selectedId}
onChange={onSeverityChange}
/>Common mistakes
// WRONG — assistive technology cannot group these radios
<EuiRadio label="Option A" checked={selected === 'a'} onChange={onChange} />
// RIGHT
<EuiRadio name="myChoice" label="Option A" checked={selected === 'a'} onChange={onChange} />
// WRONG — name is programmatic, not user-visible
<EuiRadio name={i18n.translate('x.name', { defaultMessage: 'paymentMethod' })} />
// RIGHT
<EuiRadio name="paymentMethod" />EUI tooltip on an icon button
Applies to: EuiToolTip, EuiButtonIcon
When `EuiToolTip` wraps `EuiButtonIcon` and the tooltip `content` matches the button's `aria-label`, assistive technology can announce the same text twice. Use `disableScreenReaderOutput` so the tooltip stays available to sighted users while screen readers hear the name once.
Related guides: `focus_and_keyboard.md` (tooltip anchors / tabIndex) · `icons_and_tooltips.md` (EuiIconTip vs EuiToolTip + EuiIcon).
Canonical usage
- `content` equals `aria-label` (same string or same variable / same
i18ncall) → set `disableScreenReaderOutput` on `EuiToolTip`. - `content` differs from `aria-label` → no extra prop; both will be announced as intended.
- Child is not `EuiButtonIcon` → this pattern doesn't apply; check the related guides above.
Prefer a single `i18n.translate` call (same id + defaultMessage) for both `content` and `aria-label` so the strings can't drift apart.
For {...tooltipProps} spreads, merge `disableScreenReaderOutput` at the callsite or in the spread source.
Examples
<EuiToolTip
content={i18n.translate('filter.add', { defaultMessage: 'Add filter' })}
disableScreenReaderOutput
>
<EuiButtonIcon
iconType="plusInCircle"
aria-label={i18n.translate('filter.add', { defaultMessage: 'Add filter' })}
onClick={onAdd}
/>
</EuiToolTip>Common mistakes
// WRONG — screen reader announces "Add filter" twice
<EuiToolTip content={label}>
<EuiButtonIcon iconType="plusInCircle" aria-label={label} onClick={onAdd} />
</EuiToolTip>
// RIGHT
<EuiToolTip content={label} disableScreenReaderOutput>
<EuiButtonIcon iconType="plusInCircle" aria-label={label} onClick={onAdd} />
</EuiToolTip>
// WRONG — different ids, strings may drift apart
content={i18n.translate('a.tooltip', { defaultMessage: 'Add' })}
aria-label={i18n.translate('a.button', { defaultMessage: 'Add' })}
// RIGHT — same id keeps them in sync
content={i18n.translate('a.add', { defaultMessage: 'Add' })}
aria-label={i18n.translate('a.add', { defaultMessage: 'Add' })}@elastic/eui accessibility ESLint rules
Secondary path: when starting from a rule id, jump to the canonical component guide that explains the pattern. Read `shared_principles.md` first; the manual-review column lists shapes the rule cannot resolve automatically.
| Rule id | Component guide | Manual review |
|---|---|---|
@elastic/eui/accessible-interactive-element | `components/focus_and_keyboard.md` | tabIndex only from {...props} or HOC; do not redesign roving tabindex. |
@elastic/eui/badge-accessibility-rules | `components/interactive_components.md` | Direct child of EuiFormRow (row supplies name); {...props} with unknown aria-*. |
@elastic/eui/callout-announce-on-mount | `components/callouts.md` | {...props} on EuiCallOut without explicit announceOnMount; always-mounted callout (rule should not fire). |
@elastic/eui/consistent-is-invalid-props | `components/form_layout.md` | Nested EuiFormRow (innermost pair first); child is {...fieldProps} — confirm isInvalid not already in spread. |
@elastic/eui/icon-accessibility-rules | `components/icons_and_tooltips.md` | {...iconProps} may already include a11y props. |
@elastic/eui/no-unnamed-interactive-element | `components/interactive_components.md` | Direct child of EuiFormRow (row supplies name); {...props} with unknown aria-*. |
@elastic/eui/no-unnamed-radio-group | `components/radio_groups.md` | {...groupProps} — verify name in spread before adding. |
@elastic/eui/prefer-eui-icon-tip | `components/icons_and_tooltips.md` | Not a single EuiIcon child; icon has onClick / unsupported props — skip or escalate. |
@elastic/eui/require-aria-label-for-modals | `components/overlays.md` | {...props} hides wiring; no visible title without UX change — escalate. |
@elastic/eui/require-table-caption | `components/data_tables.md` | tableCaption only via {...tableProps} — fix at source; no duplicate conflicting captions. |
@elastic/eui/sr-output-disabled-tooltip | `components/tooltip_icon.md` | EuiToolTip props from spread; child not EuiButtonIcon. |
@elastic/eui/tooltip-focusable-anchor | `components/focus_and_keyboard.md` | {...anchorProps} or unknown custom anchor. |
Shared principles (accessibility)
These principles apply to every Kibana accessibility decision — when writing new code, refactoring, or fixing a lint error. The component guides under components/ are component-specific extensions of this document.
Precedence on conflict:
1. Task-specific user or system instruction 2. This document 3. Component guide (components/*.md) or ESLint table (eslint.md)
Standards
- Meet WCAG 2.2 AA.
- Follow the WAI-ARIA Authoring Practices Guide (APG) for widget patterns.
- Prefer EUI components over native HTML — EUI handles aria attributes, focus, and keyboard out of the box. Use native HTML only when no suitable EUI component exists.
- Prefer semantic HTML over ARIA — add ARIA only when native semantics are insufficient.
Authoring decision order
Whether you are writing a new component or fixing existing code, work top-down and stop at the first level that resolves the need:
1. Semantics. Pick the right element / EUI component and use its built-in props (label, htmlFor, aria-label, aria-labelledby, aria-describedby, roles). 2. Structural wiring. Connect visible text to controls via stable ids (id + aria-labelledby) instead of duplicating strings into hidden labels. 3. Behavior. Adjust keyboard / focus behavior only when semantics are already correct. 4. Lifecycle hacks last. useEffect for focus or announcements is a fallback — only when no declarative alternative exists.
Accessible naming
- Every interactive element needs an accessible name — buttons, links, inputs, selects, custom controls.
- Prefer visible text (labels, headings, button text) for the name; wire it with `aria-labelledby` + a stable id rather than duplicating into `aria-label`.
- Use exactly one naming mechanism per control — not both `aria-label` and `aria-labelledby`.
- Do not remove
title,alt,aria-label, oraria-labelledbyunless replacing with a stronger alternative. - Images that convey meaning need
alt; decorative images usealt=""oraria-hidden="true".
Localization (i18n)
Visible and assistive-tech strings (aria-label, tableCaption, tooltip content, label, title, error messages, body copy) must be localized — never raw literals. Programmatic tokens (name on radios, internal ids) stay as plain strings.
For i18n APIs, message id conventions, and validation, follow the kibana-i18n skill. Component guides and examples in this skill assume that pattern.
When a file already exposes a shared object (e.g. i18nTexts.modalTitle), follow that local pattern for new strings instead of adding inline i18n.translate calls.
HTML ids
Use EUI's id generators for any id / aria-labelledby / titleProps.id wiring. Call once and store in a descriptive variable (e.g. modalTitleId, fieldLabelId); reuse an existing id variable when it already targets the same element.
Function components — useGeneratedHtmlId from @elastic/eui, called before the first return:
import { useGeneratedHtmlId } from '@elastic/eui';
const labelId = useGeneratedHtmlId();Class components — htmlIdGenerator from @elastic/eui, called inside render() with a stable suffix:
import { htmlIdGenerator } from '@elastic/eui';
render() {
const labelId = htmlIdGenerator()('myLabel');
}Keyboard and focus
- Every interactive element must be reachable and operable from the keyboard alone.
- Use native focusable elements (
<button>,<a>,<input>) overdiv+onClick+tabIndex. - Do not remove or hide visible focus indicators.
- Focus order follows the logical reading sequence.
- Modals and flyouts trap focus and return it to the trigger on close.
- Custom shortcuts must not conflict with browser / screen reader shortcuts.
Minimal, deterministic changes
- Apply the smallest change that fits the canonical pattern.
- No unrelated refactoring, layout / logic / license-header changes.
- Preserve existing behavior and intent.
- Same code shape → same outcome (no subjective styling tweaks).
Type safety
- Do not widen types (
string→any) or suppress errors (@ts-ignore,as any). - New props must match the component’s type definition.
When to escalate
Stop and flag for human review when:
- Spread props hide wiring.
{...props}on the component and you cannot trace whetheraria-labelledby,aria-label,name, etc. are already supplied. - No visible title exists and adding one would change UX / layout — needs design or PM input.
- Conflicting requirements without a clear trade-off (e.g. adding
aria-labelwould duplicate atitle, but removingtitlebreaks another consumer). - Uncertain intent. You cannot tell from the surrounding code whether a label, caption, or name accurately describes the element’s purpose.
Change boundaries
- Do not add narration comments to updated lines; do not delete existing comments unless the guide explicitly says to.
- If a test assertion fails because the DOM changed, update only that assertion — never delete or skip the test.
Related skills
FAQ
What does accessibility do?
accessibility skill documents Accessibility guidance for Kibana.
When should I use accessibility?
User asks about accessibility, accessibility guidance for kibana. use this skill when working with or reviewing eui compo.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.