Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 147 additions & 4 deletions docs/src/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,149 @@
],
"propsAlt": "export type TooltipProps = {\n /**\n * If the Tooltip is open.\n *\n * When passing a getter, it will be used as source of truth,\n * meaning that the value only changes when the getter returns a new value.\n *\n * Otherwise, if passing a static value, it'll serve as the default value.\n *\n * @default false\n */\n open?: MaybeGetter<boolean | undefined>;\n\n /**\n * Called when the value is supposed to change.\n */\n onOpenChange?: (value: boolean) => void;\n\n /**\n * If `true`, tooltip will close if trigger is pressed.\n *\n * @default true\n */\n closeOnPointerDown?: MaybeGetter<boolean | undefined>;\n\n /**\n * Tooltip open delay in milliseconds.\n *\n * @default 1000\n */\n openDelay?: MaybeGetter<number | undefined>;\n\n /**\n * Tooltip close delay in milliseconds.\n *\n * @default 0\n */\n closeDelay?: MaybeGetter<number | undefined>;\n\n /**\n * Config to be passed to `useFloating`\n */\n floatingConfig?: UseFloatingArgs[\"config\"];\n\n /**\n * If the popover visibility should be controlled by the user.\n *\n * @default false\n */\n forceVisible?: MaybeGetter<boolean | undefined>;\n\n /**\n * If `true`, leaving trigger will close the tooltip.\n *\n * @default false\n */\n disableHoverableContent?: MaybeGetter<boolean | undefined>;\n};"
},
"ToggleGroup": {
"constructorProps": [
{
"name": "type",
"type": "MaybeGetter<\"single\" | \"multiple\" | undefined>",
"description": "Whether the toggle(s) should allow multiple selection or just a single selection.\nIn 'single' mode, selecting one toggle will deselect any previously selected toggle.\nIn 'multiple' mode, toggles can be selected and deselected independently.",
"defaultValue": "\"single\"",
"optional": true
},
{
"name": "orientation",
"type": "MaybeGetter<\"horizontal\" | \"vertical\" | undefined>",
"description": "The orientation of the toggle group.",
"defaultValue": "\"horizontal\"",
"optional": true
},
{
"name": "disabled",
"type": "MaybeGetter<boolean | undefined>",
"description": "If `true`, prevents the user from interacting with the toggle group.",
"defaultValue": "false",
"optional": true
},
{
"name": "loop",
"type": "MaybeGetter<boolean | undefined>",
"description": "If the the item navigation should loop when using arrow keys.",
"defaultValue": "true",
"optional": true
},
{
"name": "required",
"type": "MaybeGetter<boolean | undefined>",
"description": "If `true`, requires at least one option to be selected.\nIn single mode, when required=true, the user won't be able to deselect an option without selecting another.\nIn multiple mode, at least one option must be selected.",
"defaultValue": "false",
"optional": true
},
{
"name": "value",
"type": "MaybeGetter<string | string[] | undefined>",
"description": "The controlled value of the toggle group.\nIf the type is 'single', it should be a string.\nIf the type is 'multiple', it should be an array of strings.",
"optional": true
},
{
"name": "onValueChange",
"type": "((value: string | string[]) => void) | undefined",
"description": "Called when the value changes.",
"optional": true
},
{
"name": "name",
"type": "MaybeGetter<string | undefined>",
"description": "Input name for the form hidden inputs.",
"optional": true
},
{
"name": "defaultItem",
"type": "MaybeGetter<string | undefined>",
"description": "The default item to be selected when the component is first rendered\nand required is true. This is only used if no value is provided.",
"optional": true
}
],
"methods": [
{
"name": "isSelected",
"type": "(itemValue: string) => boolean",
"description": ""
},
{
"name": "toggle",
"type": "(itemValue: string) => void",
"description": ""
},
{
"name": "getItem",
"type": "(item: string) => ToggleGroupItem",
"description": ""
}
],
"properties": [
{
"name": "ids",
"type": "{\n root: string\n item: string\n label: string\n \"hidden-input\": string\n}",
"description": ""
},
{
"name": "disabled",
"type": "boolean",
"description": ""
},
{
"name": "orientation",
"type": "\"horizontal\" | \"vertical\"",
"description": ""
},
{
"name": "type",
"type": "\"single\" | \"multiple\"",
"description": ""
},
{
"name": "loop",
"type": "boolean",
"description": ""
},
{
"name": "required",
"type": "boolean",
"description": ""
},
{
"name": "name",
"type": "string",
"description": ""
},
{
"name": "defaultItem",
"type": "string",
"description": ""
},
{
"name": "value",
"type": "string | string[]",
"description": ""
},
{
"name": "root",
"type": "{\n readonly \"data-melt-toggle-group-root\": \"\"\n readonly id: string\n readonly role: \"group\"\n readonly \"aria-labelledby\": string\n readonly \"aria-required\": boolean\n readonly \"data-required\": \"\" | undefined\n readonly \"data-orientation\": \"horizontal\" | \"vertical\"\n readonly \"data-disabled\": true | undefined\n}",
"description": ""
},
{
"name": "label",
"type": "{\n readonly \"data-melt-toggle-group-label\": \"\"\n readonly id: string\n readonly for: string\n readonly onclick: (\n e: MouseEvent & { currentTarget: EventTarget & HTMLLabelElement },\n ) => void\n readonly \"data-orientation\": \"horizontal\" | \"vertical\"\n readonly \"data-disabled\": true | undefined\n}",
"description": ""
},
{
"name": "hiddenInput",
"type": "| {\n readonly \"data-melt-toggle-group-hidden-input\": \"\"\n readonly type: \"hidden\"\n readonly name: string\n readonly value: string\n readonly disabled: boolean\n readonly required: boolean\n }[]\n | {\n readonly \"data-melt-toggle-group-hidden-input\": \"\"\n readonly type: \"hidden\"\n readonly name: string\n readonly value: string\n readonly disabled: boolean\n readonly required: boolean\n }",
"description": ""
}
],
"propsAlt": "export type ToggleGroupProps = {\n /**\n * Whether the toggle(s) should allow multiple selection or just a single selection.\n * In 'single' mode, selecting one toggle will deselect any previously selected toggle.\n * In 'multiple' mode, toggles can be selected and deselected independently.\n *\n * @default \"single\"\n */\n type?: MaybeGetter<\"single\" | \"multiple\" | undefined>;\n\n /**\n * The orientation of the toggle group.\n *\n * @default \"horizontal\"\n */\n orientation?: MaybeGetter<\"horizontal\" | \"vertical\" | undefined>;\n\n /**\n * If `true`, prevents the user from interacting with the toggle group.\n *\n * @default false\n */\n disabled?: MaybeGetter<boolean | undefined>;\n\n /**\n * If the the item navigation should loop when using arrow keys.\n *\n * @default true\n */\n loop?: MaybeGetter<boolean | undefined>;\n\n /**\n * If `true`, requires at least one option to be selected.\n * In single mode, when required=true, the user won't be able to deselect an option without selecting another.\n * In multiple mode, at least one option must be selected.\n *\n * @default false\n */\n required?: MaybeGetter<boolean | undefined>;\n\n /**\n * The controlled value of the toggle group.\n * If the type is 'single', it should be a string.\n * If the type is 'multiple', it should be an array of strings.\n */\n value?: MaybeGetter<string | string[] | undefined>;\n\n /**\n * Called when the value changes.\n */\n onValueChange?: (value: string | string[]) => void;\n\n /**\n * Input name for the form hidden inputs.\n */\n name?: MaybeGetter<string | undefined>;\n\n /**\n * The default item to be selected when the component is first rendered\n * and required is true. This is only used if no value is provided.\n */\n defaultItem?: MaybeGetter<string | undefined>;\n};"
},
"Toggle": {
"constructorProps": [
{
Expand Down Expand Up @@ -845,7 +988,7 @@
"properties": [
{
"name": "ids",
"type": "{\n root: string\n item: string\n \"hidden-input\": string\n label: string\n}",
"type": "{\n root: string\n item: string\n label: string\n \"hidden-input\": string\n}",
"description": ""
},
{
Expand Down Expand Up @@ -1076,7 +1219,7 @@
},
{
"name": "type",
"type": "MaybeGetter<\"alphanumeric\" | \"numeric\" | \"text\" | undefined>",
"type": "MaybeGetter<\"text\" | \"alphanumeric\" | \"numeric\" | undefined>",
"description": "What characters the input accepts.",
"defaultValue": "'text'",
"optional": true
Expand Down Expand Up @@ -1113,7 +1256,7 @@
},
{
"name": "type",
"type": "\"alphanumeric\" | \"numeric\" | \"text\"",
"type": "\"text\" | \"alphanumeric\" | \"numeric\"",
"description": ""
},
{
Expand All @@ -1138,7 +1281,7 @@
},
{
"name": "inputs",
"type": "{\n readonly \"data-melt-pin-input-input\": \"\"\n readonly placeholder: string | undefined\n readonly disabled: true | undefined\n readonly type: \"text\" | \"password\"\n readonly \"data-filled\": \"\" | undefined\n readonly tabindex: 0 | -1\n readonly inputmode: \"numeric\" | \"text\"\n readonly style: \"caret-color: transparent;\" | undefined\n readonly onkeydown: (e: KeyboardEvent) => void\n readonly onpointerdown: (e: Event) => void\n readonly onpointerup: (e: Event) => void\n readonly oninput: (e: Event) => void\n readonly onfocus: () => void\n readonly onblur: () => void\n readonly onpaste: (\n e: ClipboardEvent & { currentTarget: EventTarget & HTMLInputElement },\n ) => void\n}[]",
"type": "{\n readonly \"data-melt-pin-input-input\": \"\"\n readonly placeholder: string | undefined\n readonly disabled: true | undefined\n readonly type: \"password\" | \"text\"\n readonly \"data-filled\": \"\" | undefined\n readonly tabindex: 0 | -1\n readonly inputmode: \"text\" | \"numeric\"\n readonly style: \"caret-color: transparent;\" | undefined\n readonly onkeydown: (e: KeyboardEvent) => void\n readonly onpointerdown: (e: Event) => void\n readonly onpointerup: (e: Event) => void\n readonly oninput: (e: Event) => void\n readonly onfocus: () => void\n readonly onblur: () => void\n readonly onpaste: (\n e: ClipboardEvent & { currentTarget: EventTarget & HTMLInputElement },\n ) => void\n}[]",
"description": "An array of props that should be spread to the input elements."
}
],
Expand Down
147 changes: 147 additions & 0 deletions docs/src/content/docs/components/toggle-group.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
title: Toggle Group
description: A set of toggle buttons where one or multiple can be selected at a time.
---
import ApiTable from "@components/api-table.astro";
import Preview from "@previews/toggle-group.svelte";
import Features from "@components/features.astro";
import ThemedCode from "@components/themed-code.astro";
import { Tabs, TabItem } from '@astrojs/starlight/components';

<Preview client:load />

## Features

<Features>
- 🎹 Keyboard navigation with arrow keys
- 🔀 Single or multiple selection modes
- 🔄 Horizontal and vertical orientation
- ⭕ Loop navigation option
- 📝 Form support with hidden inputs
- 🏷️ Accessible label support
- 🔴 Required option support
- 🧠 Smart focus management
</Features>

## Usage

The Toggle Group component provides a way to group multiple toggle buttons with built-in keyboard navigation, focus management, and selection handling. It can operate in two modes:

- **Single mode**: Only one toggle can be selected at a time (like radio buttons)
- **Multiple mode**: Multiple toggles can be selected independently

When the `required` option is enabled, at least one toggle must be selected at all times:

- In **single mode**: Once an option is selected, it can't be deselected without selecting another
- In **multiple mode**: At least one option must remain selected

<Tabs>
<TabItem label="Builder">
```svelte
<script lang="ts">
import { ToggleGroup } from "melt/builders";

const toggleGroup = new ToggleGroup({
type: "single", // or "multiple"
orientation: "horizontal", // or "vertical"
name: "alignment", // for form submission
required: true, // requires at least one selected option
defaultItem: "left", // default selection when required is true
});
const items = ["left", "center", "right"];
</script>

<div class="flex flex-col gap-2">
<label {...toggleGroup.label}>Text Alignment</label>
<div {...toggleGroup.root} class="flex gap-1">
{#each items as i}
{@const item = toggleGroup.getItem(i)}
<button {...item.attrs} class="p-2 border rounded data-[state=on]:bg-blue-500">
{i}
</button>
{/each}

<!-- For form support -->
{#if toggleGroup.type === "multiple" && Array.isArray(toggleGroup.value)}
{#each toggleGroup.value as val}
<input type="hidden" name={toggleGroup.name} value={val} />
{/each}
{:else}
<input type="hidden" name={toggleGroup.name} value={toggleGroup.value || ""} />
{/if}
</div>
</div>
```
</TabItem>

<TabItem label="Component">
```svelte
<script lang="ts">
import { ToggleGroup } from "melt/components";
const items = ["left", "center", "right"];
</script>

<ToggleGroup type="single" orientation="horizontal" name="alignment" required={true} defaultItem="left">
{#snippet children(toggleGroup)}
<div class="flex flex-col gap-2">
<label {...toggleGroup.label}>Text Alignment</label>
<div {...toggleGroup.root} class="flex gap-1">
{#each items as i}
{@const item = toggleGroup.getItem(i)}
<button
{...item.attrs}
class="p-2 border rounded data-[state=on]:bg-blue-500"
>
{i}
</button>
{/each}

<!-- For form support -->
{#if toggleGroup.type === "multiple" && Array.isArray(toggleGroup.value)}
{#each toggleGroup.value as val}
<input type="hidden" name={toggleGroup.name} value={val} />
{/each}
{:else}
<input type="hidden" name={toggleGroup.name} value={toggleGroup.value || ""} />
{/if}
</div>
</div>
{/snippet}
</ToggleGroup>
```
</TabItem>
</Tabs>

## Keyboard Interactions

| Key | Description |
|--------------|-------------------------------------------------------|
| `Tab` | Moves focus to the next focusable element |
| `Shift + Tab`| Moves focus to the previous focusable element |
| `Space` | Toggles the focused toggle button |
| `Enter` | Toggles the focused toggle button |
| `ArrowRight` | In horizontal orientation, moves focus to the next toggle button |
| `ArrowLeft` | In horizontal orientation, moves focus to the previous toggle button |
| `ArrowDown` | In vertical orientation, moves focus to the next toggle button |
| `ArrowUp` | In vertical orientation, moves focus to the previous toggle button |
| `Home` | Moves focus to the first toggle button |
| `End` | Moves focus to the last toggle button |

## Accessibility

ToggleGroup uses the correct ARIA attributes for toggle buttons in a group:

- Each toggle button has `role="button"` and `aria-pressed` attributes
- The group container has `role="group"` with `aria-labelledby` referencing the label
- The label is properly associated with the group
- When required, the group has `aria-required="true"` for accessibility
- Focus is properly managed with keyboard navigation

For better accessibility:
- Always include a label that describes the purpose of the toggle group
- Add descriptive text or `aria-label` to each toggle button
- Use visually distinct states for selected and unselected toggles (not just color)

## API Reference

<ApiTable entry="ToggleGroup" />
Loading