fix(web): use concise accessible names for infotips (#42751)

Co-authored-by: zhsama <torvalds@linux.do>
This commit is contained in:
yyhandzhsama authored and GitHub committed 2026-09-23 01:02:21 +00:00
1 parent 0f6a6227f5
commit c9e59a5590
146 files changed
+761 -812

No files matched your search

+2
View File
@@ -64,6 +64,7 @@ Upstream behavior remains owned by the [Base UI documentation].
| ------------- | ------------------------------------------------------------------------------------------- |
| [Button] | Action semantics, submit and link choices, loading versus disabled, and content spacing. |
| [Icon Button] | Accessible names, decorative glyphs, appearance ownership, and primitive composition. |
| [Infotip] | Explanation triggers, concise dialog names, and hint surface ownership. |
| [Input Group] | Compound input anatomy, shared-surface ownership, DOM order, focus, and interactive addons. |
### Cross-component guides
@@ -89,6 +90,7 @@ For a known contract, go directly to its guide above.
[Button]: ./src/button/README.md
[Forms]: ./docs/forms.md
[Icon Button]: ./src/icon-button/README.md
[Infotip]: ./src/infotip/README.md
[Input Group]: ./src/input-group/README.md
[Overlays]: ./docs/overlays.md
[Public API authoring]: ./docs/authoring.md
@@ -126,12 +126,9 @@ with a second label or a competing error association:
```
Use `DialogTitle` and, when useful, `DialogDescription` for a short dialog summary. Do not turn a
whole form or rich dialog body into one description. Per [Base UI Tooltip guidance], Tooltip is a
supplemental visual label, not the trigger's accessible-name source. Base UI specifically recommends
an `aria-label` that closely matches the Tooltip content; apply that to icon-only triggers. When
persistent visible trigger text already supplies the name, preserve the content-derived name per
W3C and MDN guidance instead of adding a redundant override merely because Tooltip is present. Use
[Overlay] choices for essential, structured, interactive, or touch-reachable information.
whole form or rich dialog body into one description. Tooltip content is not the trigger's
accessible-name source: icon-only triggers still need their own name, while persistent visible
trigger text should continue to name the control.
Prefer descriptions associated with DOM text. When considering `aria-description`, verify target
browser and assistive-technology behavior. The [AccName 1.2 working draft] gives
@@ -159,7 +156,6 @@ that source computes to an empty description. Do not stack mechanisms to force r
[APG]: https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions#namingtechniques
[AccName 1.2 working draft]: https://www.w3.org/TR/accname-1.2#mapping_additional_nd_description
[Base UI Tooltip guidance]: https://base-ui.com/react/components/tooltip#usage-guidelines
[Base UI accessibility]: https://base-ui.com/react/overview/accessibility
[Button]: ../src/button/README.md
[Forms]: forms.md
+5 -2
View File
@@ -64,8 +64,10 @@ Toast remains one layer above ordinary overlays.
- Use `AlertDialog` only for a destructive or must-confirm decision requiring an explicit answer.
- Use `Drawer` for side-panel interactions that follow the drawer model.
- Use `DropdownMenu` for button-triggered action lists and `ContextMenu` for context actions.
- Use [`Tooltip`] only for a short, non-interactive visual label. The trigger already needs an
accessible name; use Popover for information users must reach on touch.
- Use [`Tooltip`] for a supplementary visual label on a control that already has a primary
action and an accessible name. Use `Infotip` when the information icon's action is to open
the explanation. Text length alone does not determine the component. Follow the
[Infotip contract] for its composition and accessible names.
- Use [`PreviewCard`] as a non-interactive enhancement for a link destination. Essential
information must also exist without the preview.
- Use [`Popover`] for explanatory, structured, or interactive content that users must reach on
@@ -76,6 +78,7 @@ renders a non-button element, set `nativeButton={false}`. Let the primitive own
spacing unless its API documents a measured exception.
[Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals
[Infotip contract]: ../src/infotip/README.md
[MDN `isolation`]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/isolation
[`Dialog.Portal`]: https://base-ui.com/react/components/dialog#portal
[`Popover`]: https://base-ui.com/react/components/popover
+55
View File
@@ -0,0 +1,55 @@
# Infotip
Use `Infotip` when an information icon's primary action is opening an explanation. Use
`Tooltip` when the trigger already has another primary action and only needs a visual hint.
## Naming
Prefer an existing visible topic. It can name both the trigger and the dialog:
```tsx
function ProcessingHint() {
const labelId = useId()
return (
<div className="flex items-center gap-1">
<span id={labelId}>Document processing</span>
<Infotip>
<InfotipTrigger aria-labelledby={labelId} />
<InfotipContent aria-labelledby={labelId}>
Documents are processed in the order they are received.
</InfotipContent>
</Infotip>
</div>
)
}
```
If the popup has its own visible title, use `InfotipTitle`. It gives the dialog its accessible name:
```tsx
<Infotip>
<InfotipTrigger aria-label="Processing priority" />
<InfotipContent>
<InfotipTitle>Processing priority</InfotipTitle>
<p>Priority determines which documents are processed first.</p>
</InfotipContent>
</Infotip>
```
Keep explanations, links, and other structured content as normal popup children rather than
turning the whole body into the dialog description.
A few rules:
- The trigger's name must exist while the popup is closed.
- Prefer `aria-labelledby` when a suitable visible topic already exists; otherwise use a short
localized `aria-label`.
- Sharing one short topic between the trigger and dialog is valid. Do not copy the full explanation
into their accessible names.
- Do not derive names from React children or automatically describe the dialog with its entire body.
## Visual ownership
Infotip owns its hint surface, spacing, typography, radius, and shadow. Callers may constrain width
or preserve intentional line breaks.
+29 -8
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect, userEvent, waitFor, within } from 'storybook/test'
import { Infotip, InfotipContent, InfotipTrigger } from '.'
import { Infotip, InfotipContent, InfotipTitle, InfotipTrigger } from '.'
import { Button } from '../button'
const meta = {
@@ -12,7 +12,7 @@ const meta = {
docs: {
description: {
component:
'An information button that opens explanatory content using Popover semantics. Compose the root, icon trigger, and content. Name the trigger and the dialog; use aria-labelledby when a visible heading exists. The popup remains available to keyboard and touch users and may contain links.',
'An information button whose primary action is opening an explanation. Unlike Tooltip, it uses Popover semantics and supports click, touch, keyboard, and links. Name the trigger and dialog with a short topic, preferably referencing an existing visible label. Keep the explanation in the body; do not copy it into aria-label.',
},
},
},
@@ -21,19 +21,24 @@ const meta = {
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
function ProcessingHint() {
const labelId = React.useId()
return (
<div className="flex items-center gap-1 text-text-secondary">
Document processing
<span id={labelId}>Document processing</span>
<Infotip>
<InfotipTrigger aria-label="Document processing" />
<InfotipContent aria-label="Document processing">
<InfotipTrigger aria-labelledby={labelId} />
<InfotipContent aria-labelledby={labelId}>
Documents are processed in the order they are received. Larger files can take longer to
finish indexing.
</InfotipContent>
</Infotip>
</div>
),
)
}
export const Default: Story = {
render: () => <ProcessingHint />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const body = within(canvasElement.ownerDocument.body)
@@ -75,6 +80,22 @@ export const WithLink: Story = {
},
}
function HeadingHint() {
return (
<Infotip defaultOpen>
<InfotipTrigger aria-label="Processing priority" />
<InfotipContent>
<InfotipTitle>Processing priority</InfotipTitle>
<p>Priority determines which documents are processed first.</p>
</InfotipContent>
</Infotip>
)
}
export const WithHeading: Story = {
render: () => <HeadingHint />,
}
function ControlledExample() {
const [open, setOpen] = React.useState(false)
return (
+4 -2
View File
@@ -22,7 +22,9 @@ const iconSizeClassNames = {
} as const
const Infotip = BasePopover.Root
const InfotipTitle = BasePopover.Title
type InfotipProps<Payload = unknown> = BasePopover.Root.Props<Payload>
type InfotipTitleProps = BasePopover.Title.Props
type InfotipTriggerProps<Payload = unknown> = Omit<
BasePopover.Trigger.Props<Payload>,
@@ -103,5 +105,5 @@ function InfotipContent({
)
}
export { Infotip, InfotipContent, InfotipTrigger }
export type { InfotipContentProps, InfotipProps, InfotipTriggerProps }
export { Infotip, InfotipContent, InfotipTitle, InfotipTrigger }
export type { InfotipContentProps, InfotipProps, InfotipTitleProps, InfotipTriggerProps }
@@ -22,7 +22,7 @@ const meta = {
docs: {
description: {
component:
'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` (done automatically in these stories) so multiple tooltips share open/close delays. Each tooltip pairs a `TooltipTrigger` with a `TooltipContent` and supports placement and offsets.\n\n**Usage contract** (mirrors the [Base UI tooltip guidelines](https://base-ui.com/react/components/tooltip#alternatives-to-tooltips)):\n\n- Tooltips are **supplementary visual labels** for sighted mouse and keyboard users. They are disabled on touch devices and are not announced to screen readers.\n- The trigger **must carry its own `aria-label` or visible text** that matches the tooltip — the tooltip does not replace labeling.\n- Keep content short and non-interactive (an icon-button label, a keyboard shortcut, one-word clarification).\n- **Do not** place descriptions, prose, links, or interactive controls inside a tooltip — touch and screen-reader users cannot reach them.\n- For hover-triggered rich previews that users move their cursor onto, use `PreviewCard` (dwell-able, structured content).\n- For an info icon that explains a concept (an "infotip"), or for any hover popup that needs interactive content or to reach touch/assistive-tech users, use `Popover` with `openOnHover` on the trigger.',
'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` so nested tooltips share open and close delays. Each tooltip pairs a `TooltipTrigger` with `TooltipContent` and supports placement and offsets.',
},
},
},
@@ -45,7 +45,7 @@ export const IconButton: Story = {
docs: {
description: {
story:
'The canonical tooltip use case: an icon-only button surfaces its accessible label as a tooltip for sighted mouse and keyboard users. The trigger already carries `aria-label` — the tooltip mirrors that label visually; it does **not** replace it.',
'Icon-only actions keep their own accessible name while the tooltip provides the matching visual label.',
},
},
},
@@ -71,8 +71,7 @@ export const KeyboardShortcut: Story = {
parameters: {
docs: {
description: {
story:
'A short, supplementary hint that surfaces a keyboard shortcut next to a visible button label. The trigger is fully self-describing ("Save"); the tooltip only adds non-essential extra clarity for mouse/keyboard users.',
story: 'A visible button label can use a tooltip for a supplementary keyboard shortcut.',
},
},
},
-19
View File
@@ -8,25 +8,6 @@ import { resolveClassName } from '../internals/resolve-class-name'
import { hintPopupClassName } from '../overlay-shared'
import { parsePlacement } from '../placement'
/**
* Tooltip is an **ephemeral hint** tied to a trigger (typically an icon button,
* badge, or short label). It follows Base UI's Tooltip semantics:
*
* - Opens on pointer hover or keyboard focus on the trigger.
* - Closes as soon as the pointer leaves the trigger — the popup itself is
* **not dwell-able**; users cannot move their cursor onto the tooltip.
* - Must contain only short, non-interactive text. No links, buttons, form
* controls, or structured panels.
*
* If you need any of the following, use `PreviewCard` instead (hover-triggered
* rich preview that users can move their cursor onto):
*
* - Multi-line or structured content (icon + title + metadata)
* - Content the user needs to "stop and read" for more than ~1 second
* - Content wider than ~300px
*
* If you need interactive affordances (buttons, links, forms) use `Popover`.
*/
const TooltipProvider = BaseTooltip.Provider
const Tooltip = BaseTooltip.Root
const TooltipTrigger = BaseTooltip.Trigger