Files
Htmx/docs/Components/Tooltip.md
T
2026-05-04 19:57:48 +05:00

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 text will 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 group class implicitly — this means group-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 description on 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");
}