Hover Card

A popup card that appears when the user hovers over a trigger element, displaying additional information or actions without leaving the current context.

Docs
import { HoverCard, HoverCardTrigger, HoverCardContent } from "~/components/hover-card";

export default function HoverCardBasicDemo() {
  return (
    <HoverCard>
      <HoverCardTrigger>Hover me</HoverCardTrigger>
      <HoverCardContent useArrow>
        <div>Content displayed on hover</div>
      </HoverCardContent>
    </HoverCard>
  );
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add hover-card

Manual

Create the recipe file at src/components/recipes/hover-card.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const hoverCardVariants = tv({
  slots: {
    root: "inline-flex",
    trigger:
      "inline-flex items-center justify-center gap-2 rounded-md border border-input bg-transparent px-4 py-2 text-sm font-medium text-foreground ring-offset-background transition-colors hover:bg-accent max-sm:select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
    positioner: "",
    content:
      "z-[100] w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
    arrow: "[--arrow-size:10px]",
    arrowTip: "h-2 w-2 border-t border-l border-border bg-popover",
  },
});

export type HoverCardVariants = VariantProps<typeof hoverCardVariants>;

Create the component directory and files.

src/components/hover-card/hover-card.base.tsx:

import { HoverCard as ArkHoverCard } from "@ark-ui/solid/hover-card";
import { splitProps, type Component } from "solid-js";
import { Portal } from "solid-js/web";
import { buttonVariants, hoverCardVariants, type ButtonVariants } from "../recipes/hover-card";

const styles = hoverCardVariants();

const Root = ArkHoverCard.Root;
const RootProvider = ArkHoverCard.RootProvider;

const Trigger: Component<ArkHoverCard.TriggerProps & ButtonVariants> = (props) => {
  const [local, others] = splitProps(props, ["class", "variant", "size"]);
  return (
    <ArkHoverCard.Trigger
      class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}
      {...others}
    />
  );
};

const Positioner: Component<ArkHoverCard.PositionerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <Portal>
      <ArkHoverCard.Positioner class={styles.positioner({ class: local.class })} {...others} />
    </Portal>
  );
};

const Content: Component<ArkHoverCard.ContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkHoverCard.Content class={styles.content({ class: local.class })} {...others} />;
};

const Arrow: Component<ArkHoverCard.ArrowProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkHoverCard.Arrow class={styles.arrow({ class: local.class })} {...others} />;
};

const ArrowTip: Component<ArkHoverCard.ArrowTipProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkHoverCard.ArrowTip class={styles.arrowTip({ class: local.class })} {...others} />;
};

export const HoverCard = {
  Root,
  RootProvider,
  Trigger,
  Positioner,
  Content,
  Arrow,
  ArrowTip,
};

src/components/hover-card/index.tsx:

import { splitProps, type Component } from "solid-js";
import { HoverCard as HoverCardBase } from "./hover-card.base";
import { HoverCard as ArkHoverCard } from "@ark-ui/solid/hover-card";

const HoverCard = HoverCardBase.Root;

const HoverCardTrigger = HoverCardBase.Trigger;

type HoverCardContentProps = ArkHoverCard.ContentProps & {
  /** When true, renders an arrow pointing to the trigger element */
  useArrow?: boolean;
};

const HoverCardContent: Component<HoverCardContentProps> = (props) => {
  const [local, others] = splitProps(props, ["useArrow", "children"]);
  return (
    <HoverCardBase.Positioner>
      <HoverCardBase.Content {...others}>
        {local.useArrow && (
          <HoverCardBase.Arrow>
            <HoverCardBase.ArrowTip />
          </HoverCardBase.Arrow>
        )}
        {local.children}
      </HoverCardBase.Content>
    </HoverCardBase.Positioner>
  );
};

export { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardBase };

export { hoverCardVariants, type HoverCardVariants } from "../recipes/hover-card";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Dependencies: This component imports shared recipes or sub-components from other packages. Make sure the following are also installed: button.

Usage

Basic Usage

A basic hover card that displays preview content on hover.

import { HoverCard, HoverCardTrigger, HoverCardContent } from "~/components/hover-card";

export default function HoverCardBasicDemo() {
  return (
    <HoverCard>
      <HoverCardTrigger>Hover me</HoverCardTrigger>
      <HoverCardContent useArrow>
        <div>Content displayed on hover</div>
      </HoverCardContent>
    </HoverCard>
  );
}

