ShardsUI is in beta. APIs may change before 1.0.

Preview Card

A link preview opened on hover.

The rings of Saturn are made almost entirely of ice.

<script lang="ts">
  import { PreviewCard } from '@shardsui/svelte/preview-card'
</script>

<PreviewCard.Root>
  <p class="m-0 text-sm/6 text-balance text-gray-900">
    The rings of
    <PreviewCard.Trigger
      class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
      href="https://en.wikipedia.org/wiki/Saturn"
    >
      Saturn
    </PreviewCard.Trigger>
    are made almost entirely of ice.
  </p>

  <PreviewCard.Portal>
    <PreviewCard.Positioner sideOffset={8}>
      <PreviewCard.Popup
        class="box-border h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg bg-gray-50 shadow-lg outline-1 outline-gray-200 transition-[scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
      >
        <PreviewCard.Arrow
          class="flex data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
        >
          {@render arrow()}
        </PreviewCard.Arrow>
        <div class="box-border flex w-56 flex-col items-center gap-2 p-2">
          <div
            class="size-36 rounded-full"
            style="background: radial-gradient(circle at 32% 28%, #f8efcd 0%, #e3ce85 45%, #a8934a 100%)"
            aria-hidden="true"
          ></div>
          <p class="m-0 self-stretch text-sm/5 text-pretty text-gray-900">
            <strong>Saturn</strong> is the second largest planet, a pale gas giant so light it would float,
            wrapped in bright rings of ice.
          </p>
        </div>
      </PreviewCard.Popup>
    </PreviewCard.Positioner>
  </PreviewCard.Portal>
</PreviewCard.Root>

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
  </svg>
{/snippet}

Anatomy

<script>
  import { PreviewCard } from '@shardsui/svelte/preview-card'
</script>

<PreviewCard.Root>
  <PreviewCard.Trigger />
  <PreviewCard.Portal>
    <PreviewCard.Backdrop />
    <PreviewCard.Positioner>
      <PreviewCard.Popup>
        <PreviewCard.Arrow />
        <PreviewCard.Viewport />
      </PreviewCard.Popup>
    </PreviewCard.Positioner>
  </PreviewCard.Portal>
</PreviewCard.Root>

Usage guidelines

  • Popup content should reflect the link destination: avoid placing unique or essential information in the popup unless it is also available on the linked page. Preview cards only help pointer and keyboard users; they are not accessible to touch or screen reader users.

Examples

Detached triggers

The trigger normally nests inside <PreviewCard.Root>, next to the content it reveals; that's the layout in the hero demo above.

Sometimes the link and its card can't sit together in the markup, e.g. the link runs inline in a paragraph while the card is defined elsewhere. Render <PreviewCard.Trigger> wherever the link belongs and tie it back to the root with a shared handle from new PreviewCard.Handle().

The handle's imperative methods, open() and close(), need a <PreviewCard.Root> using the same handle to be mounted. Calls made before a root mounts or after it unmounts are ignored, not queued — and each root starts from fresh state when it mounts, so no open state carries over from a previous one.

<script>
  const demoPreviewCard = new PreviewCard.Handle()
</script>

<PreviewCard.Trigger handle={demoPreviewCard} href="#">Link</PreviewCard.Trigger>

<PreviewCard.Root handle={demoPreviewCard}>...</PreviewCard.Root>

The rings of Saturn are made almost entirely of ice.

<script lang="ts">
  import { PreviewCard } from '@shardsui/svelte/preview-card'

  const demoPreviewCard = new PreviewCard.Handle()
</script>

<p class="m-0 text-sm/6 text-balance text-gray-900">
  The rings of
  <PreviewCard.Trigger
    class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
    handle={demoPreviewCard}
    href="https://en.wikipedia.org/wiki/Saturn"
  >
    Saturn
  </PreviewCard.Trigger>
  are made almost entirely of ice.
</p>

<PreviewCard.Root handle={demoPreviewCard}>
  <PreviewCard.Portal>
    <PreviewCard.Positioner sideOffset={8}>
      <PreviewCard.Popup
        class="box-border h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg bg-gray-50 shadow-lg outline-1 outline-gray-200 transition-[scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
      >
        <PreviewCard.Arrow
          class="flex data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
        >
          {@render arrow()}
        </PreviewCard.Arrow>
        <div class="box-border flex w-56 flex-col items-center gap-2 p-2">
          <div
            class="size-36 rounded-full"
            style="background: radial-gradient(circle at 32% 28%, #f8efcd 0%, #e3ce85 45%, #a8934a 100%)"
            aria-hidden="true"
          ></div>
          <p class="m-0 self-stretch text-sm/5 text-pretty text-gray-900">
            <strong>Saturn</strong> is the second largest planet, a pale gas giant so light it would float,
            wrapped in bright rings of ice.
          </p>
        </div>
      </PreviewCard.Popup>
    </PreviewCard.Positioner>
  </PreviewCard.Portal>
