Overlays · LinkPreview

LinkPreview

Anchored content preview on hover or keyboard focus — Radix HoverCard equivalent. Trigger is an <a>; the floating panel reveals after openDelay (default 700ms) with a SafePolygon bridging trigger and content so the pointer can travel without dismissing. Non-modal, no-op on touch (navigates the link directly).

parts{compiled.parts.order.length} events{events.length} sizes5 apg tooltip ↗

Read about {triggerLabel} {#if showArrow}{/if}

{previewTitle}

{previewBody}

in the W3C reference.

trace {#if trace.length === 0} hover the link to see events {:else} {#each trace.slice(0, 3) as entry (entry.at)} {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''} {fmtTime(entry.at)} {/each} {/if} open {String(open)}
{#if tab === 'live'}

Controls

Soma owns the hover-bridge polygon, open/close timing and dismissal. Eidos adds size on Content (xs..xl). Trigger is rendered as <a>; on touch the preview is a no-op and the link follows.

soma Provider props · open + delays + disabled
soma Content props · floating placement
eidos Content props · visual size · padding / min-width / font-size
Composition · toggle structural parts
Demo content · literal text
soma headless · hover-bridge + floating positioning + dismissal svelte
{somaSnippet}
eidos visual · adds size on Content svelte
{eidosSnippet}
{/if} {#if tab === 'api'}

API reference

Three parts: Provider (root), Trigger (anchor), Content (panel), plus optional Arrow inside Content. Portal wraps Content for body-level rendering.

Provider
PropTypeDefaultDescription
open bindablebooleanfalseWhether the preview is open.
onOpenChange / onOpenChangeComplete(open: boolean) => void—Fire on every transition / after settle.
openDelaynumber700Hover-open delay (ms). Matches Radix HoverCard default.
closeDelaynumber300Hover-close delay (ms). Corridor for pointer to traverse to content.
disabledbooleanfalseHover/focus do not open the preview.
Trigger
PropTypeDefaultDescription
idstringauto—
…native anchor attrsPrimitiveAnchorAttributes—Renders an <a>; all HTML anchor attrs (href, target, rel) flow through.
child{`Snippet<[{ props }]>`}—Opt out of the default <a> and render your own element (e.g. button-triggered hover-card).
Content
PropTypeDefaultDescription
size eidosResponsiveProp<'xs' | 'sm' | 'md' | 'lg' | 'xl'>'md'Visual size — padding, min-width, font-size.
side'top' | 'right' | 'bottom' | 'left''top'Side of the trigger.
align'start' | 'center' | 'end''center'Alignment along the side.
sideOffsetnumber8Distance from trigger.
alignOffsetnumber0Offset along the alignment axis.
avoidCollisionsbooleantrueFlip / shift to avoid viewport edges.
collisionBoundaryBoundary | Boundary[][]Custom boundary element(s).
collisionPaddingnumber | Record<Side, number>0Padding from boundaries.
sticky'partial' | 'always''partial'Sticking behavior on scroll.
hideWhenDetachedbooleantrueHide when trigger leaves the viewport.
customAnchorstring | HTMLElement | Measurable | null—Anchor to a different element/rect than the trigger.
forceMountbooleanfalseKeep in DOM when closed (for external animation).
{/if} {#if tab === 'morfo'}

morfo · declarative contract

Source: src/uix/morfo/components/link-preview.ts. Trigger emits the hover/focus events; Content stamps data-state and data-side via the Floating layer.

FieldValue
name"{linkPreviewMorfo.name}"
kebab"{linkPreviewMorfo.kebab}"
scope[{linkPreviewMorfo.scope.map((s) => `"${s}"`).join(', ')}]
parts.length{linkPreviewMorfo.parts.length}
events.length{events.length}
Parts
{#each partsList as part (part.kebab)} {/each}
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab} [{part.marker}] <{part.defaultElement}> {part.role ?? '—'} {part.archetype ?? '—'} {part.states.length ? part.states.join(' | ') : '—'} {part.optional ? 'yes' : 'no'}
Events declaration
{#each events as action (action.name)} {@const sem = action.semantic} {@const intentDecl = 'intent' in sem ? sem.intent : undefined} {@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'} {/each}
namefamilyverbsequenceintenttargetprewritecommit
{action.name} {sem.family} {sem.verb ?? '—'} {sem.sequence ?? 'pre'} {intentStr} {action.target} {action.prewrite.length ? action.prewrite.map((p) => `${p.attr}=${p.value}`).join(', ') : '—'} {action.commit ? `${action.commit.attr}=${action.commit.value}` : '—'}
{/if} {#if tab === 'sema'}

sema · events + perceptual signature

Hover / focus opens the preview; pointer-leave or escape closes it. Click play to fire on the live preview.

{#each events as action (action.name)} {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined} {@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined} {/each}
NameFamilyVerbSequenceIntentPlay
{action.name} {action.semantic.family} {action.semantic.verb ?? '—'} {action.semantic.sequence ?? 'pre'} {effectiveIntent ?? '—'}
{/if} {#if tab === 'recipe'}

Eidos recipe

Selectors at src/uix/eidos/components/link-preview/link-preview.css. Trigger paints the <a> directly (underline + hover/open color shift) — it does NOT render as <Button> like other triggers, because link previews are navigational anchors rather than action controls.

SelectorSourceWhat it paints
[data-link-preview-trigger]morfoAnchor: underline + accent color.
[data-link-preview-trigger][data-state='open']morfoStronger color when preview is open.
[data-link-preview-content]morfoFloating panel: border, bg, shadow, padding.
[data-link-preview-content][data-size]eidosSize variant (xs..xl) — padding / min-width / font-size.
[data-link-preview-content][data-state][data-side]morfoOpen/close transition per side (slide+fade).
[data-link-preview-arrow]morfoArrow fill matches the panel bg.
{/if} {#if tab === 'a11y'}

Accessibility

LinkPreview is decorative for assistive tech — the content panel is sighted-user enhancement, not a primary information channel. Screen readers should rely on the link's accessible name (and surrounding context) to convey the destination. The preview opens on keyboard focus too so sighted-keyboard users get the affordance.

Keyboard
KeyAction
TabFocus the trigger — preview opens after a short delay (skips on initial mount).
EscapeClose the preview, focus stays on trigger.
EnterFollow the link's href (native anchor behavior).
Pointer
GestureBehavior
Hover triggerAfter openDelay (700ms default) the preview opens.
Leave trigger toward contentSafePolygon bridges the gap; preview stays open while pointer is in the triangle.
Leave bothAfter closeDelay (300ms default) the preview closes.
TouchNo preview — the link is followed directly. Avoids modal hover-trap on mobile.
ARIA contract
PartAttributeValue
triggerdata-state"open" | "closed"
contentrole(none — sighted enhancement)
contentdata-state"open" | "closed"
contentdata-side"top" | "right" | "bottom" | "left"
{/if}