Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

OverlaysReact

Preview Card

A link that shows a preview of its destination on hover or focus, without interrupting keyboard or screen reader navigation.

Rendered by React 19.3.1

Loading React…
import { PreviewCard } from '@hamur/react/preview-card'

export default function PreviewCardExample() {
  return (
    <PreviewCard.Root>
      <p className="preview-text">
        The principles of good{' '}
        <PreviewCard.Trigger className="preview-link" href="#typography">
          typography
        </PreviewCard.Trigger>{' '}
        hold up on screens too.
      </p>
      <PreviewCard.Portal>
        <PreviewCard.Positioner sideOffset={8}>
          <PreviewCard.Popup className="popover preview-card">
            <PreviewCard.Arrow className="arrow" />
            <p className="popover-title">Typography</p>
            <p className="muted">
              The craft of arranging type so that written words are legible and pleasant to read.
            </p>
          </PreviewCard.Popup>
        </PreviewCard.Positioner>
      </PreviewCard.Portal>
    </PreviewCard.Root>
  )
}
481Tests passing in React · 0 failing
40.2 KiBSize
2.00 msOpen
17.4 msClose
16.0 msOpenMutations
11.0 msCloseMutations
0.92 MiBMemory
productdevbook and 19 upstream authors11 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

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

// Not in this example: PreviewCard.Backdrop, PreviewCard.Viewport

Examples

Detached Triggers Simple

A trigger outside Root opens it through a handle.

PreviewCardDetachedTriggersSimple.tsx
import { PreviewCard } from '@hamur/react/preview-card'

const demoPreviewCard = PreviewCard.createHandle()

export default function PreviewCardDetachedTriggersSimpleExample() {
  return (
    <div>
      <p className="preview-text">
        The principles of good{' '}
        <PreviewCard.Trigger
          className="preview-link"
          handle={demoPreviewCard}
          href="https://en.wikipedia.org/wiki/Typography"
        >
          typography
        </PreviewCard.Trigger>{' '}
        remain in the digital age.
      </p>

      <PreviewCard.Root handle={demoPreviewCard}>
        <PreviewCard.Portal>
          <PreviewCard.Positioner sideOffset={8}>
            <PreviewCard.Popup className="pc-popup">
              <PreviewCard.Arrow className="arrow" />
              <div className="pc-content">
                <img
                  width="224"
                  height="150"
                  className="pc-image"
                  src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
                  alt="Station Hofplein signage in Rotterdam, Netherlands"
                />
                <p className="pc-summary">
                  <strong>Typography</strong> is the art and science of arranging type to make
                  written language clear, visually appealing, and effective in communication.
                </p>
              </div>
            </PreviewCard.Popup>
          </PreviewCard.Positioner>
        </PreviewCard.Portal>
      </PreviewCard.Root>
    </div>
  )
}

Detached Triggers Controlled

open with triggerId lets several links share one preview.

PreviewCardDetachedTriggersControlled.tsx
import * as React from 'react'
import { PreviewCard } from '@hamur/react/preview-card'

const demoPreviewCard = PreviewCard.createHandle<React.ReactElement>()

const cardContents = {
  typography: (
    <div className="pc-content">
      <img
        width="224"
        height="150"
        className="pc-image"
        src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
        alt="Station Hofplein signage in Rotterdam, Netherlands"
      />
      <p className="pc-summary">
        <strong>Typography</strong> is the art and science of arranging type.
      </p>
    </div>
  ),
  design: (
    <div className="pc-content">
      <img
        width="241"
        height="240"
        className="pc-image"
        src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Braun_ABW30_%28schwarz%29.jpg/250px-Braun_ABW30_%28schwarz%29.jpg"
        alt="Braun ABW30"
      />
      <p className="pc-summary">
        A <strong>design</strong> is the concept or proposal for an object, process, or system.
      </p>
    </div>
  ),
  art: (
    <div className="pc-content">
      <img
        width="206"
        height="240"
        className="pc-image"
        src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/MonaLisa_sfumato.jpeg/250px-MonaLisa_sfumato.jpeg"
        alt="Mona Lisa"
      />
      <p className="pc-summary">
        <strong>Art</strong> is a diverse range of cultural activity centered around works utilizing
        creative or imaginative talents, which are expected to evoke a worthwhile experience,
        generally through an expression of emotional power, conceptual ideas, technical proficiency,
        or beauty.
      </p>
    </div>
  ),
}

