Layout & Containers
Hover Card
A rich preview that opens when the pointer rests on a trigger or the trigger takes focus. Unlike a tooltip it holds real content — a profile, a link preview — and stays open while the pointer crosses from the trigger onto the card.
A rich preview that opens when the pointer rests on a trigger or the trigger takes focus. Unlike a tooltip it holds real content — a profile, a link preview — and stays open while the pointer crosses from the trigger onto the card.
Preview
Section titled “Preview”Rest the pointer on the name, or tab to it.
Kamau Juma
Unit 4B · Tenant since March 2024
<div style="padding-bottom: 15rem">
Rent for Unit 4B is overdue —
<nana-hover-card>
<a href="#tenant">Kamau Juma</a>
<img
slot="media"
src="https://images.pexels.com/photos/6569952/pexels-photo-6569952.jpeg?auto=compress&cs=tinysrgb&w=96&h=96&fit=crop"
alt=""
/>
<span slot="title">Kamau Juma</span>
<span slot="subtitle">Unit 4B · Tenant since March 2024</span>
<div
slot="content"
style="display: flex; flex-direction: column; gap: 0.5rem"
>
<nana-status set="unit" value="arrears"></nana-status>
<span>KES 42,000 overdue · last paid 5 Aug</span>
</div>
<nana-button slot="actions" size="sm" variant="tertiary">
Message
</nana-button>
<nana-button slot="actions" size="sm" variant="primary">
Record payment
</nana-button>
</nana-hover-card>
has not paid since August.
</div> Installation
Section titled “Installation”npx @nana-tec/ui add hover-cardLands as src/components/ui/hover-card/hover-card.tsx, styled with Tailwind
classes that resolve to the design tokens. Requires Tailwind v4.
npx @nana-tec/ui add hover-cardLands in src/lib/components/ui/hover-card/ as HoverCard.svelte, styled with
Tailwind classes that resolve to the design tokens. Requires Tailwind v4.
npm install @nana-tec/ui-componentsOr copy the source in and own it:
npx @nana-tec/ui add hover-cardimport { HoverCard } from "@/components/ui/hover-card";
export function Example() { return ( <HoverCard placement="bottom" content={ <div> <strong>Ada Lovelace</strong> <p>Wrote the first published algorithm.</p> </div> } > <a href="/people/ada">@ada</a> </HoverCard> );}It takes exactly one child element and clones it to attach the hover, focus and
Escape handling plus aria-describedby — so the trigger stays your element,
not a wrapper.
<script lang="ts"> import { HoverCard } from '$lib/components/ui/hover-card';</script>
<HoverCard placement="bottom"> {#snippet children()} <a href="/people/ada">@ada</a> {/snippet} {#snippet content()} <div> <strong>Ada Lovelace</strong> <p>Wrote the first published algorithm.</p> </div> {/snippet}</HoverCard>The trigger goes in the children snippet and the card body in content. The
first element in children receives aria-describedby while the card is open.
import "@nana-tec/ui-components/hover-card";<nana-hover-card placement="bottom"> <a href="/people/ada">@ada</a> <img slot="media" src="/avatars/ada.jpg" alt="" /> <span slot="title">Ada Lovelace</span> <span slot="subtitle">Mathematician</span> <p slot="content">Wrote the first published algorithm.</p> <nana-button slot="actions" size="sm" variant="tertiary">Message</nana-button> <nana-button slot="actions" size="sm" variant="primary">Follow</nana-button></nana-hover-card>The trigger is the default slot; everything else is a named part.
Both stay in your page’s DOM, so the trigger’s aria-describedby can point at
the content — a reference into a shadow root would not resolve. show() and
hide() open and close it from code, and nana-show and nana-hide say when
it did.
placement takes a side and an optional alignment — top, bottom-start,
right-end and so on. When the side has no room the card flips to the other,
and it shifts along the trigger to stay on screen.
Classes you pass are merged with tailwind-merge, so an override wins rather
than fighting. In React, classNames targets the root and card parts
individually. See Using Tailwind.
The card
Section titled “The card”| Slot | Holds | Set in |
|---|---|---|
media | An avatar or a photograph, cropped to a 44px circle — set your own border-radius for a square | Beside the title |
title | The heading | Semibold, at the heading-4 size |
subtitle | A line under the heading | Label size, medium weight, muted |
content | The body — a sentence, a status, a list of facts | Body size and weight |
actions | Buttons, on a quiet footer | Right-aligned; actions-align="start" moves them left |
Every part is optional and a part left empty takes no space, so a card can be a single sentence or a full profile. Nothing on it is set lighter than the system’s medium body weight.
Use size="sm" buttons, with one primary at most and the rest tertiary;
the solid secondary amber is louder than a card this size wants. Keep the
buttons to what the reader would do next from this preview — anything they
need in order to act must also be reachable without hovering.
Timing
Section titled “Timing”The card opens after openDelay (700ms by default) and closes after
closeDelay (300ms by default) — open-delay and close-delay on the Web
Component. The close delay is the grace period that lets
the pointer leave the trigger and reach the card without it disappearing; once
the pointer is on the card, the card stays open until the pointer leaves both.
The longer open delay is deliberate: a hover card carries more than a tooltip, so it should not appear for a pointer that is only passing over.
The React and Svelte props, read from the components’ own types:
Trigger
| Prop | Type |
|---|---|
onMouseEnter | (event: MouseEvent<HTMLElement>) => void |
onMouseLeave | (event: MouseEvent<HTMLElement>) => void |
onFocus | (event: FocusEvent<HTMLElement>) => void |
onBlur | (event: FocusEvent<HTMLElement>) => void |
HoverCard
| Prop | Type | Default |
|---|---|---|
contentrequired | ReactNode | |
placement | HoverCardPlacement | 'bottom' |
disabled | boolean | false |
openDelay | number | |
closeDelay | number | |
childrenrequired | ReactElement<Record<string, unknown>> |
Also accepts Styled<HoverCardPart>.
HoverCard
| Prop | Type | Default |
|---|---|---|
contentrequired | Snippet | |
placement | HoverCardPlacement | 'bottom' |
disabled | boolean | false |
openDelay | number | |
closeDelay | number | |
unstyled | boolean | false |
children | Snippet |
The attribute names, events and CSS hooks below are the Web Components reference.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
actionsAlign | actions-align | 'start' | 'end' | 'end' | Which end of the footer the buttons sit at |
closeDelay | close-delay | — | DEFAULT_CLOSE_DELAY | How long the card waits for the pointer to come back before closing, in ms |
disabled | disabled | boolean | false | Never open |
open | open | boolean | false | Whether the card is showing |
openDelay | open-delay | — | DEFAULT_OPEN_DELAY | How long the pointer must rest before the card opens, in ms |
placement | placement | HoverCardPlacement | 'bottom' | Which side of the trigger the card opens on; it flips when there is no room |
Events
| Event | Description |
|---|---|
nana-show | The card opened |
nana-hide | The card closed |
Slots
| Slot | Description |
|---|---|
(default) | The trigger |
media | An avatar or photograph beside the title |
title | The card's heading |
subtitle | A line under the heading |
content | The body |
actions | Buttons along the foot of the card |
CSS parts
| Part | Description |
|---|---|
::part(base) | The wrapper around the trigger |
::part(card) | The floating card |
::part(header) | The media, title and subtitle |
::part(media) | The media area |
::part(title) | The heading |
::part(subtitle) | The line under the heading |
::part(body) | The body |
::part(footer) | The actions row |
CSS custom properties
| Property | Description |
|---|---|
--nana-hover-card-width | Widest the card grows |
--nana-hover-card-padding | Space inside each part of the card |
Keyboard
Section titled “Keyboard”| Action | Does |
|---|---|
| Focus the trigger | Shows after openDelay |
Tab into the card | Keeps it open (Web Components) |
| Blur | Hides after closeDelay |
Escape | Hides at once, however the card was opened |
Accessibility
Section titled “Accessibility”The card is linked to the trigger through aria-describedby while it is open —
the title, subtitle and body on the Web Component — so a screen reader
announces it as a description of the trigger. Buttons on the card are reached
by moving focus on from the trigger; the card stays open while focus is inside
it. Keep anything essential reachable without the card:
it is a preview, and touch users never hover. The Web Component ignores touch
outright, so a tap goes straight to whatever the trigger does.