</PreviewCard.Root>

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
  </svg>
{/snippet}

Multiple triggers

One preview card can serve many links: nest several <PreviewCard.Trigger> elements in a single <PreviewCard.Root>, or point any number of detached triggers at the same handle.

<PreviewCard.Root>
  <PreviewCard.Trigger href="#">Trigger 1</PreviewCard.Trigger>
  <PreviewCard.Trigger href="#">Trigger 2</PreviewCard.Trigger>
  ...
</PreviewCard.Root>
<script>
  const demoPreviewCard = new PreviewCard.Handle()
</script>

<PreviewCard.Trigger handle={demoPreviewCard} href="#">Trigger 1</PreviewCard.Trigger>

<PreviewCard.Trigger handle={demoPreviewCard} href="#">Trigger 2</PreviewCard.Trigger>

<PreviewCard.Root handle={demoPreviewCard}>...</PreviewCard.Root>

Each trigger can feed the card its own data through the payload prop, so one card shows a different preview per link. Read it from the children snippet on <PreviewCard.Root>. Pass a type argument to new PreviewCard.Handle() to type the payload:

<script>
  const demoPreviewCard = new PreviewCard.Handle<{ title: string }>()
</script>

<PreviewCard.Trigger handle={demoPreviewCard} payload={{ title: 'Trigger 1' }} href="#"
  >Trigger 1</PreviewCard.Trigger
>

<PreviewCard.Trigger handle={demoPreviewCard} payload={{ title: 'Trigger 2' }} href="#"
  >Trigger 2</PreviewCard.Trigger
>

<PreviewCard.Root handle={demoPreviewCard}>
  {#snippet children({ payload })}
    <PreviewCard.Portal>
      <PreviewCard.Positioner sideOffset={8}>
        <PreviewCard.Popup>
          {#if payload !== undefined}
            <span>Preview card opened by {payload.title}</span>
          {/if}
        </PreviewCard.Popup>
      </PreviewCard.Positioner>
    </PreviewCard.Portal>
  {/snippet}
</PreviewCard.Root>

Controlled mode with multiple triggers

Own the open state by binding open and handling onOpenChange on <PreviewCard.Root>. With more than one trigger, give each trigger an id and add bind:triggerId to <PreviewCard.Root>: each trigger publishes its own id when it opens the card, and setting triggerId yourself anchors the card to that trigger. Pass triggerId one-way instead if you want to drive it entirely from your own state.

Explore Mars , Saturn , or Neptune .

<script lang="ts">
  import type { Snippet } from 'svelte'
  import { PreviewCard } from '@shardsui/svelte/preview-card'

  const demoPreviewCard = new PreviewCard.Handle<Snippet>()

  let open = $state(false)
  let triggerId = $state<string | null>(null)
</script>

<div class="flex flex-wrap items-baseline justify-center gap-2">
  <p class="m-0 text-sm/6 text-balance text-gray-900">
    Explore
    <PreviewCard.Trigger
      class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
      handle={demoPreviewCard}
      href="https://en.wikipedia.org/wiki/Mars"
      id="trigger-mars"
      payload={mars}
    >
      Mars
    </PreviewCard.Trigger>
    ,
    <PreviewCard.Trigger
      class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
      handle={demoPreviewCard}
      href="https://en.wikipedia.org/wiki/Saturn"
      id="trigger-saturn"
      payload={saturn}
    >
      Saturn
    </PreviewCard.Trigger>
    , or
    <PreviewCard.Trigger
      class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
      handle={demoPreviewCard}
      href="https://en.wikipedia.org/wiki/Neptune"
      id="trigger-neptune"
      payload={neptune}
    >
      Neptune
    </PreviewCard.Trigger>
    .
  </p>
  <button
    type="button"
    class="font-inherit m-0 box-border flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm/6 font-normal text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={() => {
      triggerId = 'trigger-neptune'
      open = true
    }}
  >
    Open programmatically
  </button>
</div>