Controlled Open State

Use open and onOpenChange to control the hover card visibility externally.

Open: false

import { createSignal } from "solid-js";
import { HoverCard, HoverCardTrigger, HoverCardContent } from "~/components/hover-card";

export default function HoverCardControlledDemo() {
  const [open, setOpen] = createSignal(false);
  return (
    <div class="flex flex-col items-center gap-4">
      <p class="text-sm text-muted-foreground">Open: {open() ? "true" : "false"}</p>
      <HoverCard open={open()} onOpenChange={(e) => setOpen(e.open)}>
        <HoverCardTrigger>Hover me</HoverCardTrigger>
        <HoverCardContent>
          <div class="text-sm">Controlled hover card</div>
        </HoverCardContent>
      </HoverCard>
    </div>
  );
}

Custom Delay

Adjust the hover open and close delays using openDelay and closeDelay.

Custom delay (200ms open, 100ms close)

import { HoverCard, HoverCardTrigger, HoverCardContent } from "~/components/hover-card";

export default function HoverCardDelayDemo() {
  return (
    <div class="flex flex-col items-center gap-4">
      <p class="text-sm text-muted-foreground mb-2">Custom delay (200ms open, 100ms close)</p>
      <HoverCard openDelay={200} closeDelay={100}>
        <HoverCardTrigger>Hover me</HoverCardTrigger>
        <HoverCardContent>
          <div class="text-sm">Custom delay hover card</div>
        </HoverCardContent>
      </HoverCard>
    </div>
  );
}

Custom Positioning

Position the hover card at different sides using the positioning prop.

Positioned to the right

import { HoverCard, HoverCardTrigger, HoverCardContent } from "~/components/hover-card";

export default function HoverCardPositioningDemo() {
  return (
    <div class="flex flex-col items-center gap-4">
      <p class="text-sm text-muted-foreground mb-2">Positioned to the right</p>
      <HoverCard positioning={{ placement: "right" }}>
        <HoverCardTrigger>Hover me</HoverCardTrigger>
        <HoverCardContent>
          <div class="text-sm">Positioned to the right</div>
        </HoverCardContent>
      </HoverCard>
    </div>
  );
}

Advanced Usage

When the composite HoverCard does not provide enough control, import the raw primitive parts via HoverCardBase:

import { HoverCardBase } from "~/components/hover-card";

Or import directly from the base file:

import { HoverCard } from "~/components/hover-card/hover-card.base";

Root Provider

For machine-controlled usage with useHoverCard and HoverCardBase.RootProvider.

RootProvider pattern

import { useHoverCard } from "@ark-ui/solid/hover-card";
import { HoverCardBase } from "~/components/hover-card";

export default function HoverCardRootProviderDemo() {
  const machine = useHoverCard({ openDelay: 200, closeDelay: 100 });
  return (
    <div class="flex flex-col items-center gap-4">
      <p class="text-sm text-muted-foreground mb-2">RootProvider pattern</p>
      <HoverCardBase.RootProvider value={machine}>
        <HoverCardBase.Trigger>Hover me</HoverCardBase.Trigger>
        <HoverCardBase.Positioner>
          <HoverCardBase.Content>
            <div class="text-sm">Content controlled via machine</div>
          </HoverCardBase.Content>
        </HoverCardBase.Positioner>
      </HoverCardBase.RootProvider>
    </div>
  );
}

Custom Arrow

For custom arrow styling or positioning, compose the parts manually using HoverCardBase.

Custom arrow size

import { HoverCard, HoverCardTrigger, HoverCardBase } from "~/components/hover-card";

export default function HoverCardArrowDemo() {
  return (
    <div class="flex flex-col items-center gap-4">
      <p class="text-sm text-muted-foreground mb-2">Custom arrow size</p>
      <HoverCard>
        <HoverCardTrigger>Hover me</HoverCardTrigger>
        <HoverCardBase.Positioner>
          <HoverCardBase.Content>
            <HoverCardBase.Arrow class="[--arrow-size:14px]">
              <HoverCardBase.ArrowTip />
            </HoverCardBase.Arrow>
            <div class="text-sm">Custom arrow size</div>
          </HoverCardBase.Content>
        </HoverCardBase.Positioner>
      </HoverCard>
    </div>
  );
}

API Reference

See the Ark UI Hover Card documentation.