shshadcn-htmx

Components

Hover Card

A rich preview surface revealed when the user shows interest in a trigger — hover, keyboard focus, or long-press. Built on the native Popover API interestfor interest invoker plus popover="hint". Unlike Tooltip, it may hold interactive content. Zero JavaScript.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/hover-card.json

2. Use it

components/ui/hover-card.tsx
import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card"

<HoverCardTrigger cardFor="user-card" href="/u/productdevbook" class="font-medium underline">
  @productdevbook
</HoverCardTrigger>

<HoverCard id="user-card">
  <p>Card body — links and buttons are allowed here.</p>
</HoverCard>
Or copy the source manually
components/ui/hover-card.tsx
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cloneElement, isValidElement } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"

// Hover Card — shadcn-htmx, htmx v4 + Tailwind v4.
//
// A rich preview surface revealed when the user shows INTEREST in a trigger
// (hover, keyboard focus, or long-press). Unlike Tooltip — which the APG
// forbids from holding interactive content and which defers to this primitive
// — a Hover Card MAY contain links, buttons and other interactive content
// (e.g. a "Follow" button on a user preview).
//
// Built entirely on the native Popover API "interest invoker" mechanism — zero
// JS:
//   - The trigger (an <a> or <button>) carries `interestfor` pointing at the
//     card's id. The browser reveals the card on hover / focus / long-press
//     and hides it on lose-interest, with NO state machine of ours.
//   - The card is `popover="hint"`. Per spec a `hint` popover does NOT light-
//     dismiss `auto` popovers, can itself be light-dismissed, and responds to
//     ESC (close request) — exactly the contract we want for a preview card.
//   - Associating a popover with its interest invoker creates an IMPLICIT
//     anchor reference, so the card is positioned with CSS `position-area`
//     relative to the trigger — no JS positioner, unlike registry/ui/popover.tsx
//     which targets older click-popovers without anchor support.
//
// Progressive enhancement: in browsers without interest invokers the trigger
// is just a normal <a>/<button> (which still works), and `popover="hint"`
// falls back to `popover="manual"`, so the card simply stays hidden — no error,
// no broken UI.
//
// Refs:
//   repos/mdn/files/en-us/web/api/popover_api/using_interest_invokers/index.md
//   repos/mdn/files/en-us/web/api/popover_api/index.md  (popover="hint" state)
//   repos/mdn/files/en-us/web/html/reference/elements/a/index.md:89  (interestfor)
//   repos/mdn/files/en-us/web/css/reference/properties/position-area/index.md
//   repos/shadcn-ui/apps/v4/registry/  (HoverCard anatomy: trigger + content)

export type HoverCardSide = "top" | "right" | "bottom" | "left"

// Layout-only positioning utilities (see app/styles/input.css). They map a
// side hint onto a CSS `position-area` tile relative to the implicit anchor
// (the interest invoker), and fall back to a centred placement in browsers
// without CSS Anchor Positioning. Colour comes from theme tokens below.
const sideAnchor: Record<HoverCardSide, string> = {
  top: "anchor-hovercard-top",
  bottom: "anchor-hovercard-bottom",
  left: "anchor-hovercard-left",
  right: "anchor-hovercard-right",
}

const contentBase =
  "z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none " +
  // Native [popover] is display:none until shown; reveal + animate on open.
  "[&:not(:popover-open)]:hidden " +
  // animate-fade/scale-in keyframed in input.css (shared scn-popover-in).
  "[&:popover-open]:animate-[scn-popover-in_120ms_ease-out]"

export function hoverCardContentClasses(opts?: {
  side?: HoverCardSide
  class?: ClassValue
}): string {
  const side = opts?.side ?? "bottom"
  return cn(contentBase, sideAnchor[side], opts?.class)
}

type HoverCardTriggerProps = PropsWithChildren<{
  // Id of the HoverCard this reveals (its `interestfor` target).
  cardFor: string
  // Render the wrapped child element (e.g. an <a href>) with the trigger
  // wiring merged onto it, instead of the default bare <a>. SSR-friendly
  // equivalent of shadcn's Radix `asChild`.
  asChild?: boolean
  // Destination for the default <a>. Interest invokers REVEAL on hover/focus
  // but the trigger still navigates on click, so a real href keeps it useful
  // (and functional in non-supporting browsers).
  href?: string
  class?: ClassValue
  id?: string
}>

