Klarhimmel UI

Hover card

Stable

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-link

When 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

PropTypeDefaultDescription
triggerReactElement-Required: the link or button. It keeps its own name and gets the card as its description while open.
childrenReactNode-Required. Plain content: no buttons or links, they cannot be reached.
open / defaultOpen / onOpenChangeboolean / boolean / (open: boolean) => void-Controlled or uncontrolled.
openDelay / closeDelaynumber500 / 200Milliseconds before it opens and closes.
align'start' | 'center' | 'end''center'Where it sits against the trigger.
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
TabFocusing the link opens the card; leaving closes it
EscapeCloses it
EnterFollows 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