# Toolbar

Composable keyboard toolbar with buttons, links, and selectable groups.

- Package: `@mielui/svelte`
- Component version: `1.0.1`
- Depends on Mielui components: _internal/utils, button, separator
- Shared utilities: utils.cn, utils.pressable, utils.travelingHighlight, utils.createContext

## Install

~~~~sh
pnpm dlx @mielui/svelte add toolbar
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Button

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| disabled | boolean \| null \| undefined | - | No | No | Whether the button is disabled or not. |
| element | HTMLButtonElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Group

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| disabled | boolean \| undefined | - | No | No | Whether the toggle group is disabled or not. |
| type | "single" \| "multiple" | - | Yes | No | `single` keeps one value. `multiple` allows several and makes `value` an array. |
| value | string \| string[] \| undefined | - | No | Yes | Selected tool, or an array in multiple mode. Bindable. |
| onValueChange | ((value: string) => void) \| ((value: string[]) => void) \| undefined | - | No | No | Called with the new value when it changes. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Item

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[ToggleGroupItemSnippetProps]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| disabled | boolean \| null \| undefined | - | No | No | Whether the toggle item is disabled or not. |
| value | string | - | Yes | No | The value of the toggle item. |
| element | HTMLButtonElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Link

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| element | HTMLAnchorElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| loop | boolean \| undefined | - | No | No | Whether or not to loop through the toolbar items when navigating with the keyboard. |
| orientation | Orientation \| undefined | 'horizontal' | No | No | The orientation of the toolbar. This determines how keyboard navigation will work within the toolbar. |
| variant | "default" \| "depth" \| undefined | 'default' | No | No | `depth` gives the toolbar raised keys and a floating shell. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Separator

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| orientation | Orientation \| undefined | - | No | No | The orientation of the separator. |
| decorative | boolean \| undefined | - | No | No | Whether the separator is decorative and should be hidden from assistive technologies. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Toolbar

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { DefaultProps } from '@mielui/svelte/utils';
import type { Toolbar as Primitive } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
import Toolbar from './toolbar.svelte';

export type ToolbarProps = DefaultProps & Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'role'>;

export { default as Button } from './toolbar-button.svelte';
export { default as Group } from './toolbar-group.svelte';
export { default as Item } from './toolbar-item.svelte';
export { default as Link } from './toolbar-link.svelte';
export { default as Root } from './toolbar-root.svelte';
export { default as Separator } from './toolbar-separator.svelte';
export { Toolbar };
export type ToolbarRootProps = Omit<Primitive.RootProps, 'child' | 'ref'> & {
    /** `depth` gives the toolbar raised keys and a floating shell. */
    variant?: 'default' | 'depth';
    /** Bindable reference to the DOM element. */
    element?: HTMLDivElement | null;
};
export type ToolbarButtonProps = Omit<Primitive.ButtonProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLButtonElement | null;
};
export type ToolbarLinkProps = Omit<Primitive.LinkProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLAnchorElement | null;
};
export type ToolbarGroupProps = Omit<
    Primitive.GroupProps,
    'type' | 'value' | 'onValueChange' | 'child' | 'ref'
> &
    (
        | {
              /** `single` keeps one value. `multiple` allows several and makes `value` an array. */
              type: 'single';
              /** Selected tool, or an array in multiple mode. Bindable. */
              value?: string;
              /** Called with the new value when it changes. */
              onValueChange?: (value: string) => void;
          }
        | {
              /** `single` keeps one value. `multiple` allows several and makes `value` an array. */
              type: 'multiple';
              /** Selected tool, or an array in multiple mode. Bindable. */
              value?: string[];
              /** Called with the new value when it changes. */
              onValueChange?: (value: string[]) => void;
          }
    ) & {
        /** Bindable reference to the DOM element. */
        element?: HTMLDivElement | null;
    };
export type ToolbarItemProps = Omit<Primitive.GroupItemProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLButtonElement | null;
};
~~~~

## Examples

### Depth