export default function PreviewCardDetachedTriggersControlledExample() {
  const [open, setOpen] = React.useState(false)
  const [triggerId, setTriggerId] = React.useState<string | null>(null)

  const handleOpenChange = (isOpen: boolean, eventDetails: PreviewCard.Root.ChangeEventDetails) => {
    setOpen(isOpen)
    setTriggerId(eventDetails.trigger?.id ?? null)
  }

  return (
    <div>
      <div className="pc-container">
        <p className="preview-text">
          Discover{' '}
          <PreviewCard.Trigger
            className="preview-link"
            handle={demoPreviewCard}
            href="https://en.wikipedia.org/wiki/Typography"
            id="trigger-1"
            payload={cardContents.typography}
          >
            typography
          </PreviewCard.Trigger>
          ,{' '}
          <PreviewCard.Trigger
            className="preview-link"
            handle={demoPreviewCard}
            href="https://en.wikipedia.org/wiki/Industrial_design"
            id="trigger-2"
            payload={cardContents.design}
          >
            design
          </PreviewCard.Trigger>
          , or{' '}
          <PreviewCard.Trigger
            className="preview-link"
            handle={demoPreviewCard}
            href="https://en.wikipedia.org/wiki/Art"
            id="trigger-3"
            payload={cardContents.art}
          >
            art
          </PreviewCard.Trigger>
          .
        </p>
        <button
          type="button"
          className="button"
          onClick={() => {
            setTriggerId('trigger-2')
            setOpen(true)
          }}
        >
          Open programmatically
        </button>
      </div>

      <PreviewCard.Root
        handle={demoPreviewCard}
        open={open}
        onOpenChange={handleOpenChange}
        triggerId={triggerId}
      >
        {({ payload }) => (
          <PreviewCard.Portal>
            <PreviewCard.Positioner sideOffset={8} className="pc-positioner">
              <PreviewCard.Popup className="pc-popup">
                <PreviewCard.Arrow className="arrow" />
                {payload}
              </PreviewCard.Popup>
            </PreviewCard.Positioner>
          </PreviewCard.Portal>
        )}
      </PreviewCard.Root>
    </div>
  )
}

Detached Triggers Full

Viewport animates the content when another link opens the same preview.

PreviewCardDetachedTriggersFull.tsx
import * as React from 'react'
import { PreviewCard } from '@hamur/react/preview-card'

const demoPreviewCard = PreviewCard.createHandle<React.ReactElement>()

const cardContents = {
  typography: (
    <div className="pc-content">
      <img
        width="224"
        height="150"
        className="pc-image"
        src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
        alt="Station Hofplein signage in Rotterdam, Netherlands"
      />
      <p className="pc-summary">
        <strong>Typography</strong> is the art and science of arranging type.
      </p>
    </div>
  ),
  design: (
    <div className="pc-content">
      <img
        width="250"
        height="249"
        className="pc-image"
        src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Braun_ABW30_%28schwarz%29.jpg/250px-Braun_ABW30_%28schwarz%29.jpg"
        alt="Braun ABW30"
      />
      <p className="pc-summary">
        A <strong>design</strong> is the concept or proposal for an object, process, or system.
      </p>
    </div>
  ),
  art: (
    <div className="pc-content">
      <img
        width="250"
        height="290"
        className="pc-image"
        src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/MonaLisa_sfumato.jpeg/250px-MonaLisa_sfumato.jpeg"
        alt="Mona Lisa"
      />
      <p className="pc-summary">
        <strong>Art</strong> is a diverse range of cultural activity centered around works utilizing
        creative or imaginative talents, which are expected to evoke a worthwhile experience,
        generally through an expression of emotional power, conceptual ideas, technical proficiency,
        or beauty.
      </p>
    </div>
  ),
}

export default function PreviewCardDetachedTriggersFullExample() {
  return (
    <div>
      <p className="preview-text">
        Discover{' '}
        <PreviewCard.Trigger
          className="preview-link"
          handle={demoPreviewCard}
          href="https://en.wikipedia.org/wiki/Typography"
          payload={cardContents.typography}
        >
          typography
        </PreviewCard.Trigger>
        ,{' '}
        <PreviewCard.Trigger
          className="preview-link"
          handle={demoPreviewCard}
          href="https://en.wikipedia.org/wiki/Design"
          payload={cardContents.design}
        >
          design
        </PreviewCard.Trigger>
        , or{' '}
        <PreviewCard.Trigger
          className="preview-link"
          handle={demoPreviewCard}
          href="https://en.wikipedia.org/wiki/Art"
          payload={cardContents.art}
        >
          art
        </PreviewCard.Trigger>
        .
      </p>

      <PreviewCard.Root handle={demoPreviewCard}>
        {({ payload }) => (
          <PreviewCard.Portal>
            <PreviewCard.Positioner sideOffset={8} className="pc-positioner animated">
              <PreviewCard.Popup className="pc-popup animated">
                <PreviewCard.Arrow className="arrow" />
                <PreviewCard.Viewport className="pc-viewport">{payload}</PreviewCard.Viewport>
              </PreviewCard.Popup>
            </PreviewCard.Positioner>
          </PreviewCard.Portal>
        )}
      </PreviewCard.Root>
    </div>
  )
}

