Applies to: EuiToolTip, EuiButtonIcon
Every EuiButtonIcon needs two things:
- A visible tooltip for sighted users — wrap the button with
EuiToolTip. Do not use the nativetitleprop onEuiButtonIcon; browser tooltips are unstyled, have no delay control, and are not reliably announced by screen readers across browser / AT combinations. - An accessible name for assistive technology — keep
aria-labelon the button.
When the tooltip content and the button's aria-label match (same string, same variable, or same i18n call), also set disableScreenReaderOutput on EuiToolTip so screen readers announce the name once instead of twice.
Related guides: focus_and_keyboard.md (tooltip anchors / tabIndex) · icons_and_tooltips.md (EuiIconTip vs EuiToolTip + EuiIcon) · tooltip_content.md (no interactive elements inside tooltip content / title).
- Wrap every
EuiButtonIconwithEuiToolTip. Remove anytitleprop from the button. - Pass the same localized string to
EuiToolTipcontentand the button'saria-label. Prefer a singlei18n.translatecall (same id +defaultMessage) referenced from both places so the strings cannot drift apart. - When
contentandaria-labelmatch → adddisableScreenReaderOutputonEuiToolTip. - When
contentandaria-labelintentionally differ (the tooltip elaborates beyond the name) → do not adddisableScreenReaderOutput; both will be announced as intended.
const editLabel = i18n.translate('myFeature.editItem', {
defaultMessage: 'Edit item',
});
<EuiToolTip content={editLabel} disableScreenReaderOutput>
<EuiButtonIcon
iconType="pencil"
aria-label={editLabel}
onClick={onEdit}
/>
</EuiToolTip>// WRONG — no visible tooltip for sighted users
<EuiButtonIcon iconType="trash" aria-label="Delete" onClick={onDelete} />
// WRONG — native `title` is not reliably announced by screen readers and has no consistent visual styling
<EuiButtonIcon title="Delete" aria-label="Delete" iconType="trash" onClick={onDelete} />
// WRONG — tooltip and `aria-label` without `disableScreenReaderOutput` will lead to SR announcement duplication
<EuiToolTip content="Delete">
<EuiButtonIcon iconType="trash" aria-label="Delete" onClick={onDelete} />
</EuiToolTip>
// RIGHT — wrap, keep `aria-label`, and add `disableScreenReaderOutput` when `content` matches
<EuiToolTip content="Delete" disableScreenReaderOutput>
<EuiButtonIcon iconType="trash" aria-label="Delete" onClick={onDelete} />
</EuiToolTip>
// WRONG — different i18n ids 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' })}