- Components
Hover card
A preview of where a link goes, shown on hover and on keyboard focus, never the only copy.
npx shadcn add @klarhimmel/hover-card @klarhimmel/text-linkWhen to use
Use for
A short preview of where a link goes: a room, a person, a rate.
Not for
Anything only available in the card. Touch screens never open it. Actions. Use Popover.
Pairs with
Text link, Avatar.
Preview
Guests love Room 311 for a quiet weekend.
Hover the link, or Tab to it: the preview opens on focus too. Escape closes it.
hover-card.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { HoverCard } from '@/components/klarhimmel/ui/hover-card';
import { TextLink } from '@/components/klarhimmel/ui/text-link';
export default function Example() {
return (
<Theme theme="brand">
<HoverCard trigger={<TextLink href="#room-311">Room 311</TextLink>}>
A double room with a balcony. From 1 490 kr a night.
</HoverCard>
</Theme>
);
}Install
Import
import { HoverCard } from '@/components/klarhimmel/ui/hover-card';- Lands in
- components/klarhimmel/ui/hover-card.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
- Render in
- Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.
- Limit
- Overlays render into the Theme root with position: fixed. A transform on an ancestor re-parents fixed (a dialog centres in the wrapper) and overflow: hidden clips menus. Render <Theme> at the app root, outside any transformed wrapper.
Examples
Person
Assigned to Maja Sund.
import { Avatar } from '@/components/klarhimmel/ui/avatar'; import { HoverCard } from '@/components/klarhimmel/ui/hover-card'; import { TextLink } from '@/components/klarhimmel/ui/text-link'; export default function HoverCardPerson() { return ( <div className="flex min-h-64 w-[min(24rem,calc(100cqw-3rem))] flex-col gap-3"> <p className="text-kh-body"> Assigned to{' '} <HoverCard align="start" trigger={<TextLink href="#maja">Maja Sund</TextLink>}> <div className="flex gap-3"> <Avatar name="Maja Sund" /> <div> <p className="font-medium">Maja Sund</p> <p className="text-kh-muted">Front desk manager</p> <p className="mt-2 text-kh-caption text-kh-muted">The same details are on her profile page, which the link opens.</p> </div> </div> </HoverCard> . </p> </div> ); }Controlled
See the autumn rates.
The preview is closed. While it is open the link carries it as its description.
import { useState } from 'react'; import { HoverCard } from '@/components/klarhimmel/ui/hover-card'; import { TextLink } from '@/components/klarhimmel/ui/text-link'; export default function HoverCardControlled() { const [open, setOpen] = useState(false); return ( <div className="flex min-h-64 w-[min(24rem,calc(100cqw-3rem))] flex-col gap-3"> <p className="text-kh-body"> See the{' '} <HoverCard open={open} onOpenChange={setOpen} openDelay={150} trigger={<TextLink href="#rates">autumn rates</TextLink>}> Weekend 1 490 kr, weekday 1 190 kr, breakfast included. </HoverCard> . </p> <p role="status" className="text-kh-small text-kh-muted">The preview is {open ? 'open' : 'closed'}. While it is open the link carries it as its description.</p> </div> ); }
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactElement | - | Required: the link or button. It keeps its own name and gets the card as its description while open. |
children | ReactNode | - | Required. Plain content: no buttons or links, they cannot be reached. |
open / defaultOpen / onOpenChange | boolean / boolean / (open: boolean) => void | - | Controlled or uncontrolled. |
openDelay / closeDelay | number | 500 / 200 | Milliseconds before it opens and closes. |
align | 'start' | 'center' | 'end' | 'center' | Where it sits against the trigger. |
className | string | - | Extra classes, merged after the component's own. |
Accessibility
| Key | Does |
|---|---|
Tab | Focusing the link opens the card; leaving closes it |
Escape | Closes it |
Enter | Follows the link, as always |
While open the card is the trigger's aria-describedby. It holds plain content: nothing in it can be reached by keyboard.
Accessibility