Button Group

A container that groups related buttons together with consistent styling.

Basic
With Input
With Text
Text
With Dropdown
With Select
With Icons
With Input Group
With Fields
W
px
With Like
1.2K
With Select and Input
Nested
Pagination
Pagination Split
Navigation
Text Alignment
Vertical
Vertical Nested

Installation

Install the dependencies

pnpm add cn class-variance-authority github:sveltery/base

Copy the component

Copy the following files into src/lib/components/ui/button-group.

It imports the separator component, so copy its folder too.

src/lib/components/ui/button-group/ButtonGroup.svelte
<script lang="ts">
  import type { ButtonGroupProps } from './types.js';
  import { cn } from 'cn';
  import { buttonGroupVariants } from './variants.js';
  let { children, class: classProp, orientation, ...props }: ButtonGroupProps = $props();
</script>
<div role="group" data-slot="button-group" data-orientation={orientation} {...props} class={cn(buttonGroupVariants({ orientation }), classProp)}>
  {@render children?.()}
</div>
src/lib/components/ui/button-group/ButtonGroupSeparator.svelte
<script lang="ts">
  import type { ButtonGroupSeparatorProps } from './types.js';
  import { cn } from 'cn';
  import { Separator } from '../separator/index.js';
  let { class: classProp, orientation = 'vertical', ...props }: ButtonGroupSeparatorProps = $props();
</script>
<Separator
  data-slot="button-group-separator"
  {orientation}
  class={cn('bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto', classProp)}
  {...props}
/>
src/lib/components/ui/button-group/ButtonGroupText.svelte
<script lang="ts">
  import type { ButtonGroupTextProps, ButtonGroupTextState } from './types.js';
  import { cn } from 'cn';
  let { children, class: classProp, render, ...props }: ButtonGroupTextProps = $props();
  const state: ButtonGroupTextState = { slot: 'button-group-text' };
  const hostProps = $derived({ 'data-slot': state.slot, ...props, class: cn("bg-muted gap-2 rounded-lg border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", classProp) });
</script>
{#if render}
  {@render render(hostProps, state, children)}
{:else}
  <div {...hostProps}>{@render children?.()}</div>
{/if}
src/lib/components/ui/button-group/types.ts
import type { HTMLAttributes } from 'svelte/elements';
import type { RenderSnippet } from '../shared/render.js';
import type { SeparatorProps } from '../separator/index.js';
import type { ButtonGroupVariants } from './variants.js';

export type ButtonGroupProps = HTMLAttributes<HTMLDivElement> & ButtonGroupVariants;
export type ButtonGroupTextState = { slot: 'button-group-text' };
export type ButtonGroupTextProps = HTMLAttributes<HTMLDivElement> & { render?: RenderSnippet<ButtonGroupTextState, HTMLDivElement> };
export type ButtonGroupSeparatorProps = SeparatorProps;
src/lib/components/ui/button-group/variants.ts
// Adapted from shadcn-ui/ui d75a96ab; MIT: packages/ui/THIRD_PARTY_NOTICES.md.
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
export const buttonGroupVariants = cva("has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", {
  variants: {
    orientation: {
      horizontal: '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! *:data-slot:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0',
      vertical: '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! flex-col *:data-slot:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0',
    },
  },
  defaultVariants: { orientation: 'horizontal' },
});
export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>;
src/lib/components/ui/shared/render.ts
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';

/**
 * Replaces a part's default element, like the `render` snippets of Sveltery Base.
 * Spread `props` onto the host element and render `children` inside it.
 * `Host` is the element the part renders by default, so its typed event handlers carry over.
 */
export type RenderSnippet<State, Host extends HTMLElement = HTMLElement> = Snippet<[props: HTMLAttributes<Host>, state: State, children: Snippet | undefined]>;
src/lib/components/ui/button-group/index.ts
export { default as ButtonGroup } from './ButtonGroup.svelte';
export { default as ButtonGroupText } from './ButtonGroupText.svelte';
export { default as ButtonGroupSeparator } from './ButtonGroupSeparator.svelte';
export { buttonGroupVariants } from './variants.js';
export type { ButtonGroupVariants } from './variants.js';
export type { ButtonGroupProps, ButtonGroupTextProps, ButtonGroupTextState, ButtonGroupSeparatorProps } from './types.js';

Update the import paths

Change the imports to match your project setup.

Usage

<script lang="ts">
  import { Button } from '$lib/components/ui/button';
  import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from '$lib/components/ui/button-group';
</script>
<ButtonGroup>
  <Button variant="outline">Archive</Button>
  <Button variant="outline">Report</Button>
</ButtonGroup>