<PreviewCard.Root handle={demoPreviewCard} bind:open bind:triggerId>
  {#snippet children({ payload })}
    <PreviewCard.Portal>
      <PreviewCard.Positioner
        sideOffset={8}
        class="h-(--positioner-height) w-(--positioner-width) max-w-(--available-width)"
      >
        <PreviewCard.Popup
          class="box-border h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg bg-gray-50 shadow-lg outline-1 outline-gray-200 transition-[scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
        >
          <PreviewCard.Arrow
            class="flex data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
          >
            {@render arrow()}
          </PreviewCard.Arrow>

          {@render payload?.()}
        </PreviewCard.Popup>
      </PreviewCard.Positioner>
    </PreviewCard.Portal>
  {/snippet}
</PreviewCard.Root>

{#snippet planet(name: string, gradient: string, fact: string)}
  <div class="box-border flex w-56 flex-col items-center gap-2 p-2">
    <div class="size-36 rounded-full" style="background: {gradient}" aria-hidden="true"></div>
    <p class="m-0 self-stretch text-sm/5 text-pretty text-gray-900">
      <strong>{name}</strong>
      {fact}
    </p>
  </div>
{/snippet}

{#snippet mars()}
  {@render planet(
    'Mars',
    'radial-gradient(circle at 32% 28%, #e79c7e 0%, #c0472a 45%, #712411 100%)',
    'is a cold desert world of red dust and rock.'
  )}
{/snippet}

{#snippet neptune()}
  {@render planet(
    'Neptune',
    'radial-gradient(circle at 32% 28%, #7fb0ee 0%, #2f5fc4 45%, #14306e 100%)',
    'is the farthest planet, a ball of ice with winds faster than sound.'
  )}
{/snippet}

{#snippet saturn()}
  {@render planet(
    'Saturn',
    'radial-gradient(circle at 32% 28%, #f8efcd 0%, #e3ce85 45%, #a8934a 100%)',
    'is the second largest planet, a pale gas giant so light it would float, wrapped in bright rings of ice.'
  )}
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
  </svg>
{/snippet}

Animating the Preview Card

When a single card hops between triggers, it can slide across rather than pop in and out. Position, size, and contents each animate on their own.

Position and Size

The Positioner carries the card's position — transition its left, right, top, and bottom. The Popup carries its size — transition width and height there.

Content

The contents can cross-fade too when triggers show different previews. Wrap them in <PreviewCard.Viewport>, which detects the trigger change and sets a data-activation-direction attribute marking where the new trigger sits relative to the last — a horizontal and a vertical token separated by a space, e.g. right down; either can be empty. Match a single token with the ~= attribute selector, such as [data-activation-direction~='right'].

While a transition runs, the viewport holds both the old and new contents, each in its own wrapper:

  • data-current: the incoming content, or the only content when nothing is transitioning.
  • data-previous: the outgoing content during a transition.

Style these to author the enter and exit animations.

Explore Mars , Saturn , or Neptune .

<script lang="ts">
  import type { Snippet } from 'svelte'
  import { PreviewCard } from '@shardsui/svelte/preview-card'

  const demoPreviewCard = new PreviewCard.Handle<Snippet>()
</script>

<p class="m-0 text-sm/6 text-balance text-gray-900">
  Explore
  <PreviewCard.Trigger
    class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
    handle={demoPreviewCard}
    href="https://en.wikipedia.org/wiki/Mars"
    payload={mars}
  >
    Mars
  </PreviewCard.Trigger>
  ,
  <PreviewCard.Trigger
    class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
    handle={demoPreviewCard}
    href="https://en.wikipedia.org/wiki/Saturn"
    payload={saturn}
  >
    Saturn
  </PreviewCard.Trigger>
  , or
  <PreviewCard.Trigger
    class="text-gray-950 underline decoration-gray-950/60 decoration-1 underline-offset-2 outline-0 hover:decoration-gray-950 focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-gray-950 data-popup-open:decoration-gray-950"
    handle={demoPreviewCard}
    href="https://en.wikipedia.org/wiki/Neptune"
    payload={neptune}
  >
    Neptune
  </PreviewCard.Trigger>
  .
</p>

<PreviewCard.Root handle={demoPreviewCard}>
  {#snippet children({ payload })}
    <PreviewCard.Portal>
      <PreviewCard.Positioner
        sideOffset={8}
        class="h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] duration-350 ease-out-quint"
      >
        <PreviewCard.Popup
          class="relative box-border h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg bg-gray-50 shadow-lg outline-1 outline-gray-200 transition-[width,height,opacity,transform] duration-350 ease-out-quint data-ending-style:transform-[scale(0.98)] data-ending-style:opacity-0 data-starting-style:transform-[scale(0.98)] data-starting-style:opacity-0"
        >
          <PreviewCard.Arrow
            class="flex data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
          >
            {@render arrow()}
          </PreviewCard.Arrow>

          <PreviewCard.Viewport
            class="relative size-full overflow-clip **:data-current:w-(--popup-width) **:data-current:translate-x-0 **:data-current:opacity-100 **:data-current:transition-[translate,opacity] **:data-current:duration-[350ms,175ms] **:data-current:ease-out-quint **:data-previous:w-(--popup-width) **:data-previous:translate-x-0 **:data-previous:opacity-100 **:data-previous:transition-[translate,opacity] **:data-previous:duration-[350ms,175ms] **:data-previous:ease-out-quint data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-1/3 data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-1/3 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-1/3 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-1/3 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:opacity-0"
          >
            {@render payload?.()}
          </PreviewCard.Viewport>
        </PreviewCard.Popup>
      </PreviewCard.Positioner>
    </PreviewCard.Portal>
  {/snippet}
</PreviewCard.Root>

{#snippet planet(name: string, gradient: string, fact: string)}
  <div class="box-border flex w-56 flex-col items-center gap-2 p-2">
    <div class="size-36 rounded-full" style="background: {gradient}" aria-hidden="true"></div>
    <p class="m-0 self-stretch text-sm/5 text-pretty text-gray-900">
      <strong>{name}</strong>
      {fact}
    </p>
  </div>
{/snippet}

{#snippet mars()}
  {@render planet(
    'Mars',
    'radial-gradient(circle at 32% 28%, #e79c7e 0%, #c0472a 45%, #712411 100%)',
    'is a cold desert world of red dust and rock.'
  )}
{/snippet}

{#snippet neptune()}
  {@render planet(
    'Neptune',
    'radial-gradient(circle at 32% 28%, #7fb0ee 0%, #2f5fc4 45%, #14306e 100%)',
    'is the farthest planet, a ball of ice with winds faster than sound.'
  )}
{/snippet}

{#snippet saturn()}
  {@render planet(
    'Saturn',
    'radial-gradient(circle at 32% 28%, #f8efcd 0%, #e3ce85 45%, #a8934a 100%)',
    'is the second largest planet, a pale gas giant so light it would float, wrapped in bright rings of ice.'
  )}
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
  </svg>
{/snippet}

API reference

Root

Groups all parts of the preview card. Doesn't render its own HTML element.

PropTypeDefault

Trigger

A link that opens the preview card. Renders an <a> element.

PropTypeDefault
AttributeDescription
data-popup-openPresent when the corresponding preview card is open.

Backdrop

An overlay displayed beneath the popup. It never receives pointer events, so hovering the page through it still works. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the preview card is open.
data-closedPresent when the preview card is closed.
data-starting-stylePresent when the backdrop is animating in.
data-ending-stylePresent when the backdrop is animating out.

Portal

A portal that moves the popup out to <body>, clear of ancestor clipping and stacking. Renders a <div> element.

PropTypeDefault

Positioner

Positions the popup against the trigger. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-anchor-hiddenPresent when the anchor is hidden.
data-instantPresent when animations should be instant.
CSS VariableDescription
--available-widthAvailable width between the anchor and the viewport edge.
--available-heightAvailable height between the anchor and the viewport edge.
--anchor-widthWidth of the anchor element.
--anchor-heightHeight of the anchor element.
--transform-originTransform origin for scale animations.
--positioner-widthThe positioner element's width. Set while a <PreviewCard.Viewport> morphs content.
--positioner-heightThe positioner element's height. Set while a <PreviewCard.Viewport> morphs content.

Popup

A container for the preview card contents. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the preview card is open.
data-closedPresent when the preview card is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-starting-stylePresent when the preview card is animating in.
data-ending-stylePresent when the preview card is animating out.
data-instantPresent when animations should be instant.
CSS VariableDescription
--popup-widthThe popup element's width. Set while a <PreviewCard.Viewport> morphs content.
--popup-heightThe popup element's height. Set while a <PreviewCard.Viewport> morphs content.

Viewport

A viewport for displaying content transitions. This component is only required if one popup can be opened by multiple triggers, its content changes based on the trigger, and switching between them is animated. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-activation-directionDirection the popup was activated from. A horizontal and a vertical token separated by a space, e.g. right down; either can be empty, so match one with [data-activation-direction~='right'].
data-transitioningPresent while a content transition is in progress.
data-currentApplied to the wrapper of the current content.
data-previousApplied to the wrapper of the outgoing content, present only during a transition.
data-instantPresent when animations should be instant. Value is 'dismiss' or 'focus'.
data-starting-styleApplied to the data-current wrapper while the incoming content is animating in.
data-ending-styleApplied to the data-previous wrapper while the outgoing content is animating out.
CSS VariableDescription
--popup-widthWidth of the outgoing popup content, set on the data-previous container. Use it to freeze the popup size while the content morphs.
--popup-heightHeight of the outgoing popup content, set on the data-previous container.

Reach for the Viewport only when a single popup is opened by multiple triggers, its content differs per trigger, and the switch between them is animated. When you do, set width: var(--positioner-width) and height: var(--positioner-height) on the Positioner so its box is frozen to the measured size during the transition; otherwise content-driven resizing can make the popup thrash or flip to another side.

Arrow

Displays an element positioned against the anchor. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-uncenteredPresent when the arrow cannot be centered.

Handle

Connects a <PreviewCard.Root> with detached <PreviewCard.Trigger> components, and controls the preview card imperatively. Pass a type argument to type the payload.

const previewCard = new PreviewCard.Handle<Payload>()
MemberType