~~~~svelte
<script lang="ts">
    import {
        BubbleChatIcon,
        FrameIcon,
        MinusSignIcon,
        MousePointer01Icon,
        PenTool01Icon,
        PlusSignIcon,
        SquareIcon,
        TextFontIcon
    } from '@hugeicons/core-free-icons';
    import { numberShuffle } from '@mielui/svelte/actions/number-shuffle';
    import Kbd from '@mielui/svelte/components/kbd';
    import * as Toolbar from '@mielui/svelte/components/toolbar';
    import * as Tooltip from '@mielui/svelte/components/tooltip';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    const tools = [
        {
            id: 'move',
            label: 'Move',
            shortcut: 'V',
            icon: MousePointer01Icon
        },
        {
            id: 'frame',
            label: 'Frame',
            shortcut: 'F',
            icon: FrameIcon
        },
        {
            id: 'rectangle',
            label: 'Rectangle',
            shortcut: 'R',
            icon: SquareIcon
        },
        {
            id: 'pen',
            label: 'Pen',
            shortcut: 'P',
            icon: PenTool01Icon
        },
        {
            id: 'text',
            label: 'Text',
            shortcut: 'T',
            icon: TextFontIcon
        },
        {
            id: 'comment',
            label: 'Comment',
            shortcut: 'C',
            icon: BubbleChatIcon
        }
    ];

    let activeTool = $state('move');
    let zoom = $state(100);

    const activeLabel = $derived(tools.find((tool) => tool.id === activeTool)?.label ?? 'No tool');

    function zoomOut() {
        zoom = Math.max(25, zoom - 25);
    }

    function zoomIn() {
        zoom = Math.min(200, zoom + 25);
    }

    function resetZoom() {
        zoom = 100;
    }
</script>

