Components
Tooltip
A line of help beside whatever it wraps, shown on hover and on keyboard focus, and said by a screen reader because the trigger is wired to it. It is a hint: nothing that matters should live only here, and what must reach a phone is Toggleable, opened by a tap.
Drawn and behaving as Flux UI's tooltip does, measured against its live docs.
Tooltip
The first child is the trigger. An icon button has no text of its own, so the tooltip names it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class TooltipExample : Component
{
// An icon button has no text of its own, so the tooltip becomes its name.
protected override Component? Render() =>
Div.Class("flex pt-8")[
Ui.Tooltip.Content("Settings")[
Ui.Button.Icon(Ui.IconName.Cog6Tooth).IconVariant(Ui.IconVariant.Outline)
]
];
}Info tooltip
More than a line, and essential: Toggleable opens it on a click or a tap, and a Ui.TooltipContent holds what it says.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class TooltipInfoExample : Component
{
protected override Component? Render() =>
Div.Class("flex items-center gap-2 text-sm font-medium")[
"Tax identification number",
Ui.Tooltip.Toggleable()[
Ui.Button.Sm.Ghost.Icon(Ui.IconName.InformationCircle),
Ui.TooltipContent.Class("max-w-[20rem] space-y-2")[
P["For US businesses, enter your 9-digit Employer Identification Number (EIN) without hyphens."],
P["For European companies, enter your VAT number including the country prefix (e.g., DE123456789)."]
]
]
];
}For US businesses, enter your 9-digit Employer Identification Number (EIN) without hyphens.
For European companies, enter your VAT number including the country prefix (e.g., DE123456789).
Position
Above unless it says otherwise: right, below or left.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class TooltipPositionExample : Component
{
protected override Component? Render() =>
Div.Class("flex flex-wrap gap-8 px-12 py-8")[
Ui.Tooltip.Key("top").Content("Settings").Top[Settings()],
Ui.Tooltip.Key("right").Content("Settings").Right[Settings()],
Ui.Tooltip.Key("bottom").Content("Settings").Bottom[Settings()],
Ui.Tooltip.Key("left").Content("Settings").Left[Settings()]
];
private static UiButton Settings() =>
Ui.Button.Icon(Ui.IconName.Cog6Tooth).IconVariant(Ui.IconVariant.Outline);
}Align
Centred on its side unless Start or End puts it flush with the trigger's edge.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class TooltipAlignExample : Component
{
protected override Component? Render() =>
Div.Class("flex pb-8")[
Ui.Tooltip.Content("Flush with its start edge").Bottom.Start[Ui.Button.Sm["Aligned"]]
];
}Shortcut
Kbd shows a shortcut after the text.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class TooltipKbdExample : Component
{
protected override Component? Render() =>
Div.Class("flex pt-8")[
Ui.Tooltip.Content("Save").Kbd("⌘S")[Ui.Button.Sm["Shortcut"]]
];
}Disabled button
A disabled button takes no pointer, so the hover lands on the wrapper and the tooltip still says why.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class TooltipDisabledExample : Component
{
protected override Component? Render() =>
Div.Class("flex pt-8")[
Ui.Tooltip.Content("Available once the form is valid")[
Ui.Button.Sm.Disabled()["Disabled"]
]
];
}Behaviour
Look and placement
Measured on fluxui.dev, in light and dark, by scripts/flux/parity-tooltip.mjs: zinc-800 with white 12px text (zinc-700 inside a hairline in dark), 5px from its trigger, no arrow, no shadow, no transition and no delay. One look: the daisyUI tones are gone.
The content is a popover placed by CSS anchor positioning, and the runtime keeps it inside the viewport as Flux's script does (data-rask-place): it flips to the other side at the viewport's edge, keeps its side where it can and slides along the trigger to stay 5px inside, and is held to the height that is left. Gap(12) and Offset(20) are pixels away from the trigger and along its side; an offset slides it away from the edge it is aligned to, so with End it runs the other way.
The wrapper around the trigger lays out as inline-flex, as Flux's does, so a display written in Class wins: .Class("contents") takes it out of a flex row's layout, which is what an editor's toolbar does.
What opens it
The kit ships no script; the runtime shows it. The wrapper carries data-rask-tooltip="<content id>", one of the runtime's generic hooks, so every tooltip, whatever its trigger (a button, a link, a Span, a disabled button), is in the top layer, where no card can clip it, and behaves as Flux's does, step for step (parity-tooltip.mjs walks both): shown the moment the pointer arrives or keyboard focus does, gone the moment the pointer leaves unless that focus is still there, dismissed by Escape, and hidden by a press on the trigger until the pointer has left and come back. The pointer cannot rest on it.
Tooltip | What happens |
|---|---|
| Any trigger | The runtime opens it, on the pointer and on keyboard focus. Escape dismisses it. |
Toggleable() around a <button> | The browser opens it (popovertarget) on a click; a hover does nothing. Escape closes it, and so does a click outside. |
Toggleable() around anything else | Focus on the wrapper, which a tap gives it, shows it: in place, not in the top layer. Escape does nothing. |
Disabled() | Never shows. The trigger keeps its description. |
What is left: a Toggleable() tooltip around something that is not a <button> has no click to open it by (no runtime hook toggles a popover from an arbitrary element), and a toggleable button's expanded state is the browser's own rather than a written aria-expanded. On a page whose scripts never run (@media (scripting: none)) the stylesheet shows a tooltip in place on :hover and keyboard focus.
What a screen reader hears
The kit wires the trigger to the tooltip the way Flux's script does, at render: a trigger with text of its own gets aria-describedby, and one without (an icon button) gets aria-labelledby, so the tooltip is its name and it needs no AccessibleLabel.
Interactive() says the content is more than a description: the trigger gets aria-controls instead, with aria-expanded false at rest and true while it shows, and the content stays in the reading order. A Toggleable() tooltip is Flux's dropdown to a screen reader: its button gets aria-controls and aria-haspopup, and the content is no role="tooltip".
This needs the trigger to be ONE element: an HTML element (Button[…], A[…], Span[…]) or a kit component that is one, like Ui.Button. Around anything else nothing is wired, as Flux wires nothing but the trigger: use an element. A tooltip cannot be reached by touch at all unless it is Toggleable(), so it is a hint, never the only place something is said.
On a button
Ui.Button.Icon(Ui.IconName.Cog6Tooth).Tooltip("Settings") is Flux's shorthand: the button is wrapped in a Ui.Tooltip that says this, TooltipPosition(Ui.TooltipPosition.Bottom) puts it on another side and TooltipKbd("D") shows a shortcut after it. It names a button that has only an icon (aria-labelledby), and the button still fuses inside a Ui.ButtonGroup.
A button's own Kbd("esc") is another thing: a shortcut drawn inside the button, after its label, where TooltipKbd and a tooltip's Kbd draw it in the tooltip. Ui.Toggle, Ui.EditorButton and a sidebar item collapsed to a rail take a Tooltip the same way.
Reference
Ui.Tooltip
A line of help beside its first child, the trigger, while that is hovered or has keyboard focus.
Step | Takes | What it does |
|---|---|---|
.Content(…) | string | The text it shows. For more, write a Ui.TooltipContent after the trigger. |
.Position(…) | Ui.TooltipPositionTop Right Bottom Left Each is a step of its own too, unless an HTML tag has its name. | Which side of the trigger it opens on. Top when unset. |
.Align(…) | Ui.TooltipAlignCenter Start End Each is a step of its own too, unless an HTML tag has its name. | Where along that side it sits. Centred when unset. |
.Gap(…) | int | The distance from the trigger, in pixels. 5 when unset. |
.Offset(…) | int | How far it slides along its side, in pixels, away from the edge it is aligned to. |
.Kbd(…) | string | A shortcut shown after Content: "⌘S". |
.Disabled() | bool | Never shows. The trigger keeps its description. |
.Toggleable() | bool | Opens on a click rather than a hover: the one tooltip a touch screen shows. |
.Interactive() | bool | Its content is more than a description, so the trigger controls it rather than being described by it. |
Ui.TooltipContent
What a tooltip shows when it is more than a line of text. Class takes a max-w-* or a space-y-*.
Step | Takes | What it does |
|---|---|---|
.Kbd(…) | string | A shortcut shown after the content: "⌘S". |