ee8797c142
Co-authored-by: Copilot <copilot@github.com>
189 lines
6.8 KiB
Markdown
189 lines
6.8 KiB
Markdown
# 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
new Tooltip(
|
|
text: "Delete item",
|
|
trigger: new Button("🗑", size: "icon", variant: "ghost"))
|
|
```
|
|
|
|
### Top/bottom/left/right positions
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
new Tooltip(
|
|
text: user.DisplayName ?? "Unknown user",
|
|
trigger: new Avatar(fallback: user.Initials, src: user.AvatarUrl))
|
|
```
|
|
|
|
### Tooltip on a disabled-looking button
|
|
|
|
```csharp
|
|
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`**
|
|
```html
|
|
<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`**
|
|
```csharp
|
|
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**
|
|
```csharp
|
|
[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");
|
|
}
|
|
```
|