<div class="@container flex w-full flex-col items-center gap-3">
    <Toolbar.Root
        variant="depth"
        aria-label="Design tools"
        class="max-w-full flex-wrap justify-center gap-2 p-1"
    >
        <Toolbar.Group
            type="single"
            bind:value={activeTool}
            aria-label="Active tool"
            class="flex-wrap justify-center gap-1"
        >
            {#each tools as tool (tool.id)}
                <Tooltip.Root placement="top" delay={300}>
                    <Tooltip.Trigger>
                        <Toolbar.Item
                            value={tool.id}
                            aria-label={tool.label}
                            class="size-8 shrink-0 p-0"
                        >
                            <HugeiconsIcon
                                icon={tool.icon}
                                size={16}
                                class="size-4"
                                aria-hidden="true"
                            />
                            <span class="sr-only"><Kbd shortcut={tool.shortcut} /></span>
                        </Toolbar.Item>
                    </Tooltip.Trigger>
                    <Tooltip.Content
                        rich
                        surface="solid"
                        class="bg-[var(--color-field)] text-[var(--color-field-foreground)]"
                    >
                        <span class="flex items-center gap-2">
                            {tool.label}
                            <Kbd
                                shortcut={tool.shortcut}
                                class="border-current/15 bg-current/10 text-inherit shadow-none"
                            />
                        </span>
                    </Tooltip.Content>
                </Tooltip.Root>
            {/each}
        </Toolbar.Group>
        <Toolbar.Separator class="hidden h-7 @min-[32rem]:block" />
        <div class="flex items-center gap-1">
            <Toolbar.Button
                aria-label="Zoom out"
                disabled={zoom <= 25}
                class="size-8 p-0"
                onclick={zoomOut}
            >
                <HugeiconsIcon icon={MinusSignIcon} size={16} class="size-4" aria-hidden="true" />
            </Toolbar.Button>
            <Toolbar.Button
                aria-label="Reset zoom"
                class="h-8 min-w-16 bg-none bg-background font-mono text-xs tabular-nums shadow-[var(--mielui-toolbar-pressed)]"
                onclick={resetZoom}
            >
                <span use:numberShuffle={{ value: zoom, format: (value) => `${value}%` }}>
                    {zoom}%
                </span>
            </Toolbar.Button>
            <Toolbar.Button
                aria-label="Zoom in"
                disabled={zoom >= 200}
                class="size-8 p-0"
                onclick={zoomIn}
            >
                <HugeiconsIcon icon={PlusSignIcon} size={16} class="size-4" aria-hidden="true" />
            </Toolbar.Button>
        </div>
    </Toolbar.Root>
    <p role="status" class="text-sm text-foreground-muted">
        {activeLabel}
        selected ·
        <span use:numberShuffle={{ value: zoom, format: (value) => `${value}%` }}>{zoom}%</span>
        zoom
    </p>
</div>
~~~~

### Formatting

~~~~svelte
<script lang="ts">
    import * as Toolbar from '@mielui/svelte/components/toolbar';

    let formats = $state<string[]>(['bold']);

    function clear() {
        formats = [];
    }
</script>

<div class="flex flex-col items-center gap-4">
    <Toolbar.Root aria-label="Text formatting">
        <Toolbar.Group type="multiple" bind:value={formats} aria-label="Text styles">
            <Toolbar.Item value="bold">Bold</Toolbar.Item>
            <Toolbar.Item value="italic">Italic</Toolbar.Item>
            <Toolbar.Item value="underline">Underline</Toolbar.Item>
        </Toolbar.Group>
        <Toolbar.Separator />
        <Toolbar.Button onclick={clear}>Clear</Toolbar.Button>
    </Toolbar.Root>
    <p
        class={[
            'text-sm',
            {
                'font-bold': formats.includes('bold'),
                italic: formats.includes('italic'),
                underline: formats.includes('underline')
            }
        ]}
    >
        A note for the team.
    </p>
</div>
~~~~

### Hero

~~~~svelte
<script lang="ts">
    import {
        BubbleChatIcon,
        FrameIcon,
        MinusSignIcon,
        MousePointer01Icon,
        PenTool01Icon,
        PlusSignIcon,
        SquareIcon,
        TextFontIcon
    } from '@hugeicons/core-free-icons';
    import { numberShuffle } from '@mielui/svelte/actions/number-shuffle';
    import Kbd from '@mielui/svelte/components/kbd';
    import * as Toolbar from '@mielui/svelte/components/toolbar';
    import * as Tooltip from '@mielui/svelte/components/tooltip';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    const tools = [
        {
            id: 'move',
            label: 'Move',
            shortcut: 'V',
            icon: MousePointer01Icon
        },
        {
            id: 'frame',
            label: 'Frame',
            shortcut: 'F',
            icon: FrameIcon
        },
        {
            id: 'rectangle',
            label: 'Rectangle',
            shortcut: 'R',
            icon: SquareIcon
        },
        {
            id: 'pen',
            label: 'Pen',
            shortcut: 'P',
            icon: PenTool01Icon
        },
        {
            id: 'text',
            label: 'Text',
            shortcut: 'T',
            icon: TextFontIcon
        },
        {
            id: 'comment',
            label: 'Comment',
            shortcut: 'C',
            icon: BubbleChatIcon
        }
    ];

    let activeTool = $state('move');
    let zoom = $state(100);

    const activeLabel = $derived(tools.find((tool) => tool.id === activeTool)?.label ?? 'No tool');

    function zoomOut() {
        zoom = Math.max(25, zoom - 25);
    }

    function zoomIn() {
        zoom = Math.min(200, zoom + 25);
    }

    function resetZoom() {
        zoom = 100;
    }
</script>

<div class="@container flex w-full flex-col items-center gap-3">
    <Toolbar.Root aria-label="Design tools" class="max-w-full flex-wrap justify-center gap-2">
        <Toolbar.Group
            type="single"
            bind:value={activeTool}
            aria-label="Active tool"
            class="flex-wrap justify-center gap-1"
        >
            {#each tools as tool (tool.id)}
                <Tooltip.Root placement="top" delay={300}>
                    <Tooltip.Trigger>
                        <Toolbar.Item
                            value={tool.id}
                            aria-label={tool.label}
                            class="size-8 shrink-0 p-0"
                        >
                            <HugeiconsIcon
                                icon={tool.icon}
                                size={16}
                                class="size-4"
                                aria-hidden="true"
                            />
                            <span class="sr-only"><Kbd shortcut={tool.shortcut} /></span>
                        </Toolbar.Item>
                    </Tooltip.Trigger>
                    <Tooltip.Content
                        rich
                        surface="solid"
                        class="bg-[var(--color-field)] text-[var(--color-field-foreground)]"
                    >
                        <span class="flex items-center gap-2">
                            {tool.label}
                            <Kbd
                                shortcut={tool.shortcut}
                                class="border-current/15 bg-current/10 text-inherit shadow-none"
                            />
                        </span>
                    </Tooltip.Content>
                </Tooltip.Root>
            {/each}
        </Toolbar.Group>
        <Toolbar.Separator class="hidden h-7 @min-[32rem]:block" />
        <div class="flex items-center gap-1">
            <Toolbar.Button
                aria-label="Zoom out"
                disabled={zoom <= 25}
                class="size-8 p-0"
                onclick={zoomOut}
            >
                <HugeiconsIcon icon={MinusSignIcon} size={16} class="size-4" aria-hidden="true" />
            </Toolbar.Button>
            <Toolbar.Button
                aria-label="Reset zoom"
                class="min-w-16 font-mono text-xs tabular-nums"
                onclick={resetZoom}
            >
                <span use:numberShuffle={{ value: zoom, format: (value) => `${value}%` }}>
                    {zoom}%
                </span>
            </Toolbar.Button>
            <Toolbar.Button
                aria-label="Zoom in"
                disabled={zoom >= 200}
                class="size-8 p-0"
                onclick={zoomIn}
            >
                <HugeiconsIcon icon={PlusSignIcon} size={16} class="size-4" aria-hidden="true" />
            </Toolbar.Button>
        </div>
    </Toolbar.Root>
    <p role="status" class="text-sm text-foreground-muted">
        {activeLabel}
        selected ·
        <span use:numberShuffle={{ value: zoom, format: (value) => `${value}%` }}>{zoom}%</span>
        zoom
    </p>
</div>
~~~~

For the rendered reference, visit [/docs/components/toolbar](/docs/components/toolbar).
