ee8797c142
Co-authored-by: Copilot <copilot@github.com>
6.8 KiB
6.8 KiB
Tooltip
A text hint that appears on hover. Implemented entirely in CSS using Tailwind's group and group-hover utilities — no JavaScript required.
HTML structure
span.relative.inline-flex.items-center.group
{trigger component rendered inline}
span.tooltip-text.absolute.z-50.px-2.py-1.text-xs.rounded.bg-foreground.text-background
.whitespace-nowrap.pointer-events-none
.opacity-0.group-hover:opacity-100.transition-opacity.duration-150
.{position classes}
{tooltip text}
CSS mechanics
| Utility | Effect |
|---|---|
group on wrapper |
Enables group-hover:* utilities on descendants |
opacity-0 |
Tooltip invisible at rest |
group-hover:opacity-100 |
Tooltip fades in when the wrapper (group) is hovered |
transition-opacity duration-150 |
150ms fade animation |
pointer-events-none |
Tooltip itself doesn't interfere with hover detection |
bg-foreground text-background |
Dark-on-light / light-on-dark automatically via CSS variables |
whitespace-nowrap |
Prevents the tooltip from wrapping |
z-50 |
Floats above surrounding content |
Position classes by position parameter:
| Position | Classes |
|---|---|
top (default) |
bottom-full mb-1.5 left-1/2 -translate-x-1/2 |
bottom |
top-full mt-1.5 left-1/2 -translate-x-1/2 |
left |
right-full mr-1.5 top-1/2 -translate-y-1/2 |
right |
left-full ml-1.5 top-1/2 -translate-y-1/2 |
Constructor signature
public Tooltip(
string text,
IHtmxComponent trigger,
string position = "top")
| Parameter | Description |
|---|---|
text |
Tooltip label (plain text; HTML not supported) |
trigger |
Any IHtmxComponent that acts as the hover target |
position |
"top" / "bottom" / "left" / "right" |
Usage examples
Icon button with tooltip
new Tooltip(
text: "Delete item",
trigger: new Button("🗑", size: "icon", variant: "ghost"))
Top/bottom/left/right positions
new Tooltip(text: "Above", trigger: new Button("Hover me"), position: "top")
new Tooltip(text: "Below", trigger: new Button("Hover me"), position: "bottom")
new Tooltip(text: "Left", trigger: new Button("Hover me"), position: "left")
new Tooltip(text: "Right", trigger: new Button("Hover me"), position: "right")
Tooltip on an Avatar
new Tooltip(
text: user.DisplayName ?? "Unknown user",
trigger: new Avatar(fallback: user.Initials, src: user.AvatarUrl))
Tooltip on a disabled-looking button
new Tooltip(
text: "You need admin access",
trigger: new Button(
"Publish",
variant: "default",
hxAttrs: "disabled aria-disabled='true' tabindex='-1'"))
Tips and tricks
- The tooltip text is plain text — HTML special characters in
textwill be HTML-encoded automatically. - Tooltip position may overflow the viewport if the trigger is near an edge — test all four positions and choose the one that fits.
- Since there is no JS, the tooltip works even when JavaScript is disabled.
- The trigger receives the
groupclass implicitly — this meansgroup-hover:*utilities on any child of the trigger will also activate on hover. Keep this in mind if the trigger component uses nested group utilities. - For touch devices the hover state is never triggered — consider providing the tooltip content elsewhere (e.g. as a
descriptionon a form field) if the information is essential. - To show a tooltip on a non-interactive element (e.g. a truncated table cell), wrap the element in a
<span>via a custom slot and pass that as the trigger. - To show a tooltip on a non-interactive element (e.g. a truncated table cell), wrap the element in a
<span>via a custom slot and pass that as the trigger.
Complete page example
Templates/ActionToolbarPage.htmx
<div class="max-w-3xl mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Document editor</h1>
<div class="flex items-center gap-2 border rounded-md p-2 mb-6">
$$BoldBtn$$
$$ItalicBtn$$
$$UnderlineBtn$$
$$SepToolbar$$
$$UndoBtn$$
$$RedoBtn$$
</div>
<div class="border rounded-md p-4 min-h-64 prose">
$$EditorContent$$
</div>
</div>
Templates/ActionToolbarPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class ActionToolbarPage : ActionToolbarPageBase
{
private readonly IHtmxComponent _bold;
private readonly IHtmxComponent _italic;
private readonly IHtmxComponent _underline;
private readonly IHtmxComponent _sep;
private readonly IHtmxComponent _undo;
private readonly IHtmxComponent _redo;
private readonly byte[] _content;
public ActionToolbarPage()
{
_bold = TooltipButton("Bold", "B", "font-bold");
_italic = TooltipButton("Italic", "I", "italic");
_underline = TooltipButton("Underline", "U", "underline");
_sep = new Components.Separator(orientation: "vertical", extraClasses: "mx-1 h-6");
_undo = TooltipButton("Undo (Ctrl+Z)", "↩", "");
_redo = TooltipButton("Redo (Ctrl+Y)", "↪", "");
_content = "<p>Start typing...</p>".ToUtf8Bytes();
}
private static IHtmxComponent TooltipButton(string tip, string label, string textClass)
=> new Components.Tooltip(
content: tip,
trigger: new Components.Button(
label,
variant: "ghost",
size: "icon",
extraClasses: textClass));
protected override void RenderBoldBtn(HtmxRenderContext ctx) => _bold.Render(ctx.Next());
protected override void RenderItalicBtn(HtmxRenderContext ctx) => _italic.Render(ctx.Next());
protected override void RenderUnderlineBtn(HtmxRenderContext ctx) => _underline.Render(ctx.Next());
protected override void RenderSepToolbar(HtmxRenderContext ctx) => _sep.Render(ctx.Next());
protected override void RenderUndoBtn(HtmxRenderContext ctx) => _undo.Render(ctx.Next());
protected override void RenderRedoBtn(HtmxRenderContext ctx) => _redo.Render(ctx.Next());
protected override void RenderEditorContent(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_content);
}
GET handler
[Handler]
[MapGet("/editor")]
public static partial class GetEditorHandler
{
public record Query();
private static Task<IResult> HandleAsync(Query _, HttpContext ctx, CancellationToken ct)
=> ctx.WriteHtmxPage(new ActionToolbarPage(), title: "Document editor");
}