export function HoverCardTrigger(props: HoverCardTriggerProps) {
  const { cardFor, asChild, href, class: className, id, children, ...rest } = props

  // asChild: clone the single child (an <a>/<button>) and merge the interest-
  // invoker wiring onto it so the markup the page already has becomes the
  // trigger — no extra wrapper element in the accessibility tree.
  if (asChild && isValidElement(children)) {
    const child = children as any
    return cloneElement(child, {
      ...rest,
      interestfor: cardFor,
      "data-slot": "hover-card-trigger",
      class: cn(child?.props?.class, className),
    })
  }

  // Default: a real <a>. interestfor reveals the card on interest; click still
  // navigates. Anchors are the canonical interest-invoker element (MDN).
  return (
    <a
      id={id}
      href={href ?? "#"}
      interestfor={cardFor}
      data-slot="hover-card-trigger"
      class={cn(className)}
      {...rest}
    >
      {children}
    </a>
  )
}

type HoverCardProps = PropsWithChildren<{
  // Required — referenced by the trigger's `interestfor`.
  id: string
  // Placement relative to the trigger. Drives `position-area` (anchor) with a
  // centred fallback. Default "bottom".
  side?: HoverCardSide
  class?: ClassValue
  // Forward hx-*, data-*, aria-* (e.g. hx-get to lazily fetch the preview).
  [key: string]: unknown
}>

export function HoverCard(props: HoverCardProps) {
  const { id, side = "bottom", class: className, children, ...rest } = props
  return (
    <div
      id={id}
      // `hint`: shows on interest, light-dismissable, ESC-closeable, and does
      // NOT close sibling `auto` popovers. Falls back to `manual` (stays
      // hidden) in unsupporting browsers — safe progressive enhancement.
      // Cast: hono/jsx's `popover` type predates the `"hint"` state.
      {...({ popover: "hint" } as Record<string, string>)}
      data-slot="hover-card"
      data-side={side}
      class={hoverCardContentClasses({ side, class: className })}
      {...rest}
    >
      {children}
    </div>
  )
}

1. Save the file

Copy hover-card.html into templates/components/.

2. Use it

templates/components/hover-card.html
{% from "components/hover-card.html" import hover_card_trigger, hover_card_open, hover_card_close %}

{{ hover_card_trigger("@productdevbook", card_for="user-card", href="/u/productdevbook", class_="font-medium underline") }}

