Popover
Rich content in a floating panel opened by a button. Unlike Dialog, the page behind stays usable.
import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "@/components/ui/popover";
export default function Demo() { return ( <Popover> <PopoverTrigger render={<Button variant="outline" />}> Set limits </PopoverTrigger> <PopoverContent align="start" className="w-80"> <PopoverHeader> <PopoverTitle>Rebalance limits</PopoverTitle> <PopoverDescription> Applied to the next keeper run. </PopoverDescription> </PopoverHeader> <div className="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input defaultValue="5%" /> <span>Max slippage</span> <Input defaultValue="1%" /> </div> </PopoverContent> </Popover> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "@/components/ui/popover";</script>
<template> <Popover> <PopoverTrigger as-child> <Button variant="outline">Set limits</Button> </PopoverTrigger> <PopoverContent align="start" class="w-80"> <PopoverHeader> <PopoverTitle>Rebalance limits</PopoverTitle> <PopoverDescription>Applied to the next keeper run.</PopoverDescription> </PopoverHeader> <div class="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input default-value="5%" /> <span>Max slippage</span> <Input default-value="1%" /> </div> </PopoverContent> </Popover></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import * as Popover from "$lib/components/ui/popover";</script>
<Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>Set limits</Button> {/snippet} </Popover.Trigger> <Popover.Content align="start" class="w-80"> <Popover.Header> <Popover.Title>Rebalance limits</Popover.Title> <Popover.Description>Applied to the next keeper run.</Popover.Description> </Popover.Header> <div class="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input value="5%" /> <span>Max slippage</span> <Input value="1%" /> </div> </Popover.Content></Popover.Root>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/popoverpnpm dlx shadcn@latest add @edmi-ui/popoveryarn dlx shadcn@latest add @edmi-ui/popoverbunx shadcn@latest add @edmi-ui/popoverPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/popoverpnpm dlx shadcn-vue@latest add @edmi-ui/popoveryarn dlx shadcn-vue@latest add @edmi-ui/popoverbunx shadcn-vue@latest add @edmi-ui/popoverPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/popover.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/popover.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/popover.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/popover.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "@/components/ui/popover";
<Popover> <PopoverTrigger render={<Button variant="outline" />}> Set limits </PopoverTrigger> <PopoverContent align="start" className="w-80"> <PopoverHeader> <PopoverTitle>Rebalance limits</PopoverTitle> <PopoverDescription> Applied to the next keeper run. </PopoverDescription> </PopoverHeader> <div className="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input defaultValue="5%" /> <span>Max slippage</span> <Input defaultValue="1%" /> </div> </PopoverContent></Popover><script setup lang="ts">import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "@/components/ui/popover";</script>
<template> <Popover> <PopoverTrigger as-child> <Button variant="outline">Set limits</Button> </PopoverTrigger> <PopoverContent align="start" class="w-80"> <PopoverHeader> <PopoverTitle>Rebalance limits</PopoverTitle> <PopoverDescription>Applied to the next keeper run.</PopoverDescription> </PopoverHeader> <div class="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input default-value="5%" /> <span>Max slippage</span> <Input default-value="1%" /> </div> </PopoverContent> </Popover></template><script lang="ts"> import * as Popover from "$lib/components/ui/popover"; import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input";</script>
<Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>Set limits</Button> {/snippet} </Popover.Trigger> <Popover.Content align="start" class="w-80"> <Popover.Header> <Popover.Title>Rebalance limits</Popover.Title> <Popover.Description>Applied to the next keeper run.</Popover.Description> </Popover.Header> <div class="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input value="5%" /> <span>Max slippage</span> <Input value="1%" /> </div> </Popover.Content></Popover.Root>Component source
Section titled “Component source”"use client";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";import { cn } from "cn";import type * as React from "react";
import { type Elevation, SurfaceProvider, useElevation,} from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise.const overlayElevation = { sunken: "", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating",};
function Popover({ ...props }: PopoverPrimitive.Root.Props) { return <PopoverPrimitive.Root data-slot="popover" {...props} />;}
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) { return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;}
function PopoverContent({ className, align = "center", alignOffset = 0, side = "bottom", sideOffset = 6, elevation, children, ...props}: PopoverPrimitive.Popup.Props & Pick< PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" > & { /** ✦ depth: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */ elevation?: Elevation; }) { const level = useElevation(elevation, "overlay"); return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Positioner align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} className="isolate z-50" > <PopoverPrimitive.Popup data-slot="popover-content" className={cn( // Flat (recipes.surface.popover); `elevation` adds the bevel / drop. "z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 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", overlayElevation[level], className, )} {...props} > <SurfaceProvider level={level}>{children}</SurfaceProvider> </PopoverPrimitive.Popup> </PopoverPrimitive.Positioner> </PopoverPrimitive.Portal> );}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="popover-header" className={cn("flex flex-col gap-0.5 text-sm", className)} {...props} /> );}
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) { return ( <PopoverPrimitive.Title data-slot="popover-title" className={cn("font-medium tracking-[-0.1px]", className)} {...props} /> );}
function PopoverDescription({ className, ...props}: PopoverPrimitive.Description.Props) { return ( <PopoverPrimitive.Description data-slot="popover-description" className={cn("text-[13px] text-muted-foreground", className)} {...props} /> );}
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,};<script setup lang="ts">import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui'import { PopoverRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<PopoverRootProps>()const emits = defineEmits<PopoverRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)</script>
<template> <PopoverRoot v-slot="slotProps" data-slot="popover" v-bind="forwarded" > <slot v-bind="slotProps" /> </PopoverRoot></template><script setup lang="ts">import type { PopoverAnchorProps } from 'reka-ui'import { PopoverAnchor } from 'reka-ui'
const props = defineProps<PopoverAnchorProps>()</script>
<template> <PopoverAnchor data-slot="popover-anchor" v-bind="props" > <slot /> </PopoverAnchor></template><script setup lang="ts">import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { PopoverContent, PopoverPortal, useForwardPropsEmits,} from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, provideSurface, useElevation } from '@/registry/edmi/ui/elevation'
defineOptions({ inheritAttrs: false,})
const props = withDefaults( defineProps<PopoverContentProps & { class?: HTMLAttributes['class'], elevation?: Elevation }>(), { elevation: undefined, align: 'center', sideOffset: 6, },)const emits = defineEmits<PopoverContentEmits>()
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise.const overlayElevation = { sunken: '', flat: '', raised: 'border-transparent shadow-raised', floating: 'border-transparent shadow-floating',}const level = useElevation(() => props.elevation, 'overlay')provideSurface(() => level.value)
const delegatedProps = reactiveOmit(props, 'class', 'elevation')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <PopoverPortal> <PopoverContent data-slot="popover-content" v-bind="{ ...$attrs, ...forwarded }" :class=" cn( // Flat by default (recipes.surface.popover); `elevation` ✦ adds the bevel / drop. 'z-50 flex w-72 max-w-(--reka-popover-content-available-width) origin-(--reka-popover-content-transform-origin) flex-col gap-2.5 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', overlayElevation[level], props.class, ) " > <slot /> </PopoverContent> </PopoverPortal></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="popover-description" :class="cn('text-[13px] text-muted-foreground', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="popover-header" :class="cn('flex flex-col gap-0.5 text-sm', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="popover-title" :class="cn('font-medium tracking-[-0.1px]', props.class)" > <slot /> </div></template><script setup lang="ts">import type { PopoverTriggerProps } from 'reka-ui'import { PopoverTrigger } from 'reka-ui'
const props = defineProps<PopoverTriggerProps>()</script>
<template> <PopoverTrigger data-slot="popover-trigger" v-bind="props" > <slot /> </PopoverTrigger></template>export { default as Popover } from "./Popover.vue";export { default as PopoverAnchor } from "./PopoverAnchor.vue";export { default as PopoverContent } from "./PopoverContent.vue";export { default as PopoverDescription } from "./PopoverDescription.vue";export { default as PopoverHeader } from "./PopoverHeader.vue";export { default as PopoverTitle } from "./PopoverTitle.vue";export { default as PopoverTrigger } from "./PopoverTrigger.vue";<script lang="ts"> import { Popover as PopoverPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();</script>
<PopoverPrimitive.Root bind:open {...restProps} /><script lang="ts"> import { Popover as PopoverPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();</script>
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} /><script lang="ts"> import { Popover as PopoverPrimitive } from "bits-ui"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { type Elevation, setSurface, useElevation } from "$lib/registry/ui/elevation/index.js"; import PopoverPortal from "./popover-portal.svelte"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, sideOffset = 6, align = "center", portalProps, elevation = "auto", ...restProps }: PopoverPrimitive.ContentProps & { /** ✦ depth: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */ elevation?: Elevation; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>; } = $props();
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise. const overlayElevation = { sunken: "", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating", }; const level = useElevation(() => elevation, "overlay"); setSurface(() => level.current);</script>
<PopoverPortal {...portalProps}> <PopoverPrimitive.Content bind:ref data-slot="popover-content" {sideOffset} {align} class={cn( // recipes.surface.popover (flat); elevation ✦ adds the bevel / drop. "z-50 flex w-72 origin-(--bits-popover-content-transform-origin) flex-col gap-2.5 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", overlayElevation[level.current], className )} {...restProps} /></PopoverPortal><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="popover-description" class={cn("text-[13px] text-muted-foreground", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="popover-header" class={cn("flex flex-col gap-0.5 text-sm", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Popover as PopoverPrimitive } from "bits-ui";
let { ...restProps }: PopoverPrimitive.PortalProps = $props();</script>
<PopoverPrimitive.Portal {...restProps} /><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="popover-title" class={cn("font-medium tracking-[-0.1px]", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Popover as PopoverPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: PopoverPrimitive.TriggerProps = $props();</script>
<PopoverPrimitive.Trigger bind:ref data-slot="popover-trigger" class={cn(className)} {...restProps}/>import Root from "./popover.svelte";import Close from "./popover-close.svelte";import Content from "./popover-content.svelte";import Description from "./popover-description.svelte";import Header from "./popover-header.svelte";import Portal from "./popover-portal.svelte";import Title from "./popover-title.svelte";import Trigger from "./popover-trigger.svelte";
export { Close, Close as PopoverClose, Content, Content as PopoverContent, Description, Description as PopoverDescription, Header, Header as PopoverHeader, Portal, Portal as PopoverPortal, Root, // Root as Popover, Title, Title as PopoverTitle, Trigger, Trigger as PopoverTrigger,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. On PopoverContent, raised (+1) adds the bevel and floating (+2) the bevel plus one soft drop (the natural level in layered mode).
import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "@/components/ui/popover";
export default function Demo() { return ( <Popover> <PopoverTrigger render={<Button variant="outline" />}> Set limits </PopoverTrigger> <PopoverContent elevation="floating" align="start" className="w-80"> <PopoverHeader> <PopoverTitle>Rebalance limits</PopoverTitle> <PopoverDescription> Applied to the next keeper run. </PopoverDescription> </PopoverHeader> <div className="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input defaultValue="5%" /> <span>Max slippage</span> <Input defaultValue="1%" /> </div> </PopoverContent> </Popover> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "@/components/ui/popover";</script>
<template> <Popover> <PopoverTrigger as-child> <Button variant="outline">Set limits</Button> </PopoverTrigger> <PopoverContent elevation="floating" align="start" class="w-80"> <PopoverHeader> <PopoverTitle>Rebalance limits</PopoverTitle> <PopoverDescription>Applied to the next keeper run.</PopoverDescription> </PopoverHeader> <div class="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input default-value="5%" /> <span>Max slippage</span> <Input default-value="1%" /> </div> </PopoverContent> </Popover></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import * as Popover from "$lib/components/ui/popover";</script>
<Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>Set limits</Button> {/snippet} </Popover.Trigger> <Popover.Content elevation="floating" align="start" class="w-80"> <Popover.Header> <Popover.Title>Rebalance limits</Popover.Title> <Popover.Description>Applied to the next keeper run.</Popover.Description> </Popover.Header> <div class="grid grid-cols-[1fr_120px] items-center gap-2 text-[13px]"> <span>Drift</span> <Input value="5%" /> <span>Max slippage</span> <Input value="1%" /> </div> </Popover.Content></Popover.Root>| Prop | Type | Default |
|---|---|---|
PopoverContent side |
top | right | bottom | left |
bottom |
PopoverContent align |
start | center | end |
center |
PopoverContent sideOffset |
number |
6 |
PopoverTrigger render |
ReactElement |
none (Base UI, replaces asChild) |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
Parts: Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription.
✦ Edmi additions
Section titled “✦ Edmi additions”- Flat by default;
elevationadds the bevel / drop. Solid--popoversurface with a 1px border.