API reference

28 props across 8 parts, in React syntax. className, style, render are listed once, under Shared props.

ArrowPreviewCard.Arrow

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

Arrow data attributes
Data attributeDescription
data-openPresent when the preview card is open.
data-closedPresent when the preview card is closed.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-uncenteredPresent when the preview card arrow is uncentered.

BackdropPreviewCard.Backdrop

A presentational overlay displayed beneath the popup. Renders a <div>.

Backdrop data attributes
Data attributeDescription
data-openPresent when the preview card is open.
data-closedPresent when the preview card is closed.
data-starting-stylePresent when the preview card begins animating in.
data-ending-stylePresent when the preview card is animating out.

PopupPreviewCard.Popup

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

Popup data attributes
Data attributeDescription
data-openPresent when the preview card is open.
data-closedPresent when the preview card is closed.
data-starting-stylePresent when the preview card begins animating in.
data-ending-stylePresent when the preview card is animating out.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.

PortalPreviewCard.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse

PositionerPreviewCard.Positioner

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

Positioner props
PropTypeDefault
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
alignHow to align the popup relative to the specified side.Align'center'
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
Positioner data attributes
Data attributeDescription
data-openPresent when the preview card is open.
data-closedPresent when the preview card is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
Positioner CSS variables
CSS variableDescription
--available-widthThe available width between the trigger and the edge of the viewport.
--available-heightThe available height between the trigger and the edge of the viewport.
--anchor-widthThe anchor's width.
--anchor-heightThe anchor's height.
--transform-originThe coordinates that this element is anchored to. Used for animations and transitions.
--positioner-widthThe width of the preview card's positioner. It is important to set width to this value when using CSS to animate size changes.
--positioner-heightThe height of the preview card's positioner. It is important to set height to this value when using CSS to animate size changes.

RootPreviewCard.Root

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

Root props
PropTypeDefault
defaultOpenWhether the preview card is initially open.booleanfalse
openWhether the preview card is currently open.boolean—
onOpenChangeEvent handler called when the preview card is opened or closed.((open: boolean, eventDetails: PreviewCardRootChangeEventDetails) => void)—
actionsRefA ref to imperative actions.React.RefObject<PreviewCardRootActions | null>—
defaultTriggerIdID of the trigger that the preview card is associated with.string | null—
handleA handle to associate the preview card with a trigger.PreviewCardHandle<Payload>—
onOpenChangeCompleteEvent handler called after any animations complete when the preview card is opened or closed.((open: boolean) => void)—
triggerIdID of the trigger that the preview card is associated with.string | null—
childrenThe content of the preview card.React.ReactNode | PayloadChildRenderFunction<Payload>—

TriggerPreviewCard.Trigger

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

Trigger props
PropTypeDefault
handleA handle to associate the trigger with a preview card.PreviewCardHandle<Payload>—
payloadA payload to pass to the preview card when it is opened.Payload—
delayHow long to wait before the preview card opens.number600
closeDelayHow long to wait before closing the preview card.number300
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding preview card is open.

ViewportPreviewCard.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>.

Viewport props
PropTypeDefault
childrenThe content to render inside the transition container.React.ReactNode—
Viewport data attributes
Data attributeDescription
data-currentApplied to the direct child of the viewport when no transitions are present or the new content when it's entering.
data-previousApplied to the direct child of the viewport that contains the exiting content when transitions are present.
data-activation-directionIndicates the direction from which the popup was activated. This can be used to create directional animations based on how the popup was triggered. Contains space-separated values for both horizontal and vertical axes.
data-transitioningIndicates that the viewport is currently transitioning between old and new content.
data-instantPresent if animations should be instant.
Viewport CSS variables
CSS variableDescription
--popup-widthThe width of the parent popup. This variable is placed on the 'previous' container and stores the width of the popup when the previous content was rendered. It can be used to freeze the dimensions of the popup when animating between different content.
--popup-heightThe height of the parent popup. This variable is placed on the 'previous' container and stores the height of the popup when the previous content was rendered. It can be used to freeze the dimensions of the popup when animating between different content.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
TabFocusing the link opens the previewTrigger
EscapeCloses the previewPopup

Accessibility

What the parts set
PartSetsSo that
Triggerrenders an <a> and keeps the link's own semanticsthe link is read and followed like any other
Popupnot linked to the trigger and not in the tab orderscreen reader and keyboard users stay on the page and are not taken through the preview
Arrowaria-hidden="true"the decorative arrow is not read
Backdroprole="presentation"the overlay is not read

Focus order

  1. Trigger is a tab stop; focus never moves into Popup.
  2. Moving focus away, or Escape, closes the preview.