{% call hover_card_open(id="user-card") %}
  <p>Card body — links and buttons are allowed here.</p>
{% endcall %}
View source
templates/components/hover-card.html
{# Hover Card macros — shadcn-htmx, htmx v4 + Tailwind v4.

   A rich preview surface revealed on INTEREST (hover / focus / long-press)
   of a trigger. Unlike Tooltip it MAY hold interactive content. Built on the
   native Popover API interest-invoker mechanism — zero JS:
     - the trigger carries `interestfor` pointing at the card's id;
     - the card is `popover="hint"` (shows on interest, ESC-closeable, does
       not light-dismiss `auto` popovers; falls back to `manual` when the
       feature is unsupported);
     - the implicit anchor reference lets CSS `position-area` place the card.

   Refs:
     repos/mdn/files/en-us/web/api/popover_api/using_interest_invokers/index.md
     repos/mdn/files/en-us/web/api/popover_api/index.md  (popover="hint")
     repos/mdn/files/en-us/web/css/reference/properties/position-area/index.md

   Usage:
     {% from "components/hover-card.html" import hover_card_trigger, hover_card_open, hover_card_close %}

     {{ hover_card_trigger("@productdevbook", card_for="user-card", href="/u/productdevbook", class_="font-medium underline") }}

     {% call hover_card_open(id="user-card") %}
       <p>Card body — links and buttons are allowed here.</p>
     {% endcall %} #}

{% macro hover_card_trigger(label, card_for, href="#", class_="", id=none) %}
<a {% if id %}id="{{ id }}"{% endif %}
   href="{{ href }}"
   interestfor="{{ card_for }}"
   data-slot="hover-card-trigger"
   class="{{ class_ }}">{{ label }}</a>
{% endmacro %}

{% macro hover_card_open(id, side="bottom", extra_class="", attrs={}) %}
{%- set sides = {
    "top":    "anchor-hovercard-top",
    "bottom": "anchor-hovercard-bottom",
    "left":   "anchor-hovercard-left",
    "right":  "anchor-hovercard-right"
} -%}
<div id="{{ id }}"
     popover="hint"
     data-slot="hover-card" data-side="{{ side }}"
     class="z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] {{ sides[side] }} {{ extra_class }}"
     {% for k, v in attrs.items() %}{{ k|replace('_','-') }}="{{ v }}" {% endfor %}>
{% endmacro %}

{% macro hover_card_close() %}</div>{% endmacro %}

1. Save the file

Add hover-card.tmpl alongside your templates.

2. Use it

components/hover-card.tmpl
{{template "hover_card_trigger" (dict "Label" "@productdevbook" "CardFor" "user-card" "Href" "/u/productdevbook" "Class" "font-medium underline")}}

{{template "hover_card" (dict "ID" "user-card" "Body" (htmlSafe `<p>Card body — links and buttons are allowed here.</p>`))}}
View source
components/hover-card.tmpl
{{/*
  Hover Card templates — shadcn-htmx, htmx v4 + Tailwind v4.

  A rich preview surface revealed on INTEREST (hover / focus / long-press) of a
  trigger. Unlike Tooltip it MAY hold interactive content. Built on the native
  Popover API interest-invoker mechanism — zero JS:
    - the trigger carries `interestfor` pointing at the card's id;
    - the card is `popover="hint"` (shows on interest, ESC-closeable, does not
      light-dismiss `auto` popovers; falls back to `manual` when unsupported);
    - the implicit anchor reference lets CSS `position-area` place the card.

  Refs:
    repos/mdn/files/en-us/web/api/popover_api/using_interest_invokers/index.md
    repos/mdn/files/en-us/web/api/popover_api/index.md  (popover="hint")
    repos/mdn/files/en-us/web/css/reference/properties/position-area/index.md

      type HoverCardArgs struct {
          ID, Side string
          Body     template.HTML
      }
      type HoverCardTriggerArgs struct {
          Label, CardFor, Href, Class, ID string
      }
*/}}

{{define "hover_card"}}
{{- $side := or .Side "bottom" -}}
{{- $sides := dict "top" "anchor-hovercard-top" "bottom" "anchor-hovercard-bottom" "left" "anchor-hovercard-left" "right" "anchor-hovercard-right" -}}
<div id="{{.ID}}" popover="hint" data-slot="hover-card" data-side="{{$side}}"
     class="z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] {{index $sides $side}}">
  {{.Body}}
</div>
{{end}}

{{define "hover_card_trigger"}}
<a {{if .ID}}id="{{.ID}}"{{end}}
   href="{{or .Href "#"}}"
   interestfor="{{.CardFor}}"
   data-slot="hover-card-trigger"
   class="{{.Class}}">{{.Label}}</a>
{{end}}

1. Save the file

Drop hover_card.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/hover_card.ex
<.hover_card_trigger card_for="user-card" href="/u/productdevbook" class="font-medium underline">
  @productdevbook
</.hover_card_trigger>

<.hover_card id="user-card">
  <p>Card body — links and buttons are allowed here.</p>
</.hover_card>
View source
lib/my_app_web/components/hover_card.ex
defmodule ShadcnHtmx.Components.HoverCard do
  @moduledoc """
  Hover Card — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  A rich preview surface revealed on INTEREST (hover / focus / long-press) of a
  trigger. Unlike Tooltip it MAY hold interactive content (links, buttons).
  Built on the native Popover API interest-invoker mechanism — zero JS:

    * the trigger carries `interestfor` pointing at the card's id;
    * the card is `popover="hint"` — shows on interest, ESC-closeable, does not
      light-dismiss `auto` popovers; falls back to `manual` when unsupported;
    * the implicit anchor reference lets CSS `position-area` place the card.

  Refs:
    repos/mdn/files/en-us/web/api/popover_api/using_interest_invokers/index.md
    repos/mdn/files/en-us/web/api/popover_api/index.md  (popover="hint")
    repos/mdn/files/en-us/web/css/reference/properties/position-area/index.md

  ## Examples

      <.hover_card_trigger card_for="user-card" href="/u/productdevbook" class="font-medium underline">
        @productdevbook
      </.hover_card_trigger>

      <.hover_card id="user-card">
        <p>Card body — links and buttons are allowed here.</p>
      </.hover_card>
  """

  use Phoenix.Component

  @sides %{
    "top" => "anchor-hovercard-top",
    "bottom" => "anchor-hovercard-bottom",
    "left" => "anchor-hovercard-left",
    "right" => "anchor-hovercard-right"
  }

  attr :id, :string, required: true
  attr :side, :string, default: "bottom", values: ~w(top right bottom left)
  attr :class, :string, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  def hover_card(assigns) do
    assigns = assign(assigns, :side_class, Map.fetch!(@sides, assigns.side))

    ~H"""
    <div
      id={@id}
      popover="hint"
      data-slot="hover-card"
      data-side={@side}
      class={[
        "z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none",
        "[&:not(:popover-open)]:hidden",
        "[&:popover-open]:animate-[scn-popover-in_120ms_ease-out]",
        @side_class,
        @class
      ]}
      {@rest}
    >
      {render_slot(@inner_block)}
    </div>
    """
  end

  attr :card_for, :string, required: true
  attr :href, :string, default: "#"
  attr :class, :string, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  def hover_card_trigger(assigns) do
    ~H"""
    <a
      href={@href}
      interestfor={@card_for}
      data-slot="hover-card-trigger"
      class={@class}
      {@rest}
    >
      {render_slot(@inner_block)}
    </a>
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens.

2. Use it

snippets/hover-card.html
<a href="/u/productdevbook" interestfor="user-card" data-slot="hover-card-trigger"
   class="font-medium text-primary underline-offset-4 hover:underline">@productdevbook</a>

<div id="user-card" popover="hint" data-slot="hover-card" data-side="bottom"
  class="z-50 m-0 w-64 rounded-md border bg-popover p-4 … anchor-hovercard-bottom">
  Card body — links and buttons are allowed here.
</div>
View source
snippets/hover-card.html
<!--
  shadcn-htmx — raw HTML hover-card snippet.

  A rich preview surface revealed on INTEREST (hover / focus / long-press) of a
  trigger. Unlike a tooltip it MAY hold interactive content (links, buttons).
  Built on the native Popover API interest-invoker mechanism — NO JS required:
    - the trigger <a> carries `interestfor` pointing at the card's id;
    - the card is `popover="hint"` — shows on interest, ESC-closeable, and does
      not light-dismiss `auto` popovers. It falls back to `popover="manual"`
      (stays hidden) in browsers without interest-invoker support, so the link
      still works — pure progressive enhancement.
    - the implicit anchor reference lets CSS `position-area` (anchor-hovercard-*)
      place the card relative to the trigger.

  Relies only on theme tokens + the anchor-hovercard-* utilities in styles.css.
-->

<p>
  Built by
  <a href="/u/productdevbook" interestfor="user-card"
     data-slot="hover-card-trigger"
     class="font-medium text-primary underline-offset-4 hover:underline">@productdevbook</a>.
</p>

<div id="user-card" popover="hint" data-slot="hover-card" data-side="bottom"
  class="z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-hovercard-bottom">
  <div class="flex gap-3">
    <span data-slot="avatar"
      class="relative inline-flex size-10 shrink-0 overflow-hidden rounded-full bg-muted">
      <span class="flex size-full items-center justify-center text-sm font-medium text-muted-foreground">PD</span>
    </span>
    <div class="space-y-1">
      <p class="font-semibold">@productdevbook</p>
      <p class="text-muted-foreground">Building shadcn-htmx. Web standards first.</p>
      <button type="button"
        class="mt-1 inline-flex h-7 items-center rounded-md bg-primary px-2.5 text-xs font-medium text-primary-foreground hover:bg-primary/90">
        Follow
      </button>
    </div>
  </div>
</div>

Examples

User preview — interactive content allowed

Hover or keyboard-focus the username. The card reveals a profile with a real Follow button. Press ESC to dismiss.

This is the primitive Tooltip explicitly defers to: a preview that may contain links and buttons. The trigger carries interestfor and the card is popover="hint", so the browser owns the hover / focus reveal, ESC dismissal, and anchor positioning — no state machine, no JavaScript. In browsers without interest invokers the trigger stays a working link and the card simply doesn't appear.

Built by @productdevbook.

PD

@productdevbook

Building shadcn-htmx. Web standards first.

<HoverCardTrigger cardFor="user" href="/u/productdevbook" class="…">
  @productdevbook
</HoverCardTrigger>

<HoverCard id="user">
  <div class="flex gap-3">
    <Avatar size="lg" fallback="PD" />
    <div>
      <p class="font-semibold">@productdevbook</p>
      <p class="text-muted-foreground">Building shadcn-htmx.</p>
      <Button size="xs">Follow</Button>
    </div>
  </div>
</HoverCard>
{{ hover_card_trigger("@productdevbook", card_for="user", href="/u/productdevbook", class_="…") }}

{% call hover_card_open(id="user") %}
  <div class="flex gap-3">{{ avatar(size="lg", fallback="PD") }}
    <div><p class="font-semibold">@productdevbook</p>{{ button("Follow", size="xs") }}</div>
  </div>
{% endcall %}
{{template "hover_card_trigger" (dict "Label" "@productdevbook" "CardFor" "user" "Href" "/u/productdevbook" "Class" "…")}}
{{template "hover_card" (dict "ID" "user" "Body" (htmlSafe `<div class="flex gap-3">…<button>Follow</button></div>`))}}
<.hover_card_trigger card_for="user" href="/u/productdevbook" class="…">@productdevbook</.hover_card_trigger>
<.hover_card id="user">
  <div class="flex gap-3">
    <.avatar size="lg" fallback="PD" />
    <div><p class="font-semibold">@productdevbook</p><.button size="xs">Follow</.button></div>
  </div>
</.hover_card>
<div class="flex items-center justify-center py-6 text-sm">
  <p>
    Built by
    <a href="#" interestfor="hc-user" data-slot="hover-card-trigger" class="font-medium text-primary underline-offset-4 hover:underline">@productdevbook</a>
    .
  </p>
  <div id="hc-user" popover="hint" data-slot="hover-card" data-side="bottom" class="z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-hovercard-bottom">
    <div class="flex gap-3">
      <span data-slot="avatar" data-size="lg" role="img" aria-label="PD" class="group/avatar relative inline-flex shrink-0 overflow-hidden rounded-full select-none size-10">
        <span data-slot="avatar-fallback" class="flex size-full items-center justify-center rounded-full bg-muted text-sm font-medium text-muted-foreground">PD</span>
      </span>
      <div class="space-y-1">
        <p class="font-semibold">@productdevbook</p>
        <p class="text-muted-foreground">Building shadcn-htmx. Web standards first.</p>
        <button type="button" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 [&amp;_svg:not([class*=&#39;size-&#39;])]:size-4 [&amp;.htmx-request]:pointer-events-none [&amp;.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-6 gap-1 rounded-md px-2 text-xs has-[&gt;svg]:px-1.5 [&amp;_svg:not([class*=&#39;size-&#39;])]:size-3 mt-1" data-slot="button" data-variant="default" data-size="xs">Follow</button>
      </div>
    </div>
  </div>
</div>

Placement — side hint

side drives CSS position-area off the implicit anchor (the trigger). Hover the term to see the card open to its right.

Associating a popover with its interest invoker creates an implicit anchor reference, so the card is positioned purely in CSS via position-area — no JS positioner. Pick the side with the side prop; browsers without anchor support fall back to a centred placement.

The top layer renders above everything else.

Top layer

A browser-managed layer above the rest of the document. Popovers and modal dialogs render here, free of z-index conflicts.

<HoverCardTrigger cardFor="glossary" href="#" class="…">top layer</HoverCardTrigger>

<HoverCard id="glossary" side="right">
  <p class="font-semibold">Top layer</p>
  <p class="text-muted-foreground">A browser-managed layer above the document.</p>
</HoverCard>
{{ hover_card_trigger("top layer", card_for="glossary", class_="…") }}

{% call hover_card_open(id="glossary", side="right") %}
  <p class="font-semibold">Top layer</p>
{% endcall %}
{{template "hover_card_trigger" (dict "Label" "top layer" "CardFor" "glossary" "Class" "…")}}
{{template "hover_card" (dict "ID" "glossary" "Side" "right" "Body" (htmlSafe `<p class="font-semibold">Top layer</p>`))}}
<.hover_card_trigger card_for="glossary" class="…">top layer</.hover_card_trigger>
<.hover_card id="glossary" side="right">
  <p class="font-semibold">Top layer</p>
</.hover_card>
<div class="flex items-center justify-center py-6 text-sm">
  <p>
    The
    <a href="#" interestfor="hc-glossary" data-slot="hover-card-trigger" class="font-medium text-primary underline-offset-4 hover:underline">top layer</a>
    renders above everything else.
  </p>
  <div id="hc-glossary" popover="hint" data-slot="hover-card" data-side="right" class="z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-hovercard-right">
    <p class="font-semibold">Top layer</p>
    <p class="mt-1 text-muted-foreground">
      A browser-managed layer above the rest of the document. Popovers and modal dialogs render here, free of z-index conflicts.
    </p>
  </div>
</div>

Further reading

Lazy-loaded — fetch the preview on first interest

The card fetches its body from the server the first time it is revealed, so the page ships no preview markup up front.

Forward htmx attributes onto the card. With hx-trigger="interest once" the body is fetched from hx-get the first time the user shows interest — the same native interest event the Popover API fires on the target. Repeat hovers reuse the cached markup.

See the release notes.

Loading…

<HoverCardTrigger cardFor="rel" href="#" class="…">release notes</HoverCardTrigger>

<HoverCard id="rel" hx-get="/preview" hx-trigger="interest once" hx-swap="innerHTML">
  <p class="text-muted-foreground">Loading…</p>
</HoverCard>
{{ hover_card_trigger("release notes", card_for="rel", class_="…") }}

{% call hover_card_open(id="rel", attrs={"hx_get": "/preview", "hx_trigger": "interest once", "hx_swap": "innerHTML"}) %}
  <p class="text-muted-foreground">Loading…</p>
{% endcall %}
{{template "hover_card_trigger" (dict "Label" "release notes" "CardFor" "rel" "Class" "…")}}
{{template "hover_card" (dict "ID" "rel" "Body" (htmlSafe `<p hx-get="/preview" hx-trigger="interest once">Loading…</p>`))}}
<.hover_card_trigger card_for="rel" class="…">release notes</.hover_card_trigger>
<.hover_card id="rel" hx-get="/preview" hx-trigger="interest once" hx-swap="innerHTML">
  <p class="text-muted-foreground">Loading</p>
</.hover_card>
<div class="flex items-center justify-center py-6 text-sm">
  <p>
    See the
    <a href="#" interestfor="hc-lazy" data-slot="hover-card-trigger" class="font-medium text-primary underline-offset-4 hover:underline">release notes</a>
    .
  </p>
  <div id="hc-lazy" popover="hint" data-slot="hover-card" data-side="bottom" class="z-50 m-0 w-64 rounded-md border bg-popover p-4 text-sm text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-hovercard-bottom" hx-get="/docs/hover-card/preview" hx-trigger="interest once" hx-swap="innerHTML">
    <p class="text-muted-foreground">Loading…</p>
  </div>
</div>

API Reference

Hover Card

PropTypeDefaultDescription
id*string
Required on <HoverCard>. Referenced by the trigger's cardFor (rendered as interestfor).
cardFor*string
On <HoverCardTrigger>. Id of the HoverCard to reveal; rendered as the interestfor attribute on the trigger.MDNinterestfor
side"top"|"right"|"bottom"|"left""bottom"
Placement relative to the trigger. Drives CSS position-area off the implicit anchor reference, with a centred fallback in browsers without anchor positioning.MDNposition-area
hrefstring"#"
On <HoverCardTrigger>. Click destination for the default <a>. Interest invokers reveal on hover/focus, but the trigger still navigates on click — keeping it useful and functional in non-supporting browsers.
asChildbooleanfalse
On <HoverCardTrigger>. Merge the interest-invoker wiring onto the single child element (e.g. an existing <a> or <button>) instead of rendering a wrapper anchor, so no extra element enters the accessibility tree.
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference

* required