Buttons with no words
A button whose whole face is an icon says its name on hover. IconButton takes that name once and spends it twice.
A button with no visible text carries a tooltip that names it. Not a
title, which the browser draws in its own grey box a full second late and
never on a touch screen — a Tooltip, in the app's own popover surface, on the
app's own delay.
The name is not decoration. A chevron, an X and a downward arrow are each three different actions on three different screens, and the icon alone does not distinguish them. Someone landing on a board they have not used before is reading those glyphs cold.
Reach for IconButton
IconButton is a Button that takes a label and spends it twice: once as the
tooltip's text, once as the aria-label. One string, so the two can never drift
apart, and no call site can supply the second and forget the first.
import { IconButton } from "@statshub/ui-web/components/icon-button";
<IconButton
label={t("ui.iconButtons.previousPage", undefined, "Previous page")}
variant="ghost"
onClick={previous}
>
<ChevronLeft className="h-4 w-4" />
</IconButton>;size defaults to icon. Pass hideTooltip for the rare button that already
sits inside something explaining it — a segmented control where the group is
labelled, say — and it renders as a plain Button with the aria-label intact.
When the button is not a Button
Plenty of icon buttons here are native <button>s carrying a page's own
styling, or a SidebarMenuAction, or a primitive's Close. Swapping those for
IconButton would take their appearance with it, so wrap them instead:
<Tooltip>
<TooltipTrigger asChild>
<button aria-label={label} className={/* the page's own */}>
<X className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>The same shape works around a DialogTrigger or CollapsibleTrigger — put
TooltipTrigger outside it, never between the trigger and its button.
Tooltip in a file that also imports a chart library's Tooltip needs an
alias; player-stats-charts.tsx and team-stats-charts.tsx both import ours as
UiTooltip for exactly that reason.
Where the name comes from
Tooltip text is user-facing, so it is translated like everything else:
t(key, undefined, "English fallback"), with the key under ui.iconButtons in
packages/statshub-i18n. The fallback keeps a missing key readable rather than
printing the key itself.
TooltipProvider is mounted once, in the web app's providers.tsx. Individual
screens do not mount their own.
What is not an icon button
A button with an icon and a word is already named — <Save /> Save view
needs no tooltip, and adding one repeats the label under the cursor for no gain.
The rule is about buttons with nothing to read: an icon, or an icon and a
sr-only span.

