ComponentsOverlays

Hover preview card

A record's summary when you rest on its name, so you can check who someone is without leaving Flo's answer or the page you're on.

Playground

Change the controls to try each prop. The preview holds the card open; on a page it opens 600ms after the pointer rests on the name, or at once on keyboard focus. The code under the preview lists only the props you changed.

import { RecordPreview } from '@/ui/hover-card';<RecordPreview name="Maya Chen" subtitle="Candidate · Data Engineer at Northwind" openLabel="Open candidate" />

Person and company

Rest on a name, or Tab to it. The card shows the 32px mark, the name at 16/20/600, the record type and one identifying line, then up to three facts and one link to open the record. A click on the name opens the record too.

import { Avatar } from '@/ui/avatar';
import { HoverCard, HoverCardContent, HoverCardTrigger, RecordPreview } from '@/ui/hover-card';
import { Lozenge } from '@/ui/lozenge';
import { RecordChip } from '@/ui/record-chip';

// Rest on a name, or Tab to it: the card opens after 600ms. Moving into the card keeps it open.
export default function HoverCardDefault() {
  return (
    <div className="flex items-center gap-300">
      <HoverCard>
        <HoverCardTrigger render={<RecordChip type="candidate" name="Maya Chen" href="#maya-chen" />} />
        <HoverCardContent>
          <RecordPreview
            type="candidate"
            name="Maya Chen"
            subtitle="Candidate · Data Engineer at Northwind"
            facts={[
              { label: 'Stage', value: <Lozenge>Interview</Lozenge> },
              { label: 'Job', value: 'Senior Data Engineer' },
              { label: 'Last contact', value: 'Met 13 Oct' },
            ]}
            href="#maya-chen"
            openLabel="Open candidate"
          />
        </HoverCardContent>
      </HoverCard>
      <HoverCard>
        <HoverCardTrigger render={<RecordChip type="company" name="Halcyon Bank" href="#halcyon-bank" />} />
        <HoverCardContent>
          <RecordPreview
            type="company"
            name="Halcyon Bank"
            subtitle="Client · halcyonbank.com"
            facts={[
              { label: 'Open jobs', value: '3' },
              {
                label: 'Owner',
                value: (
                  <span className="flex items-center gap-075">
                    <Avatar name="Joseph Ariel" size="small" />
                    Joseph A.
                  </span>
                ),
              },
              { label: 'Last contact', value: 'Emailed 13 Oct' },
            ]}
            href="#halcyon-bank"
            openLabel="Open company"
          />
        </HoverCardContent>
      </HoverCard>
    </div>
  );
}

When things go wrong

The header shows at once from what the name already knows; the facts fill in under it, never as a spinner card. When the facts can't load, one line says so and the link still opens the record. A long name or line cuts off with an ellipsis, and a missing value shows "—".

import { useState } from 'react';
import { HoverCard, HoverCardContent, HoverCardTrigger, RecordPreview } from '@/ui/hover-card';
import { RecordChip } from '@/ui/record-chip';

// The header shows at once from what the chip knows; the facts load, never a spinner card. One card fails and says so,
// one has a very long name and empty facts.
export default function HoverCardUnhappy() {
  const [loaded, setLoaded] = useState(false);
  return (
    <div className="flex flex-wrap items-center gap-300">
      <HoverCard onOpenChange={(open) => open && setTimeout(() => setLoaded(true), 1500)}>
        <HoverCardTrigger render={<RecordChip type="candidate" name="王小明" href="#wang" />} />
        <HoverCardContent>
          <RecordPreview
            type="candidate"
            name="王小明"
            subtitle="Candidate · Platform Engineer"
            loading={!loaded}
            facts={[{ label: 'Stage', value: 'Short list' }, { label: 'Job', value: 'Platform Engineer' }, { label: 'Last contact' }]}
            href="#wang"
            openLabel="Open candidate"
          />
        </HoverCardContent>
      </HoverCard>
      <HoverCard>
        <HoverCardTrigger render={<RecordChip type="contact" name="Zoë O'Brien-Smith" href="#zoe" />} />
        <HoverCardContent>
          <RecordPreview
            type="contact"
            name="Zoë O'Brien-Smith"
            subtitle="Contact · Northbridge Talent"
            error="Couldn’t load the details. Open the contact to see them."
            href="#zoe"
            openLabel="Open contact"
          />
        </HoverCardContent>
      </HoverCard>
      <HoverCard>
        <HoverCardTrigger render={<RecordChip type="candidate" name="Nguyễn Thị Minh Khai-Montgomery" href="#nguyen" />} />
        <HoverCardContent>
          <RecordPreview
            type="candidate"
            name="Nguyễn Thị Minh Khai-Montgomery O’Brien-Smith"
            subtitle="Candidate · Senior Data Engineer at Halcyon Bank Asia Pacific"
            facts={[{ label: 'Stage' }, { label: 'Job' }, { label: 'Last contact' }]}
            href="#nguyen"
            openLabel="Open candidate"
          />
        </HoverCardContent>
      </HoverCard>
    </div>
  );
}

Motion

The card uses the Popup motion. Press Replay to close and open it again.

import { useState } from 'react';
import { Button } from '@/ui/button';
import { HoverCard, HoverCardContent, HoverCardTrigger, RecordPreview } from '@/ui/hover-card';
import { RecordChip } from '@/ui/record-chip';

// Replay closes the card, then opens it again, so the 120ms exit and the 160ms entry run back to back.
export default function HoverCardMotion() {
  const [open, setOpen] = useState(false);
  const replay = () => {
    setOpen(false);
    setTimeout(() => setOpen(true), 400);
  };
  return (
    <div className="flex items-center gap-300">
      <HoverCard open={open} onOpenChange={setOpen}>
        <HoverCardTrigger render={<RecordChip type="company" name="Orbital Freight" href="#orbital" />} />
        <HoverCardContent>
          <RecordPreview type="company" name="Orbital Freight" subtitle="Client · orbitalfreight.com" facts={[{ label: 'Open jobs', value: '1' }]} href="#orbital" openLabel="Open company" />
        </HoverCardContent>
      </HoverCard>
      <Button variant="ghost" onClick={replay}>
        Replay
      </Button>
    </div>
  );
}

On this page