Button Group
Joins related buttons, inputs and text into one control with shared borders.
import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText,} from "@/components/ui/button-group";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <div className="flex flex-wrap items-start gap-6"> <ButtonGroup> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> <Button variant="outline">Snooze</Button> </ButtonGroup> <ButtonGroup> <Button>Join</Button> <ButtonGroupSeparator /> <Button size="icon" aria-label="More"> <IconPlaceholder lucide="ChevronDownIcon" tabler="IconChevronDown" hugeicons="ArrowDown01Icon" phosphor="CaretDownIcon" remixicon="RiArrowDownSLine" /> </Button> </ButtonGroup> <ButtonGroup> <ButtonGroupText>Amount</ButtonGroupText> <ButtonGroupText>1,000 USDC</ButtonGroupText> </ButtonGroup> <ButtonGroup orientation="vertical"> <Button variant="outline" size="icon"> + </Button> <Button variant="outline" size="icon"> - </Button> </ButtonGroup> </div> );}<script setup lang="ts">import { ChevronDown } from "@lucide/vue";import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText,} from "@/components/ui/button-group";</script>
<template> <div class="flex flex-wrap items-start gap-6"> <ButtonGroup> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> <Button variant="outline">Snooze</Button> </ButtonGroup> <ButtonGroup> <Button>Join</Button> <ButtonGroupSeparator /> <Button size="icon" aria-label="More"> <ChevronDown /> </Button> </ButtonGroup> <ButtonGroup> <ButtonGroupText>Amount</ButtonGroupText> <ButtonGroupText>1,000 USDC</ButtonGroupText> </ButtonGroup> <ButtonGroup orientation="vertical"> <Button variant="outline" size="icon">+</Button> <Button variant="outline" size="icon">-</Button> </ButtonGroup> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import * as ButtonGroup from "$lib/components/ui/button-group";</script>
<div class="flex flex-wrap items-start gap-6"> <ButtonGroup.Root> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> <Button variant="outline">Snooze</Button> </ButtonGroup.Root> <ButtonGroup.Root> <Button>Join</Button> <ButtonGroup.Separator /> <Button size="icon" aria-label="More">▾</Button> </ButtonGroup.Root> <ButtonGroup.Root> <ButtonGroup.Text>Amount</ButtonGroup.Text> <ButtonGroup.Text>1,000 USDC</ButtonGroup.Text> </ButtonGroup.Root> <ButtonGroup.Root orientation="vertical"> <Button variant="outline" size="icon">+</Button> <Button variant="outline" size="icon">-</Button> </ButtonGroup.Root></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/button-grouppnpm dlx shadcn@latest add @edmi-ui/button-groupyarn dlx shadcn@latest add @edmi-ui/button-groupbunx shadcn@latest add @edmi-ui/button-groupPick 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/react class-variance-authorityTerminal window pnpm add @base-ui/react class-variance-authorityTerminal window yarn add @base-ui/react class-variance-authorityTerminal window bun add @base-ui/react class-variance-authorityAdd the Edmi components this one builds on:
separator,elevation,button.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/button-grouppnpm dlx shadcn-vue@latest add @edmi-ui/button-groupyarn dlx shadcn-vue@latest add @edmi-ui/button-groupbunx shadcn-vue@latest add @edmi-ui/button-groupPick 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 class-variance-authority @vueuse/coreTerminal window pnpm add reka-ui class-variance-authority @vueuse/coreTerminal window yarn add reka-ui class-variance-authority @vueuse/coreTerminal window bun add reka-ui class-variance-authority @vueuse/coreAdd the Edmi components this one builds on:
separator,elevation,button.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/button-group.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button-group.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button-group.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/button-group.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:
separator,elevation,button.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 { ButtonGroup, ButtonGroupSeparator, ButtonGroupText,} from "@/components/ui/button-group";
<div className="flex flex-wrap items-start gap-6"> <ButtonGroup> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> <Button variant="outline">Snooze</Button> </ButtonGroup> <ButtonGroup> <Button>Join</Button> <ButtonGroupSeparator /> <Button size="icon" aria-label="More"> <CaretDownIcon /> </Button> </ButtonGroup> <ButtonGroup> <ButtonGroupText>Amount</ButtonGroupText> <ButtonGroupText>1,000 USDC</ButtonGroupText> </ButtonGroup> <ButtonGroup orientation="vertical"> <Button variant="outline" size="icon"> + </Button> <Button variant="outline" size="icon"> - </Button> </ButtonGroup></div><script setup lang="ts">import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText,} from "@/components/ui/button-group";</script>
<template> <div class="flex flex-wrap items-start gap-6"> <ButtonGroup> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> <Button variant="outline">Snooze</Button> </ButtonGroup> <ButtonGroup> <Button>Join</Button> <ButtonGroupSeparator /> <Button size="icon" aria-label="More"> <ChevronDown /> </Button> </ButtonGroup> <ButtonGroup> <ButtonGroupText>Amount</ButtonGroupText> <ButtonGroupText>1,000 USDC</ButtonGroupText> </ButtonGroup> <ButtonGroup orientation="vertical"> <Button variant="outline" size="icon">+</Button> <Button variant="outline" size="icon">-</Button> </ButtonGroup> </div></template><script lang="ts"> import * as ButtonGroup from "$lib/components/ui/button-group"; import { Button } from "$lib/components/ui/button";</script>
<div class="flex flex-wrap items-start gap-6"> <ButtonGroup.Root> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> <Button variant="outline">Snooze</Button> </ButtonGroup.Root> <ButtonGroup.Root> <Button>Join</Button> <ButtonGroup.Separator /> <Button size="icon" aria-label="More">▾</Button> </ButtonGroup.Root> <ButtonGroup.Root> <ButtonGroup.Text>Amount</ButtonGroup.Text> <ButtonGroup.Text>1,000 USDC</ButtonGroup.Text> </ButtonGroup.Root> <ButtonGroup.Root orientation="vertical"> <Button variant="outline" size="icon">+</Button> <Button variant="outline" size="icon">-</Button> </ButtonGroup.Root></div>Component source
Section titled “Component source”"use client";
import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "cn";import type * as React from "react";
import { ButtonElevationContext } from "@/registry/edmi/ui/button";import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";import { Separator } from "@/registry/edmi/ui/separator";
const buttonGroupVariants = cva( "flex w-fit items-stretch [&>[data-variant=default]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,var(--primary-foreground)_25%,var(--primary))] [&>[data-variant=brand]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,var(--brand-foreground)_25%,var(--brand))] [&>[data-variant=destructive]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,white_25%,var(--destructive))] *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 [&>[data-slot=input-group]]:shadow-none [&>[data-slot=input]]:shadow-none has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", { variants: { orientation: { horizontal: "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0", vertical: "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0", }, }, defaultVariants: { orientation: "horizontal", }, },);
function ButtonGroup({ className, orientation, elevation, ...props}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants> & { /** ✦ raised: each item is raised; floating: the whole group floats as one plate (items stay raised). */ elevation?: Elevation; }) { const level = useElevation(elevation, "button-filled"); return ( <ButtonElevationContext.Provider value={ elevation && elevation !== "auto" ? level === "floating" ? "raised" : level : undefined } > {/* biome-ignore lint/a11y/useSemanticElements: stock markup; a div group avoids fieldset layout quirks */} <div role="group" data-slot="button-group" data-orientation={orientation} className={cn( buttonGroupVariants({ orientation }), level === "floating" && "rounded-lg shadow-group-float", className, )} {...props} /> </ButtonElevationContext.Provider> );}
function ButtonGroupText({ className, render, ...props}: useRender.ComponentProps<"div">) { return useRender({ defaultTagName: "div", props: mergeProps<"div">( { className: cn( "flex items-center gap-2 rounded-lg border border-input bg-muted px-2.5 text-[13px] font-medium text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", className, ), }, props, ), render, state: { slot: "button-group-text", }, });}
function ButtonGroupSeparator({ className, orientation = "vertical", ...props}: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="button-group-separator" orientation={orientation} className={cn( "relative self-stretch bg-input data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto", className, )} {...props} /> );}
export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants,};<script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { ButtonGroupVariants } from '.'import { provide } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { BUTTON_ELEVATION_KEY } from '@/registry/edmi/ui/button'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'import { buttonGroupVariants } from '.'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] orientation?: ButtonGroupVariants['orientation'] /** ✦ raised: each item is raised; floating: the whole group floats as one plate (items stay raised) */ elevation?: Elevation}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'button-filled')
// items follow the group only when it has an explicit level (floating group => raised items)provide(BUTTON_ELEVATION_KEY, { get value() { if (!props.elevation || props.elevation === 'auto') return undefined return level.value === 'floating' ? 'raised' : level.value },})</script>
<template> <div role="group" data-slot="button-group" :data-orientation="props.orientation" :class="cn(buttonGroupVariants({ orientation: props.orientation }), level === 'floating' && 'rounded-lg shadow-group-float', props.class)" > <slot /> </div></template><script setup lang="ts">import type { SeparatorProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { cn } from '@/registry/edmi/lib/utils'import { Separator } from '@/registry/edmi/ui/separator'
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), { orientation: 'vertical',})const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <Separator data-slot="button-group-separator" v-bind="delegatedProps" :orientation="props.orientation" :class="cn( 'relative self-stretch bg-input data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto', props.class, )" /></template><script setup lang="ts">import type { PrimitiveProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { Primitive } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
interface Props extends PrimitiveProps { class?: HTMLAttributes['class']}
const props = withDefaults(defineProps<Props>(), { as: 'div',})</script>
<template> <Primitive data-slot="button-group-text" :as="as" :as-child="asChild" :class="cn(`flex items-center gap-2 rounded-lg border border-input bg-muted px-2.5 text-[13px] font-medium text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4`, props.class)" > <slot /> </Primitive></template>import type { VariantProps } from "class-variance-authority";import { cva } from "class-variance-authority";
export { default as ButtonGroup } from "./ButtonGroup.vue";export { default as ButtonGroupSeparator } from "./ButtonGroupSeparator.vue";export { default as ButtonGroupText } from "./ButtonGroupText.vue";
export const buttonGroupVariants = cva( "flex w-fit items-stretch [&>[data-variant=default]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,var(--primary-foreground)_25%,var(--primary))] [&>[data-variant=brand]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,var(--brand-foreground)_25%,var(--brand))] [&>[data-variant=destructive]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,white_25%,var(--destructive))] *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 [&>[data-slot=input]]:shadow-none [&>[data-slot=input-group]]:shadow-none", { variants: { orientation: { horizontal: "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0", vertical: "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0", }, }, defaultVariants: { orientation: "horizontal", }, },);
export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>;<script lang="ts" module> import { tv, type VariantProps } from "tailwind-variants";
export const buttonGroupVariants = tv({ base: "group/button-group flex w-fit items-stretch [&>[data-variant=default]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,var(--primary-foreground)_25%,var(--primary))] [&>[data-variant=brand]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,var(--brand-foreground)_25%,var(--brand))] [&>[data-variant=destructive]+[data-slot=button-group-separator]]:bg-[color-mix(in_srgb,white_25%,var(--destructive))] *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", variants: { orientation: { horizontal: "*:data-slot:rounded-r-none [&>[data-slot=input]]:shadow-none [&>[data-slot=input-group]]:shadow-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0", vertical: "flex-col [&>[data-slot=input]]:shadow-none [&>[data-slot=input-group]]:shadow-none *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0", }, }, defaultVariants: { orientation: "horizontal", }, });
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>["orientation"];</script>
<script lang="ts"> import { setContext } from "svelte"; import { BUTTON_ELEVATION_CONTEXT } from "$lib/registry/ui/button/index.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, orientation = "horizontal", elevation = "auto", ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { orientation?: ButtonGroupOrientation; /** ✦ raised: each item is raised; floating: the whole group floats as one plate (items stay raised). */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "button-filled");
// items follow the group only when it has an explicit level (floating group => raised items) setContext(BUTTON_ELEVATION_CONTEXT, () => { if (!elevation || elevation === "auto") return undefined; return level.current === "floating" ? "raised" : level.current; });</script>
<div bind:this={ref} role="group" data-slot="button-group" data-orientation={orientation} class={cn(buttonGroupVariants({ orientation }), level.current === "floating" && "rounded-lg shadow-group-float", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Separator } from "$lib/registry/ui/separator/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, orientation = "vertical", ...restProps }: ComponentProps<typeof Separator> = $props();</script>
<Separator bind:ref data-slot="button-group-separator" {orientation} class={cn( "relative self-stretch bg-input data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto", className )} {...restProps}/><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, child, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const mergedProps = $derived({ ...restProps, class: cn( "flex items-center gap-2 rounded-lg border border-input bg-muted px-2.5 text-[13px] font-medium text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", className ), "data-slot": "button-group-text", });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <div bind:this={ref} {...mergedProps}> {@render mergedProps.children?.()} </div>{/if}import Root, { type ButtonGroupOrientation, buttonGroupVariants,} from "./button-group.svelte";import Separator from "./button-group-separator.svelte";import Text from "./button-group-text.svelte";
export { type ButtonGroupOrientation, buttonGroupVariants, Root, // Root as ButtonGroup, Separator, Separator as ButtonGroupSeparator, Text, Text as ButtonGroupText,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
elevation="raised" raises every item. elevation="floating" floats the whole group as one plate (a single soft drop); the items stay raised, never one drop per item. Buttons inside can still override with their own elevation.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="flex flex-wrap items-center gap-4"> <ButtonGroup elevation={value}> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup> <ButtonGroup elevation={value}> <Button variant="secondary">Buy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Sell</Button> </ButtonGroup> </div> </div> ))} </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="flex flex-wrap items-center gap-4"> <ButtonGroup :elevation="level.value"> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup> <ButtonGroup :elevation="level.value"> <Button variant="secondary">Buy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Sell</Button> </ButtonGroup> </div> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { ButtonGroup, ButtonGroupSeparator } from "$lib/components/ui/button-group";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="flex flex-wrap items-center gap-4"> <ButtonGroup elevation={level.value}> <Button variant="outline">Day</Button> <Button variant="outline">Week</Button> <Button variant="outline">Month</Button> </ButtonGroup> <ButtonGroup elevation={level.value}> <Button variant="secondary">Buy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Sell</Button> </ButtonGroup> </div> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
| orientation | horizontal | vertical |
horizontal |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
Parts: ButtonGroup, ButtonGroupSeparator, ButtonGroupText (render supported).
✦ Edmi additions
Section titled “✦ Edmi additions”- Same API as shadcn; depends on the
separatoritem.