= {
+ bold: {value()},
+ italic: {value()},
+ underline: {value()},
+ };
+
+ const render = () => {
+ return list[value()];
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ Your text style is: {render()}.
+
+ >
+ );
+}
+
+export function MultipleSelectionExample() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/docs/src/routes/docs/core.tsx b/apps/docs/src/routes/docs/core.tsx
index cf04179a..43de7b31 100644
--- a/apps/docs/src/routes/docs/core.tsx
+++ b/apps/docs/src/routes/docs/core.tsx
@@ -100,7 +100,6 @@ const CORE_NAV_SECTIONS: NavSection[] = [
{
title: "Number Field",
href: "/docs/core/components/number-field",
- status: "new",
},
{
title: "Pagination",
@@ -154,6 +153,11 @@ const CORE_NAV_SECTIONS: NavSection[] = [
title: "Toggle Button",
href: "/docs/core/components/toggle-button",
},
+ {
+ title: "Toggle Group",
+ href: "/docs/core/components/toggle-group",
+ status: "new",
+ },
{
title: "Tooltip",
href: "/docs/core/components/tooltip",
diff --git a/apps/docs/src/routes/docs/core/components/toggle-group.mdx b/apps/docs/src/routes/docs/core/components/toggle-group.mdx
new file mode 100644
index 00000000..79b53b8c
--- /dev/null
+++ b/apps/docs/src/routes/docs/core/components/toggle-group.mdx
@@ -0,0 +1,253 @@
+import { Kbd, Preview, TabsSnippets } from "../../../../components";
+import {
+ BasicExample,
+ ControlledExample,
+ DefaultValueExample,
+ MultipleSelectionExample,
+} from "../../../../examples/toggle-group";
+
+# Toggle Group
+
+A set of two-state buttons that can be toggled on (pressed) or off (not pressed).
+
+## Import
+
+```ts
+import { ToggleGroup } from "@kobalte/core";
+```
+
+## Features
+
+- Supports horizontal/vertical orientation.
+- Keyboard event support for Space and Enter keys.
+- Can be controlled or uncontrolled.
+
+## Anatomy
+
+The toggle group consists of:
+
+- **ToggleGroup.Root:** the root container for a toggle group.
+
+The toggle item consists of:
+
+- **ToggleGroup.Item:** the root container for a toggle button.
+
+```tsx
+
+
+
+```
+
+## Example
+
+
+
+
+
+
+
+ index.tsx
+ style.css
+
+ { /* */}
+
+ ```tsx
+ import {ToggleButton} from "@kobalte/core";
+ import {BoldIcon, ItalicIcon, UnderlineIcon} from "some-icon-library";
+ import "./style.css";
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+
+
+ ```css
+ .toggle-group {
+ display: flex;
+ padding: .5rem;
+ gap: .5rem;
+ }
+
+ .toggle-group[data-orientation="vertical"] {
+ flex - direction: column;
+ }
+
+ .toggle-group__item {
+ padding: .5rem;
+ border-radius: .5rem;
+ appearance: none;
+ user-select: none;
+ outline: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition-property: background-color, color;
+ transition-duration: 150ms;
+ transition-timing-function: ease-in-out
+ }
+
+ .toggle-group__item:hover,
+ .toggle-group__item[data-pressed] {
+ background - color: hsl(200 98% 39%);
+ color: white;
+ }
+
+ .toggle-group__item:focus-visible {
+ outline: 2px solid hsl(200 98% 39%);
+ outline-offset: 2px;
+ }
+ ```
+
+
+ { /* */}
+
+
+
+## Usage
+
+### Default pressed
+
+An initial, uncontrolled value can be provided using the `defaultValue` prop.
+
+
+
+
+
+```tsx {0, 7-9}
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Controlled pressed
+
+The `value` prop can be used to make the pressed state controlled. The `onChange` event is fired when the user toggle the button, and receives the new value.
+
+
+
+
+
+```tsx {3,7}
+import { createSignal } from "solid-js";
+
+function ControlledExample() {
+ const [value, setValue] = createSignal("underline");
+
+ return (
+ <>
+
+ ...
+
+ Your text style is: {value()}.
+ >
+ );
+}
+```
+
+### Multiple selection
+
+The `multiple` prop can be used to create a select that allow multi-selection.
+
+
+
+
+
+```tsx {3,8-10,22-23,28,32,39}
+import { createSignal } from "solid-js";
+
+function MultipleSelectionExample() {
+ const [values, setValues] = createSignal(["bold", "underline"]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+## API Reference
+
+### ToggleGroup.Root
+
+| Prop | Description |
+| :----------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
+| value | `string \| string[]`
The controlled pressed state of the toggle button. |
+| defaultValue | `string \| string[]`
The default pressed state when initially rendered. Useful when you do not need to control the pressed state. |
+| onChange | `(value: string \| string[]) => void`
Event handler called when the pressed state of an item changes. |
+| multiple | `boolean`
Whether the toggle group allows multi-selection. |
+| orientation | `'horizontal' \| 'vertical'`
The orientation of the toggle group. |
+| disabled | `boolean`
Whether toggle group should be disabled. |
+
+| Data attribute | Description |
+| :---------------------------- | :----------------------------------------------------- |
+| data-orientation='horizontal' | Present when the separator has horizontal orientation. |
+| data-orientation='vertical' | Present when the separator has vertical orientation. |
+
+### ToggleGroup.Item
+
+| Prop | Description |
+| :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| value | `string`
A unique value for the item. |
+| disabled | `boolean`
Whether the item is disabled. |
+| children | `JSX.Element \| (state: ToggleButtonState) => JSX.Element`
The children of the item. Can be a `JSX.Element` or a _render prop_ for having access to the internal state. |
+
+| Render Prop | Description |
+| :---------- | :---------------------------------------------------------------------------------------- |
+| pressed | `Accessor`
Whether the toggle button is on (pressed) or off (not pressed). |
+
+| Data attribute | Description |
+| :---------------------------- | :----------------------------------------------------- |
+| data-orientation='horizontal' | Present when the separator has horizontal orientation. |
+| data-orientation='vertical' | Present when the separator has vertical orientation. |
+| data-disabled | Present when the accordion item is disabled. |
+| data-pressed | Present when the toggle button is on (pressed). |
+
+## Rendered elements
+
+| Component | Default rendered element |
+| :----------------- | :----------------------- |
+| `ToggleGroup.Root` | `div` |
+| `ToggleGroup.Item` | `button` |
+
+## Accessibility
+
+### Keyboard Interactions
+
+| Key | Description |
+| :-------------------- | :-------------------------------------------------------------------- |
+| Tab | Move focus to either the pressed item or the first item in the group. |
+| ArrowDown | If orientation is vertical, moves focus to the next item. |
+| ArrowRight | If orientation is horizontal, Moves focus to the next item. |
+| ArrowUp | If orientation is vertical, moves focus to the previous item. |
+| ArrowLeft | If orientation is vertical, moves focus to the previous item. |
+| Home | Moves focus to the first item. |
+| End | Moves focus to the last item. |
+| Enter | Activates/deactivates the item. |
+| Space | Activates/deactivates the item. |
diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx
index b4b2803f..8fef543e 100644
--- a/packages/core/src/index.tsx
+++ b/packages/core/src/index.tsx
@@ -42,4 +42,5 @@ export * as Tabs from "./tabs";
export * as TextField from "./text-field";
export * as Toast from "./toast";
export * as ToggleButton from "./toggle-button";
+export * as ToggleGroup from "./toggle-group";
export * as Tooltip from "./tooltip";
diff --git a/packages/core/src/toggle-group/index.ts b/packages/core/src/toggle-group/index.ts
new file mode 100644
index 00000000..8b79e01a
--- /dev/null
+++ b/packages/core/src/toggle-group/index.ts
@@ -0,0 +1,18 @@
+import type {
+ ToggleGroupItemOptions,
+ ToggleGroupItemProps,
+} from "./toggle-group-item";
+import { ToggleGroupItem as Item } from "./toggle-group-item";
+import type {
+ ToggleGroupRootOptions,
+ ToggleGroupRootProps,
+} from "./toggle-group-root";
+import { ToggleGroup as Root } from "./toggle-group-root";
+
+export { Item, Root };
+export type {
+ ToggleGroupItemOptions,
+ ToggleGroupItemProps,
+ ToggleGroupRootOptions,
+ ToggleGroupRootProps,
+};
diff --git a/packages/core/src/toggle-group/toggle-group-base.tsx b/packages/core/src/toggle-group/toggle-group-base.tsx
new file mode 100644
index 00000000..51587231
--- /dev/null
+++ b/packages/core/src/toggle-group/toggle-group-base.tsx
@@ -0,0 +1,150 @@
+import {
+ Orientation,
+ OverrideComponentProps,
+ composeEventHandlers,
+ createGenerateId,
+ mergeDefaultProps,
+ mergeRefs,
+} from "@kobalte/utils";
+import { createSignal, createUniqueId, splitProps } from "solid-js";
+import { useLocale } from "../i18n";
+import { createListState } from "../list";
+import { AsChildProp, Polymorphic } from "../polymorphic";
+import { CollectionItemWithRef } from "../primitives";
+import { createDomCollection } from "../primitives/create-dom-collection";
+import { SelectionMode, createSelectableCollection } from "../selection";
+import { TabsKeyboardDelegate } from "../tabs/tabs-keyboard-delegate";
+import {
+ ToggleGroupContext,
+ ToggleGroupContextValue,
+} from "./toggle-group-context";
+
+export interface ToggleGroupBaseOptions extends AsChildProp {
+ /** The controlled value of the toggle group. */
+ value?: string[];
+
+ /**
+ * The value of the select when initially rendered.
+ * Useful when you do not need to control the value.
+ */
+ defaultValue?: string[];
+
+ /** Event handler called when the value changes. */
+ onChange?: (value: string[]) => void;
+
+ /** The type of selection that is allowed in the toggle group. */
+ selectionMode?: Exclude;
+
+ /** Whether the toggle group is disabled. */
+ disabled?: boolean;
+
+ /** The axis the toggle group items should align with. */
+ orientation?: Orientation;
+}
+
+export interface ToggleGroupBaseProps
+ extends OverrideComponentProps<"div", ToggleGroupBaseOptions>,
+ AsChildProp {}
+
+export const ToggleGroupBase = (props: ToggleGroupBaseProps) => {
+ let ref: HTMLDivElement | undefined;
+
+ const defaultID = `group-${createUniqueId()}`;
+
+ const mergedProps = mergeDefaultProps(
+ {
+ id: defaultID,
+ selectionMode: "single",
+ orientation: "horizontal",
+ },
+ props,
+ );
+
+ const [local, others] = splitProps(mergedProps, [
+ "ref",
+ "value",
+ "defaultValue",
+ "disabled",
+ "orientation",
+ "selectionMode",
+ "onChange",
+ "onKeyDown",
+ "onMouseDown",
+ "onFocusIn",
+ "onFocusOut",
+ ]);
+
+ const [items, setItems] = createSignal([]);
+
+ const { DomCollectionProvider } = createDomCollection({
+ items,
+ onItemsChange: setItems,
+ });
+
+ const listState = createListState({
+ selectedKeys: () => local.value,
+ defaultSelectedKeys: () => local.defaultValue,
+ onSelectionChange: (key) => local.onChange?.(Array.from(key)),
+ disallowEmptySelection: false,
+ selectionMode: () => local.selectionMode,
+ dataSource: items,
+ });
+
+ const { direction } = useLocale();
+
+ const delegate = new TabsKeyboardDelegate(
+ () => context.listState().collection(),
+ direction,
+ () => local.orientation!,
+ );
+
+ const selectableList = createSelectableCollection(
+ {
+ selectionManager: () => listState.selectionManager(),
+ keyboardDelegate: () => delegate,
+ disallowEmptySelection: () =>
+ listState.selectionManager().disallowEmptySelection(),
+ disallowTypeAhead: true,
+ },
+ () => ref,
+ );
+
+ const context: ToggleGroupContextValue = {
+ listState: () => listState,
+ isDisabled: () => local.disabled ?? false,
+ isMultiple: () => local.selectionMode === "multiple",
+ generateId: createGenerateId(() => others.id!),
+ orientation: () => local.orientation!,
+ };
+
+ return (
+
+
+ (ref = el), local.ref)}
+ tabIndex={!local.disabled ? selectableList.tabIndex() : undefined}
+ data-orientation={local.orientation}
+ onKeyDown={composeEventHandlers([
+ local.onKeyDown,
+ selectableList.onKeyDown,
+ ])}
+ onMouseDown={composeEventHandlers([
+ local.onMouseDown,
+ selectableList.onMouseDown,
+ ])}
+ onFocusIn={composeEventHandlers([
+ local.onFocusIn,
+ selectableList.onFocusIn,
+ ])}
+ onFocusOut={composeEventHandlers([
+ local.onFocusOut,
+ selectableList.onFocusOut,
+ ])}
+ {...others}
+ />
+
+
+ );
+};
diff --git a/packages/core/src/toggle-group/toggle-group-context.tsx b/packages/core/src/toggle-group/toggle-group-context.tsx
new file mode 100644
index 00000000..c036e858
--- /dev/null
+++ b/packages/core/src/toggle-group/toggle-group-context.tsx
@@ -0,0 +1,25 @@
+import { Orientation } from "@kobalte/utils";
+import { Accessor, createContext, useContext } from "solid-js";
+import { ListState } from "../list";
+
+export interface ToggleGroupContextValue {
+ isMultiple: Accessor;
+ isDisabled: Accessor;
+ listState: Accessor;
+ generateId: (part: string) => string;
+ orientation: Accessor;
+}
+
+export const ToggleGroupContext = createContext();
+
+export function useToggleGroupContext() {
+ const context = useContext(ToggleGroupContext);
+
+ if (context === undefined) {
+ throw new Error(
+ "[kobalte]: `useToggleGroupContext` must be used within a `ToggleGroup` component",
+ );
+ }
+
+ return context;
+}
diff --git a/packages/core/src/toggle-group/toggle-group-item.tsx b/packages/core/src/toggle-group/toggle-group-item.tsx
new file mode 100644
index 00000000..2e0e2071
--- /dev/null
+++ b/packages/core/src/toggle-group/toggle-group-item.tsx
@@ -0,0 +1,115 @@
+import {
+ OverrideComponentProps,
+ callHandler,
+ composeEventHandlers,
+ mergeDefaultProps,
+ mergeRefs,
+} from "@kobalte/utils";
+import { JSX, createUniqueId, splitProps } from "solid-js";
+import { CollectionItemWithRef } from "../primitives";
+import { createDomCollectionItem } from "../primitives/create-dom-collection";
+import { createSelectableItem } from "../selection";
+import * as ToggleButton from "../toggle-button";
+import { useToggleGroupContext } from "./toggle-group-context";
+
+export interface ToggleGroupItemOptions
+ extends Omit<
+ ToggleButton.ToggleButtonRootOptions,
+ "pressed" | "defaultPressed" | "onChange"
+ > {
+ /** A string value for the toggle group item. All items within a toggle group should use a unique value. */
+ value: string;
+
+ onChange?: JSX.ChangeEventHandlerUnion;
+}
+
+export interface ToggleGroupItemProps
+ extends OverrideComponentProps<"button", ToggleGroupItemOptions> {}
+
+export const ToggleGroupItem = (props: ToggleGroupItemProps) => {
+ let ref: HTMLButtonElement | undefined;
+
+ const rootContext = useToggleGroupContext();
+
+ const defaultID = rootContext.generateId(`item-${createUniqueId()}`);
+
+ const mergedProps = mergeDefaultProps(
+ {
+ id: defaultID,
+ },
+ props,
+ );
+
+ const [local, others] = splitProps(mergedProps, [
+ "ref",
+ "value",
+ "disabled",
+ "onPointerDown",
+ "onPointerUp",
+ "onClick",
+ "onKeyDown",
+ "onMouseDown",
+ "onFocus",
+ "onChange",
+ ]);
+
+ const selectionManager = () => rootContext.listState().selectionManager();
+
+ const isDisabled = () => rootContext.isDisabled() || local.disabled;
+
+ createDomCollectionItem({
+ getItem: () => ({
+ ref: () => ref,
+ type: "item",
+ key: local.value,
+ textValue: "",
+ disabled: local.disabled! || rootContext.isDisabled(),
+ }),
+ });
+
+ const selectableItem = createSelectableItem(
+ {
+ key: () => local.value,
+ selectionManager: selectionManager,
+ disabled: local.disabled || rootContext.isDisabled(),
+ },
+ () => ref,
+ );
+
+ const onKeyDown: JSX.EventHandlerUnion = (e) => {
+ // Prevent `Enter` and `Space` default behavior which fires a click event when using a