Skip to content
edmi-ui

Hover Card

A preview of what is behind a link, for sighted mouse users. Opens on hover after a short delay.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
@dewi
Terminal window
npx shadcn@latest add @edmi-ui/hover-card
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/hover-card
Terminal window
yarn dlx shadcn@latest add @edmi-ui/hover-card
Terminal window
bunx shadcn@latest add @edmi-ui/hover-card

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/hover-card
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/hover-card
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/hover-card
Terminal window
bunx shadcn-vue@latest add @edmi-ui/hover-card

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/hover-card.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/hover-card.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/hover-card.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/hover-card.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
<HoverCard>
<HoverCardTrigger
render={
<a href="#dewi" className="text-sm underline underline-offset-4" />
}
>
@dewi
</HoverCardTrigger>
<HoverCardContent className="w-72">
<div className="flex flex-col gap-1">
<p className="font-medium">Dewi Lestari</p>
<p className="text-[13px] text-muted-foreground">
Builds AI and megacap baskets. 3 indexes, 412 holders.
</p>
<p className="text-xs text-muted-foreground">Joined Sep 2026</p>
</div>
</HoverCardContent>
</HoverCard>
<script setup lang="ts">
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
</script>
<template>
<HoverCard>
<HoverCardTrigger as-child>
<a href="#dewi" class="text-sm underline underline-offset-4">@dewi</a>
</HoverCardTrigger>
<HoverCardContent class="w-72">
<div class="flex flex-col gap-1">
<p class="font-medium">Dewi Lestari</p>
<p class="text-[13px] text-muted-foreground">
Builds AI and megacap baskets. 3 indexes, 412 holders.
</p>
<p class="text-xs text-muted-foreground">Joined Sep 2026</p>
</div>
</HoverCardContent>
</HoverCard>
</template>
<script lang="ts">
import * as HoverCard from "$lib/components/ui/hover-card";
</script>
<HoverCard.Root>
<HoverCard.Trigger href="#dewi" class="text-sm underline underline-offset-4">@dewi</HoverCard.Trigger>
<HoverCard.Content class="w-72">
<div class="flex flex-col gap-1">
<p class="font-medium">Dewi Lestari</p>
<p class="text-[13px] text-muted-foreground">
Builds AI and megacap baskets. 3 indexes, 412 holders.
</p>
<p class="text-xs text-muted-foreground">Joined Sep 2026</p>
</div>
</HoverCard.Content>
</HoverCard.Root>
registry/ui/hover-card.tsx
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
import { cn } from "cn";
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
return (
<PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
side = "bottom",
sideOffset = 6,
align = "center",
alignOffset = 4,
...props
}: PreviewCardPrimitive.Popup.Props &
Pick<
PreviewCardPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
<PreviewCardPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PreviewCardPrimitive.Popup
data-slot="hover-card-content"
className={cn(
"z-50 w-64 origin-(--transform-origin) rounded-xl border border-border bg-popover p-3 text-sm text-popover-foreground outline-hidden transition-[opacity,scale] duration-100 data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
className,
)}
{...props}
/>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardContent, HoverCardTrigger };
registry/ui/hover-card/HoverCard.vue
<script setup lang="ts">
import type { HoverCardRootEmits, HoverCardRootProps } from 'reka-ui'
import { HoverCardRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<HoverCardRootProps>()
const emits = defineEmits<HoverCardRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<HoverCardRoot
v-slot="slotProps"
data-slot="hover-card"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</HoverCardRoot>
</template>
registry/ui/hover-card/HoverCardContent.vue
<script setup lang="ts">
import type { HoverCardContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import {
HoverCardContent,
HoverCardPortal,
useForwardProps,
} from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: HTMLAttributes['class'] }>(),
{
side: 'bottom',
sideOffset: 6,
align: 'center',
alignOffset: 4,
},
)
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<HoverCardPortal>
<HoverCardContent
data-slot="hover-card-content"
v-bind="{ ...$attrs, ...forwardedProps }"
:class="
cn(
'z-50 w-64 origin-(--reka-hover-card-content-transform-origin) rounded-xl border border-border bg-popover p-3 text-sm text-popover-foreground outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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',
props.class,
)
"
>
<slot />
</HoverCardContent>
</HoverCardPortal>
</template>
registry/ui/hover-card/HoverCardTrigger.vue
<script setup lang="ts">
import type { HoverCardTriggerProps } from 'reka-ui'
import { HoverCardTrigger } from 'reka-ui'
const props = defineProps<HoverCardTriggerProps>()
</script>
<template>
<HoverCardTrigger
data-slot="hover-card-trigger"
v-bind="props"
>
<slot />
</HoverCardTrigger>
</template>
registry/ui/hover-card/index.ts
export { default as HoverCard } from "./HoverCard.vue";
export { default as HoverCardContent } from "./HoverCardContent.vue";
export { default as HoverCardTrigger } from "./HoverCardTrigger.vue";
src/lib/registry/ui/hover-card/hover-card-content.svelte
<script lang="ts">
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import HoverCardPortal from "./hover-card-portal.svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
side = "bottom",
sideOffset = 6,
align = "center",
alignOffset = 4,
portalProps,
...restProps
}: HoverCardPrimitive.ContentProps & {
alignOffset?: number;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof HoverCardPortal>>;
} = $props();
</script>
<HoverCardPortal {...portalProps}>
<HoverCardPrimitive.Content
bind:ref
data-slot="hover-card-content"
{side}
{sideOffset}
{align}
{alignOffset}
class={cn(
"z-50 w-64 origin-(--bits-link-preview-content-transform-origin) rounded-xl border border-border bg-popover p-3 text-sm text-popover-foreground outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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",
className
)}
{...restProps}
/>
</HoverCardPortal>
src/lib/registry/ui/hover-card/hover-card-portal.svelte
<script lang="ts">
import { LinkPreview as Primitive } from "bits-ui";
let { ...restProps }: Primitive.PortalProps = $props();
</script>
<Primitive.Portal {...restProps} />
src/lib/registry/ui/hover-card/hover-card-trigger.svelte
<script lang="ts">
import { LinkPreview as Primitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: Primitive.TriggerProps = $props();
</script>
<Primitive.Trigger bind:ref data-slot="hover-card-trigger" {...restProps} />
src/lib/registry/ui/hover-card/hover-card.svelte
<script lang="ts">
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: HoverCardPrimitive.RootProps = $props();
</script>
<HoverCardPrimitive.Root bind:open {...restProps} />
src/lib/registry/ui/hover-card/index.ts
import Root from "./hover-card.svelte";
import Content from "./hover-card-content.svelte";
import Portal from "./hover-card-portal.svelte";
import Trigger from "./hover-card-trigger.svelte";
export {
Content,
Content as HoverCardContent,
Portal,
Portal as HoverCardPortal,
Root,
Root as HoverCard,
Trigger,
Trigger as HoverCardTrigger,
};
Prop Type Default
HoverCard delay / closeDelay number (ms) Base UI defaults
HoverCardContent side / align positioner props bottom / center

Parts: HoverCard, HoverCardTrigger, HoverCardContent. Built on Base UI PreviewCard.

  • Hard 4px lip, solid popover surface, no blur.