Skip to content
edmi-ui

Tooltip

A short label on hover or focus. Only for extra info; never put required content or actions in a tooltip.

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

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/tooltip
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/tooltip
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/tooltip
Terminal window
bunx shadcn-vue@latest add @edmi-ui/tooltip

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/tooltip.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tooltip.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tooltip.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tooltip.json

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

import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
<TooltipProvider>
<div className="flex gap-2">
{(["top", "right", "bottom", "left"] as const).map((side) => (
<Tooltip key={side}>
<TooltipTrigger render={<Button variant="outline" size="sm" />}>
{side}
</TooltipTrigger>
<TooltipContent side={side}>Copy address</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
</script>
<template>
<TooltipProvider>
<div class="flex gap-2">
<Tooltip :key="side">
<TooltipTrigger as-child>
<Button variant="outline" size="sm">{{ side }}</Button>
</TooltipTrigger>
<TooltipContent :side="side">Copy address</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</template>
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip";
import { Button } from "$lib/components/ui/button";
</script>
<Tooltip.Provider>
<div class="flex gap-2">
{#each sides as side (side)}
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button variant="outline" size="sm" {...props}>{side}</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content {side}>Copy address</Tooltip.Content>
</Tooltip.Root>
{/each}
</div>
</Tooltip.Provider>
registry/ui/tooltip.tsx
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { cn } from "cn";
function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
);
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
side = "top",
sideOffset = 6,
align = "center",
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
// recipes.tooltip: solid primary, 7px radius.
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-[7px] bg-primary px-2.5 py-1.5 text-[12.5px] text-primary-foreground transition-[opacity,scale] duration-100 has-data-[slot=kbd]:pr-1.5 data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0 **:data-[slot=kbd]:rounded-sm",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2 rotate-45 rounded-[1px] bg-primary data-[side=bottom]:-top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-1" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
registry/ui/tooltip/Tooltip.vue
<script setup lang="ts">
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<TooltipRootProps>()
const emits = defineEmits<TooltipRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<TooltipRoot
v-slot="slotProps"
data-slot="tooltip"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</TooltipRoot>
</template>
registry/ui/tooltip/TooltipContent.vue
<script setup lang="ts">
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
side: 'top',
sideOffset: 6,
})
const emits = defineEmits<TooltipContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TooltipPortal>
<TooltipContent
data-slot="tooltip-content"
v-bind="{ ...forwarded, ...$attrs }"
:class="cn(
// recipes.tooltip: solid primary, 7px radius.
'z-50 inline-flex w-fit max-w-xs origin-(--reka-tooltip-content-transform-origin) items-center gap-1.5 rounded-[7px] bg-primary px-2.5 py-1.5 text-[12.5px] text-balance text-primary-foreground has-data-[slot=kbd]:pr-1.5 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-[state=instant-open]:animate-in data-[state=instant-open]:fade-in-0 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 **:data-[slot=kbd]:rounded-sm',
props.class,
)"
>
<slot />
<TooltipArrow class="z-50 size-2 translate-y-[calc(-50%-1px)] rotate-45 rounded-[1px] bg-primary fill-primary" />
</TooltipContent>
</TooltipPortal>
</template>
registry/ui/tooltip/TooltipProvider.vue
<script setup lang="ts">
import type { TooltipProviderProps } from 'reka-ui'
import { TooltipProvider } from 'reka-ui'
const props = withDefaults(defineProps<TooltipProviderProps>(), {
delayDuration: 0,
})
</script>
<template>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
</template>
registry/ui/tooltip/TooltipTrigger.vue
<script setup lang="ts">
import type { TooltipTriggerProps } from 'reka-ui'
import { TooltipTrigger } from 'reka-ui'
const props = defineProps<TooltipTriggerProps>()
</script>
<template>
<TooltipTrigger
data-slot="tooltip-trigger"
v-bind="props"
>
<slot />
</TooltipTrigger>
</template>
registry/ui/tooltip/index.ts
export { default as Tooltip } from "./Tooltip.vue";
export { default as TooltipContent } from "./TooltipContent.vue";
export { default as TooltipProvider } from "./TooltipProvider.vue";
export { default as TooltipTrigger } from "./TooltipTrigger.vue";
src/lib/registry/ui/tooltip/tooltip-content.svelte
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
import TooltipPortal from "./tooltip-portal.svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
sideOffset = 6,
side = "top",
children,
arrowClasses,
portalProps,
...restProps
}: TooltipPrimitive.ContentProps & {
arrowClasses?: string;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
} = $props();
</script>
<TooltipPortal {...portalProps}>
<TooltipPrimitive.Content
bind:ref
data-slot="tooltip-content"
{sideOffset}
{side}
class={cn(
// recipes.tooltip: solid primary, 7px radius.
"z-50 inline-flex w-fit max-w-xs origin-(--bits-tooltip-content-transform-origin) items-center gap-1.5 rounded-[7px] bg-primary px-2.5 py-1.5 text-[12.5px] text-balance text-primary-foreground has-data-[slot=kbd]:pr-1.5 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-[state=instant-open]:animate-in data-[state=instant-open]:fade-in-0 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 **:data-[slot=kbd]:rounded-sm",
className
)}
{...restProps}
>
{@render children?.()}
<TooltipPrimitive.Arrow>
{#snippet child({ props })}
<div
class={cn(
"z-50 size-2 translate-y-[calc(-50%-2px)] rotate-45 rounded-[1px] bg-primary fill-primary data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%+1px)] data-[side=left]:-translate-y-[calc(50%-3px)] data-[side=right]:translate-x-[calc(50%+2px)] data-[side=right]:translate-y-1/2 data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%+2px)]",
arrowClasses
)}
{...props}
></div>
{/snippet}
</TooltipPrimitive.Arrow>
</TooltipPrimitive.Content>
</TooltipPortal>
src/lib/registry/ui/tooltip/tooltip-portal.svelte
<script lang="ts">
import { Tooltip as Primitive } from "bits-ui";
let { ...restProps }: Primitive.PortalProps = $props();
</script>
<Primitive.Portal {...restProps} />
src/lib/registry/ui/tooltip/tooltip-provider.svelte
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { delayDuration = 0, ...restProps }: TooltipPrimitive.ProviderProps = $props();
</script>
<TooltipPrimitive.Provider {delayDuration} {...restProps} />
src/lib/registry/ui/tooltip/tooltip-trigger.svelte
<script lang="ts" generics="T = never">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps<T> = $props();
</script>
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
src/lib/registry/ui/tooltip/tooltip.svelte
<script lang="ts" generics="T = never">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: TooltipPrimitive.RootProps<T> = $props();
</script>
<TooltipPrimitive.Root bind:open {...restProps} />
src/lib/registry/ui/tooltip/index.ts
import Root from "./tooltip.svelte";
import Content from "./tooltip-content.svelte";
import Portal from "./tooltip-portal.svelte";
import Provider from "./tooltip-provider.svelte";
import Trigger from "./tooltip-trigger.svelte";
export {
Content,
Content as TooltipContent,
Portal,
Portal as TooltipPortal,
Provider,
Provider as TooltipProvider,
Root,
//
Root as Tooltip,
Trigger,
Trigger as TooltipTrigger,
};
Prop Type Default
TooltipContent side top | right | bottom | left top
TooltipContent sideOffset number 6
TooltipProvider delay number (ms) 0

Parts: TooltipProvider, Tooltip, TooltipTrigger, TooltipContent. A disabled trigger must be wrapped in a span.

  • Solid --primary chip (7px radius) with a small arrow; a Kbd inside gets tighter right padding.