+
+ 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}
+
+
+ sizes 5
+
+
+ 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)}
+
+
+
+
+
+
+ (tab = 'live')}>Live
+ (tab = 'api')}>
+ API 3
+
+ (tab = 'morfo')}>
+ morfo
+ {partsList.length}p · {events.length}e
+
+ (tab = 'sema')}>
+ sema
+ {events.length}
+
+ (tab = 'recipe')}>Recipe
+ (tab = 'a11y')}>A11y
+
+
+ {#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
+
+
+
+ open bindable
+
+
+ {open ? 'open' : 'closed'}
+
+
+
+ openDelay {openDelay}ms
+
+
+
+ closeDelay {closeDelay}ms
+
+
+
+ disabled preview never opens
+
+
+ {disabled ? 'on' : 'off'}
+
+
+
+
+
+
+ soma Content props · floating placement
+
+
+
+ side
+
+ {#each ['top', 'right', 'bottom', 'left'] as s (s)}
+ (side = s as Side)}>{s}
+ {/each}
+
+
+
+ align
+
+ {#each ['start', 'center', 'end'] as a (a)}
+ (align = a as Align)}>{a}
+ {/each}
+
+
+
+ sideOffset {sideOffset}px
+
+
+
+ alignOffset {alignOffset}px
+
+
+
+ avoidCollisions
+
+
+ {avoidCollisions ? 'on' : 'off'}
+
+
+
+
+
+
+ eidos Content props · visual size
+
+ · padding / min-width / font-size
+
+
+
+
+ size
+
+ {#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
+ (size = s as LinkPreviewSize)}>{s}
+ {/each}
+
+
+
+
+
+ Composition · toggle structural parts
+
+
+ render Arrow part
+
+
+ {showArrow ? 'on' : 'off'}
+
+
+
+
+
+ Demo content · literal text
+
+
+ trigger label
+
+
+
+ href
+
+
+
+ preview title
+
+
+
+ preview body
+
+
+
+
+
+
+
+ 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
+
+
+ Prop Type Default Description
+
+ open bindable boolean false Whether the preview is open.
+ onOpenChange / onOpenChangeComplete (open: boolean) => void — Fire on every transition / after settle.
+ openDelay number 700 Hover-open delay (ms). Matches Radix HoverCard default.
+ closeDelay number 300 Hover-close delay (ms). Corridor for pointer to traverse to content.
+ disabled boolean false Hover/focus do not open the preview.
+
+
+
+
+ Trigger
+
+
+ Prop Type Default Description
+
+ id string auto —
+ …native anchor attrs PrimitiveAnchorAttributes — 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
+
+
+ Prop Type Default Description
+
+ size eidos ResponsiveProp<'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.
+ sideOffset number 8 Distance from trigger.
+ alignOffset number 0 Offset along the alignment axis.
+ avoidCollisions boolean true Flip / shift to avoid viewport edges.
+ collisionBoundary Boundary | Boundary[] [] Custom boundary element(s).
+ collisionPadding number | Record<Side, number> 0 Padding from boundaries.
+ sticky 'partial' | 'always' 'partial' Sticking behavior on scroll.
+ hideWhenDetached boolean true Hide when trigger leaves the viewport.
+ customAnchor string | HTMLElement | Measurable | null — Anchor to a different element/rect than the trigger.
+ forceMount boolean false Keep 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.
+
+
+
+
+ Field Value
+
+ name "{linkPreviewMorfo.name}"
+ kebab "{linkPreviewMorfo.kebab}"
+ scope [{linkPreviewMorfo.scope.map((s) => `"${s}"`).join(', ')}]
+ parts.length {linkPreviewMorfo.parts.length}
+ events.length {events.length}
+
+
+
+
+ Parts
+
+
+ Part Marker Element Role Archetype States Optional
+
+ {#each partsList as part (part.kebab)}
+
+ {part.kebab}
+ [{part.marker}]
+ <{part.defaultElement}>
+ {part.role ?? '—'}
+ {part.archetype ?? '—'}
+ {part.states.length ? part.states.join(' | ') : '—'}
+ {part.optional ? 'yes' : 'no'}
+
+ {/each}
+
+
+
+
+ Events declaration
+
+
+ name family verb sequence intent target prewrite commit
+
+ {#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 ?? '?'}` : '—'}
+
+ {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}` : '—'}
+
+ {/each}
+
+
+
+
+ {/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.
+
+
+
+
+ Name Family Verb Sequence Intent Play
+
+ {#each events as action (action.name)}
+ {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
+ {@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined}
+
+ {action.name}
+ {action.semantic.family}
+ {action.semantic.verb ?? '—'}
+ {action.semantic.sequence ?? 'pre'}
+ {effectiveIntent ?? '—'}
+
+ {
+ const target = (stageRef?.querySelector('[data-link-preview-content]') ?? stageRef?.querySelector('[data-link-preview-trigger]') ?? stageRef) as HTMLElement | null;
+ if (!target) return;
+ void uix.events?.emit({
+ name: action.name,
+ family: action.semantic.family,
+ target,
+ ...(effectiveIntent ? { intent: effectiveIntent } : {})
+ });
+ }}
+ >▶ play
+
+
+ {/each}
+
+
+
+
+ {/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.
+
+
+
+ Selector Source What it paints
+
+ [data-link-preview-trigger] morfo Anchor: underline + accent color.
+ [data-link-preview-trigger][data-state='open'] morfo Stronger color when preview is open.
+ [data-link-preview-content] morfo Floating panel: border, bg, shadow, padding.
+ [data-link-preview-content][data-size] eidos Size variant (xs..xl) — padding / min-width / font-size.
+ [data-link-preview-content][data-state][data-side] morfo Open/close transition per side (slide+fade).
+ [data-link-preview-arrow] morfo Arrow 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
+
+
+ Key Action
+
+ Tab Focus the trigger — preview opens after a short delay (skips on initial mount).
+ Escape Close the preview, focus stays on trigger.
+ Enter Follow the link's href (native anchor behavior).
+
+
+
+
+ Pointer
+
+
+ Gesture Behavior
+
+ Hover trigger After openDelay (700ms default) the preview opens.
+ Leave trigger toward content SafePolygon bridges the gap; preview stays open while pointer is in the triangle.
+ Leave both After closeDelay (300ms default) the preview closes.
+ Touch No preview — the link is followed directly. Avoids modal hover-trap on mobile.
+
+
+
+
+ ARIA contract
+
+
+ Part Attribute Value
+
+ trigger data-state "open" | "closed"
+ content role (none — sighted enhancement)
+ content data-state "open" | "closed"
+ content data-side "top" | "right" | "bottom" | "left"
+
+
+
+
+ {/if}
+