Toggle Group
Segmented control on bits-ui ToggleGroup. With spacing=0 the children share a continuous rail; raise spacing for detached pills. Each segment is an independent Toggle that keeps its own hover, focus, and pressed states.
Single
toggle-group-single.svelte
<script lang="ts">
import { ToggleGroup, ToggleGroupItem } from '$lib/components/ui/toggle-group';
let value = $state<string>('center');
</script>
<ToggleGroup type="single" bind:value>
<ToggleGroupItem value="left">Left</ToggleGroupItem>
<ToggleGroupItem value="center">Center</ToggleGroupItem>
<ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>Multiple
toggle-group-multiple.svelte
<script lang="ts">
let value = $state<string[]>([]);
</script>
<ToggleGroup type="multiple" bind:value>
<ToggleGroupItem value="bold">B</ToggleGroupItem>
<ToggleGroupItem value="italic">I</ToggleGroupItem>
<ToggleGroupItem value="underline">U</ToggleGroupItem>
</ToggleGroup>Detached pills
toggle-group.svelte
<script lang="ts" module>
import { getContext, setContext } from 'svelte';
import type { VariantProps } from 'tailwind-variants';
import { toggleVariants } from '$lib/components/ui/toggle/index.js';
type ToggleVariants = VariantProps<typeof toggleVariants>;
interface ToggleGroupContext extends ToggleVariants {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
}
export function setToggleGroupCtx(props: ToggleGroupContext) {
setContext('toggleGroup', props);
}
export function getToggleGroupCtx() {
return getContext<Required<ToggleGroupContext>>('toggleGroup');
}
</script>
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { proximity } from '$lib/motion/index.js';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size = 'default',
spacing = 0,
orientation = 'horizontal',
variant = 'default',
label,
labelledby,
children,
...restProps
}: ToggleGroupPrimitive.RootProps &
ToggleVariants & {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
/** Accessible name for the toggle group. Set this or `labelledby`. */
label?: string;
/** ID of an element labeling the toggle group. Set this or `label`. */
labelledby?: string;
} = $props();
setToggleGroupCtx({
get variant() {
return variant;
},
get size() {
return size;
},
get spacing() {
return spacing;
},
get orientation() {
return orientation;
}
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<ToggleGroupPrimitive.Root
bind:value={value as never}
bind:ref
{orientation}
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={`--gap: ${spacing}`}
aria-label={label}
aria-labelledby={labelledby}
{...restProps}
>
{#snippet child({ props })}
<div
{...props}
class={cn(
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch data-[spacing=0]:data-[variant=outline]:rounded-(--radius-control) [&>[data-slot=toggle-group-item]]:relative [&>[data-slot=toggle-group-item]]:before:pointer-events-none [&>[data-slot=toggle-group-item]]:before:absolute [&>[data-slot=toggle-group-item]]:before:inset-0 [&>[data-slot=toggle-group-item]]:before:rounded-[inherit] [&>[data-slot=toggle-group-item]]:before:transition-[background-color] [&>[data-slot=toggle-group-item]]:before:duration-(--motion-duration-fast) [&>[data-slot=toggle-group-item]]:before:content-[''] [&>[data-slot=toggle-group-item]]:before:[background:color-mix(in_oklch,var(--accent)_calc(var(--proximity,0)*25%),transparent)]",
className
)}
use:proximity={{ selector: '[data-slot=toggle-group-item]', maxDistance: 80 }}
>
{@render children?.()}
</div>
{/snippet}
</ToggleGroupPrimitive.Root>
Sizes
toggle-group.svelte
<script lang="ts" module>
import { getContext, setContext } from 'svelte';
import type { VariantProps } from 'tailwind-variants';
import { toggleVariants } from '$lib/components/ui/toggle/index.js';
type ToggleVariants = VariantProps<typeof toggleVariants>;
interface ToggleGroupContext extends ToggleVariants {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
}
export function setToggleGroupCtx(props: ToggleGroupContext) {
setContext('toggleGroup', props);
}
export function getToggleGroupCtx() {
return getContext<Required<ToggleGroupContext>>('toggleGroup');
}
</script>
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { proximity } from '$lib/motion/index.js';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size = 'default',
spacing = 0,
orientation = 'horizontal',
variant = 'default',
label,
labelledby,
children,
...restProps
}: ToggleGroupPrimitive.RootProps &
ToggleVariants & {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
/** Accessible name for the toggle group. Set this or `labelledby`. */
label?: string;
/** ID of an element labeling the toggle group. Set this or `label`. */
labelledby?: string;
} = $props();
setToggleGroupCtx({
get variant() {
return variant;
},
get size() {
return size;
},
get spacing() {
return spacing;
},
get orientation() {
return orientation;
}
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<ToggleGroupPrimitive.Root
bind:value={value as never}
bind:ref
{orientation}
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={`--gap: ${spacing}`}
aria-label={label}
aria-labelledby={labelledby}
{...restProps}
>
{#snippet child({ props })}
<div
{...props}
class={cn(
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch data-[spacing=0]:data-[variant=outline]:rounded-(--radius-control) [&>[data-slot=toggle-group-item]]:relative [&>[data-slot=toggle-group-item]]:before:pointer-events-none [&>[data-slot=toggle-group-item]]:before:absolute [&>[data-slot=toggle-group-item]]:before:inset-0 [&>[data-slot=toggle-group-item]]:before:rounded-[inherit] [&>[data-slot=toggle-group-item]]:before:transition-[background-color] [&>[data-slot=toggle-group-item]]:before:duration-(--motion-duration-fast) [&>[data-slot=toggle-group-item]]:before:content-[''] [&>[data-slot=toggle-group-item]]:before:[background:color-mix(in_oklch,var(--accent)_calc(var(--proximity,0)*25%),transparent)]",
className
)}
use:proximity={{ selector: '[data-slot=toggle-group-item]', maxDistance: 80 }}
>
{@render children?.()}
</div>
{/snippet}
</ToggleGroupPrimitive.Root>
Outline
Outline variant keeps the segment border visible in the unpressed state — useful on crowded toolbars where the rail needs to read as a control cluster at rest.
toggle-group-outline.svelte
<script lang="ts" module>
import { getContext, setContext } from 'svelte';
import type { VariantProps } from 'tailwind-variants';
import { toggleVariants } from '$lib/components/ui/toggle/index.js';
type ToggleVariants = VariantProps<typeof toggleVariants>;
interface ToggleGroupContext extends ToggleVariants {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
}
export function setToggleGroupCtx(props: ToggleGroupContext) {
setContext('toggleGroup', props);
}
export function getToggleGroupCtx() {
return getContext<Required<ToggleGroupContext>>('toggleGroup');
}
</script>
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { proximity } from '$lib/motion/index.js';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size = 'default',
spacing = 0,
orientation = 'horizontal',
variant = 'default',
label,
labelledby,
children,
...restProps
}: ToggleGroupPrimitive.RootProps &
ToggleVariants & {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
/** Accessible name for the toggle group. Set this or `labelledby`. */
label?: string;
/** ID of an element labeling the toggle group. Set this or `label`. */
labelledby?: string;
} = $props();
setToggleGroupCtx({
get variant() {
return variant;
},
get size() {
return size;
},
get spacing() {
return spacing;
},
get orientation() {
return orientation;
}
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<ToggleGroupPrimitive.Root
bind:value={value as never}
bind:ref
{orientation}
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={`--gap: ${spacing}`}
aria-label={label}
aria-labelledby={labelledby}
{...restProps}
>
{#snippet child({ props })}
<div
{...props}
class={cn(
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch data-[spacing=0]:data-[variant=outline]:rounded-(--radius-control) [&>[data-slot=toggle-group-item]]:relative [&>[data-slot=toggle-group-item]]:before:pointer-events-none [&>[data-slot=toggle-group-item]]:before:absolute [&>[data-slot=toggle-group-item]]:before:inset-0 [&>[data-slot=toggle-group-item]]:before:rounded-[inherit] [&>[data-slot=toggle-group-item]]:before:transition-[background-color] [&>[data-slot=toggle-group-item]]:before:duration-(--motion-duration-fast) [&>[data-slot=toggle-group-item]]:before:content-[''] [&>[data-slot=toggle-group-item]]:before:[background:color-mix(in_oklch,var(--accent)_calc(var(--proximity,0)*25%),transparent)]",
className
)}
use:proximity={{ selector: '[data-slot=toggle-group-item]', maxDistance: 80 }}
>
{@render children?.()}
</div>
{/snippet}
</ToggleGroupPrimitive.Root>
Disabled
Disable the whole group with disabled on the root, or
disable individual items by passing disabled on a
single ToggleGroupItem.
toggle-group-disabled.svelte
<script lang="ts" module>
import { getContext, setContext } from 'svelte';
import type { VariantProps } from 'tailwind-variants';
import { toggleVariants } from '$lib/components/ui/toggle/index.js';
type ToggleVariants = VariantProps<typeof toggleVariants>;
interface ToggleGroupContext extends ToggleVariants {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
}
export function setToggleGroupCtx(props: ToggleGroupContext) {
setContext('toggleGroup', props);
}
export function getToggleGroupCtx() {
return getContext<Required<ToggleGroupContext>>('toggleGroup');
}
</script>
<script lang="ts">
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import { proximity } from '$lib/motion/index.js';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
size = 'default',
spacing = 0,
orientation = 'horizontal',
variant = 'default',
label,
labelledby,
children,
...restProps
}: ToggleGroupPrimitive.RootProps &
ToggleVariants & {
spacing?: number;
orientation?: 'horizontal' | 'vertical';
/** Accessible name for the toggle group. Set this or `labelledby`. */
label?: string;
/** ID of an element labeling the toggle group. Set this or `label`. */
labelledby?: string;
} = $props();
setToggleGroupCtx({
get variant() {
return variant;
},
get size() {
return size;
},
get spacing() {
return spacing;
},
get orientation() {
return orientation;
}
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<ToggleGroupPrimitive.Root
bind:value={value as never}
bind:ref
{orientation}
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={`--gap: ${spacing}`}
aria-label={label}
aria-labelledby={labelledby}
{...restProps}
>
{#snippet child({ props })}
<div
{...props}
class={cn(
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch data-[spacing=0]:data-[variant=outline]:rounded-(--radius-control) [&>[data-slot=toggle-group-item]]:relative [&>[data-slot=toggle-group-item]]:before:pointer-events-none [&>[data-slot=toggle-group-item]]:before:absolute [&>[data-slot=toggle-group-item]]:before:inset-0 [&>[data-slot=toggle-group-item]]:before:rounded-[inherit] [&>[data-slot=toggle-group-item]]:before:transition-[background-color] [&>[data-slot=toggle-group-item]]:before:duration-(--motion-duration-fast) [&>[data-slot=toggle-group-item]]:before:content-[''] [&>[data-slot=toggle-group-item]]:before:[background:color-mix(in_oklch,var(--accent)_calc(var(--proximity,0)*25%),transparent)]",
className
)}
use:proximity={{ selector: '[data-slot=toggle-group-item]', maxDistance: 80 }}
>
{@render children?.()}
</div>
{/snippet}
</ToggleGroupPrimitive.Root>
ToggleGroup props
Inherits from bits-ui ToggleGroup.Root. When spacing=0 the items seam into a rail; otherwise they become separated pills with a `--gap` token.
| Prop | Type | Default | Description |
|---|---|---|---|
'single' | 'multiple' | — | Single picks one value at a time; multiple lets users stack toggles. | |
string | string[] (bindable) | — | Currently selected value(s). String for single, array for multiple. Two-way bindable. | |
'default' | 'outline' | 'default' | Inherited from Toggle. Outline draws a bordered segment. | |
'default' | 'sm' | 'lg' | 'default' | Sets segment height to 36 / 32 / 40 px. | |
number | 0 | Spacing-scale gap between items. Zero fuses the rail; raise for detached pills. | |
'horizontal' | 'vertical' | 'horizontal' | Layout axis. Vertical stacks segments. | |
boolean | false | Disables every item in the group. |