PlHoverCard
A preview of what is behind a link, shown when the pointer rests on it. Long enough to open that it does not fire at every link on the way somewhere else, and slow enough to close that it can be reached.
import { PlHoverCard, PlTextLink } from 'plass-ui';
<PlHoverCard
title="Ada Lovelace"
description="Mathematician, 1815–1852"
trigger={<PlTextLink href="/ada">Ada Lovelace</PlTextLink>}
>
Wrote the first algorithm intended to be carried out by a machine.
</PlHoverCard>;import 'package:plass_ui/plass_ui.dart';
PlHoverCard(
title: const Text('Ada Lovelace'),
description: const Text('Mathematician, 1815–1852'),
trigger: PlTextLink(onPressed: open, child: const Text('Ada Lovelace')),
child: const Text('Wrote the first algorithm intended to be carried out by a machine.'),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger * | ReactElement | — | What the card previews. Usually a link, and it is rendered rather than wrapped |
| title · description | ReactNode | — | The heading and a line under it |
| children | ReactNode | — | The body |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Which edge of the trigger it appears on. Flips to the opposite side when there is no room |
| align | 'start' | 'center' | 'end' | 'center' | Where it sits along that edge |
| sideOffset · alignOffset | number | 8 · 0 | Distance from the trigger and shift along that edge, in pixels |
| delay | number | 600 | How long the pointer has to rest before it opens, in ms. Long, so it does not fire at every link a reader passes |
| closeDelay | number | 300 | How long it waits after the pointer leaves, in ms. It cannot be zero: the gap between the trigger and the card has no pointer in it |
| arrow | boolean | false | Draws the little wedge pointing at the trigger |
| open · defaultOpen · onOpenChange | boolean · boolean · (open) => void | — | Drives it from outside. Without them it is left to the pointer and the keyboard |
| width | number | string | — | A hard cap on the card's width, overriding the one size implies |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Type scale, radius and padding of the sheet |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The family anything inside takes |
| densityshared | 'default' | 'compact' | 'default' | How tightly the sheet packs its content |
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger * | Widget | — | What the card previews. Usually a link, and it is rendered rather than wrapped |
| title · description | Widget? | — | The heading and a line under it |
| child | Widget? | — | The body |
| side | PlassSide | PlassSide.bottom | Which edge of the trigger it appears on. Flips to the opposite side when there is no room |
| align | PlassAlign | PlassAlign.center | Where it sits along that edge |
| offset | double | 8 | Distance from the trigger and shift along that edge, in pixels |
| delay | Duration | 600ms | How long the pointer has to rest before it opens, in ms. Long, so it does not fire at every link a reader passes |
| closeDelay | Duration | 300ms | How long it waits after the pointer leaves, in ms. It cannot be zero: the gap between the trigger and the card has no pointer in it |
| arrow | bool | false | Draws the little wedge pointing at the trigger |
| open · onOpenChanged | bool? · ValueChanged<bool>? | — | Drives it from outside. Without them it is left to the pointer and the keyboard |
| disabled | bool | false | Stops the card opening at all, without disabling the trigger |
| width | double? | — | A hard cap on the card's width, overriding the one size implies |
| sizeshared | PlassSize | PlassSize.md | Type scale, radius and padding of the sheet |
| colorshared | PlassColor | PlassColor.primary | The family anything inside takes |
| densityshared | PlassDensity | PlassDensity.standard | How tightly the sheet packs its content |
What the shared axes mean across the library is in prop conventions.
Hover card, tooltip or popover
Three of them, and what tells them apart is what opens them and what you can do once they are open, not how they look. All three are the same sheet.
| Opened by | Once it is open | |
|---|---|---|
PlTooltip | resting on something | one phrase, and nothing in it can be reached |
PlHoverCard | resting on something | the pointer can move onto it; a title, a picture, a figure |
PlPopover | a press | it stays until dismissed, and can be typed into |
Nothing may live only in here
The rule that decides whether a hover card is the right component at all.
A card that opens on hover does not open for a finger. A link, a button or a fact that exists nowhere else on the page is a link, a button or a fact that every touch reader misses. So everything inside is a preview of something already reachable. The page the trigger goes to, a profile that has its own screen, a figure repeated in the table below.
That is what makes it safe to have at all, and it is also why it needs no dismiss button, no focus trap and no scroll lock. Nothing is lost by never seeing it.
The delays
delay is 600ms and that is deliberately long. A card that opens the moment a pointer crosses a link opens on every link a reader passes on the way somewhere else, which turns a page of prose into a page that flinches.
closeDelay is 300ms and it cannot be zero. The gap between the trigger and the card has no pointer in it, so a card that closed the instant the pointer left the trigger could never be reached, and reaching it is the whole difference from a tooltip.
Shorten delay for a page whose links are all previews (a table of people, a list of issues), and leave it alone everywhere else.
Examples
A person
The ordinary case, and the shape the component was drawn for.
<PlHoverCard
title="Ada Lovelace"
description="Mathematician"
trigger={<PlTextLink href="/people/ada">Ada Lovelace</PlTextLink>}
>
<div className="flex items-center gap-3">
<PlAvatar name="Ada Lovelace" />
<p>Wrote the first algorithm intended for a machine.</p>
</div>
</PlHoverCard>Somewhere other than under the link
side, align and the two offsets are the same four a popover takes, and the card flips to the opposite side when there is no room.
<PlHoverCard side="right" align="start" arrow trigger={…}>…</PlHoverCard>Notes
- The trigger is rendered, not wrapped: a link stays a link, keeps its
href, its styling and its place in the tab order, and the card adds no box to the layout. - The sheet is the same frosted panel a popover draws, one rung wider at every
sizestep. A popover is a detail beside a control, and a preview squeezed to the width of a hint is a preview nobody reads. arrowis off by default, as it is on a popover: the sheet is translucent over a blurred backdrop, and a wedge sticking out past its own box cannot carry that backdrop with it.
- Base UI's
PreviewCardowns the anchoring, the flip at the window edge, the two delays and the dismissal.
- The pointer is tracked on the trigger and on the card, as two flags rather than one, which is what lets it cross the gap between them.
- Needs an
Overlayabove it, whichWidgetsAppwith a navigator andMaterialAppboth provide.
Accessibility
- It opens on keyboard focus as well as on hover, so a reader tabbing along a paragraph of links gets the same preview a pointer would. That is the half a hover-only card loses, and it is free here.
- Escape closes it.
- It is not a dialog and does not take the focus. What is inside can be reached with the pointer; what is inside must also be reachable without it, per the rule above.
- The trigger keeps whatever role it already had. A link that opens a card is still a link, and it still goes where it says it goes.