ConventionsUI

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.

On this page