diff --git a/.changeset/checkbox-examples-1.md b/.changeset/checkbox-examples-1.md
new file mode 100644
index 0000000000..a1c74d49f8
--- /dev/null
+++ b/.changeset/checkbox-examples-1.md
@@ -0,0 +1,6 @@
+---
+"@ariakit/react-core": patch
+"@ariakit/react": patch
+---
+
+The `Checkbox` component now accepts `string[]` as the `value` prop. This is to conform with the native input prop type. If a string array is passed, it will be stringified, just like in the native input element. ([#2456](https://github.com/ariakit/ariakit/pull/2456))
diff --git a/.changeset/checkbox-examples-2.md b/.changeset/checkbox-examples-2.md
new file mode 100644
index 0000000000..ac8fcef8c7
--- /dev/null
+++ b/.changeset/checkbox-examples-2.md
@@ -0,0 +1,6 @@
+---
+"@ariakit/react-core": patch
+"@ariakit/react": patch
+---
+
+Fixed the `clickOnEnter` prop on `Checkbox` not working when rendering the component as a native input element. ([#2456](https://github.com/ariakit/ariakit/pull/2456))
diff --git a/components/checkbox.md b/components/checkbox.md
index 60393102bf..a7c60a1c29 100644
--- a/components/checkbox.md
+++ b/components/checkbox.md
@@ -6,13 +6,15 @@
Example
-## Installation
+## Examples
-```sh
-npm i @ariakit/react
-```
+
-Learn more on the [Getting started](/guide/getting-started) guide.
+- [](/examples/checkbox-as-button)
+- [](/examples/checkbox-custom)
+- [](/examples/checkbox-group)
+
+
## API
@@ -23,3 +25,16 @@ Learn more on the [Getting started](/guide/getting-started) guide.
<CheckboxCheck />
</Checkbox>
+
+## Related components
+
+
+
+- [](/components/button)
+- [](/components/form)
+- [](/components/menu)
+- [](/components/radio)
+- [](/components/select)
+- [](/components/command)
+
+
diff --git a/components/group.md b/components/group.md
new file mode 100644
index 0000000000..fb886e9516
--- /dev/null
+++ b/components/group.md
@@ -0,0 +1,22 @@
+# Group
+
+
+ Group related elements in a generic container that may have a label. This abstract component is based on the WAI-ARIA Group Role .
+
+
+## API
+
+
+<Group >
+ <GroupLabel />
+</Group>
+
+
+## Related components
+
+
+
+- [](/components/form)
+- [](/components/composite)
+
+
diff --git a/examples/checkbox-as-button/readme.md b/examples/checkbox-as-button/readme.md
index ba815ecaf3..496f3f7b7e 100644
--- a/examples/checkbox-as-button/readme.md
+++ b/examples/checkbox-as-button/readme.md
@@ -4,8 +4,39 @@
Rendering a custom Checkbox as a button
element in React, while keeping it accessible to screen reader and keyboard users.
+
+
+This example demonstrates the rendering of a button element. However, if you intend to render the `Checkbox` as a form control or require the preservation of native input element properties for any specific purpose, please refer to the [Custom Checkbox](/examples/checkbox-custom) example.
+
+
+
Example
+## Components
+
+
+
+- [](/components/checkbox)
+- [](/components/button)
+
+
+
+## Activating on Enter
+
+By default, native checkbox elements are activated on Space , but not on Enter . The Ariakit `Checkbox` component allows you to control this behavior using the [`clickOnEnter`](/apis/checkbox#clickonenter) and [`clickOnSpace`](/apis/checkbox#clickonspace) props. However, when rendering the `Checkbox` as any non-native input element, the `clickOnEnter` prop will be automatically set to `true`.
+
+## Reading the state
+
+In this example, we're reading the [`value`](/apis/checkbox-store#value) state from the checkbox store to render the button's text. This is done by using the selector form of the [`useState`](/apis/checkbox-store#usestate) hook:
+
+```jsx
+const label = checkbox.useState((state) =>
+ state.value ? "Checked" : "Unchecked"
+);
+```
+
+Learn more about reading the state on the [Component stores](/guide/component-stores#reading-the-state) guide.
+
## Styling
When rendering the `Checkbox` component as a non-native `input` element, the `:checked` pseudo-class is not supported. To style the checked state, use the `aria-checked` attribute selector:
@@ -13,8 +44,18 @@ When rendering the `Checkbox` component as a non-native `input` element, the `:c
```css
.button[aria-checked="true"] {
background-color: hsl(204 100% 40%);
- color: hsl(204, 20%, 100%);
+ color: hsl(204 20% 100%);
}
```
Learn more on the [Styling](/guide/styling) guide.
+
+## Related examples
+
+
+
+- [](/examples/checkbox-custom)
+- [](/examples/checkbox-group)
+- [](/examples/menu-item-checkbox)
+
+
diff --git a/examples/checkbox-as-button/site-icon.tsx b/examples/checkbox-as-button/site-icon.tsx
new file mode 100644
index 0000000000..e0e2ae6c70
--- /dev/null
+++ b/examples/checkbox-as-button/site-icon.tsx
@@ -0,0 +1,20 @@
+export default function Icon() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/examples/checkbox-as-button/style.css b/examples/checkbox-as-button/style.css
index ee52eb9911..dd1b5f064c 100644
--- a/examples/checkbox-as-button/style.css
+++ b/examples/checkbox-as-button/style.css
@@ -5,15 +5,12 @@
text-blue-900
bg-blue-200/40
hover:bg-blue-200/60
-
dark:text-blue-100
dark:bg-blue-600/25
dark:hover:bg-blue-600/40
-
aria-checked:text-white
aria-checked:bg-blue-600
aria-checked:hover:bg-blue-800
-
dark:aria-checked:text-white
dark:aria-checked:bg-blue-600
dark:aria-checked:hover:bg-blue-800
diff --git a/examples/checkbox-controlled/readme.md b/examples/checkbox-controlled/readme.md
deleted file mode 100644
index 7f9e65bbab..0000000000
--- a/examples/checkbox-controlled/readme.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Controlled Checkbox
-
-
- Using controlled props, such as checked
and onChange
, with a native Checkbox component in React.
-
-
-Example
diff --git a/examples/checkbox-custom/checkbox.tsx b/examples/checkbox-custom/checkbox.tsx
new file mode 100644
index 0000000000..1ca49c7078
--- /dev/null
+++ b/examples/checkbox-custom/checkbox.tsx
@@ -0,0 +1,48 @@
+import { forwardRef, useState } from "react";
+import type { ComponentPropsWithoutRef, ReactNode } from "react";
+import * as Ariakit from "@ariakit/react";
+
+interface CheckboxProps extends ComponentPropsWithoutRef<"input"> {
+ children?: ReactNode;
+}
+
+export const Checkbox = forwardRef(
+ function Checkbox({ children, ...props }, ref) {
+ const [checked, setChecked] = useState(props.defaultChecked ?? false);
+ const [focusVisible, setFocusVisible] = useState(false);
+ return (
+
+
+ setFocusVisible(true)}
+ onBlur={() => setFocusVisible(false)}
+ onChange={(event) => {
+ setChecked(event.target.checked);
+ props.onChange?.(event);
+ }}
+ />
+
+
+ {children}
+
+ );
+ }
+);
diff --git a/examples/checkbox-custom/index.tsx b/examples/checkbox-custom/index.tsx
index 99c5cf851a..debbb62df4 100644
--- a/examples/checkbox-custom/index.tsx
+++ b/examples/checkbox-custom/index.tsx
@@ -1,24 +1,6 @@
-import { useState } from "react";
-import * as Ariakit from "@ariakit/react";
+import { Checkbox } from "./checkbox.jsx";
import "./style.css";
export default function Example() {
- const checkbox = Ariakit.useCheckboxStore({ defaultValue: false });
- const [focusVisible, setFocusVisible] = useState(false);
- const checked = checkbox.useState("value");
- return (
-
-
- setFocusVisible(true)}
- onBlur={() => setFocusVisible(false)}
- />
-
-
- I have read and agree to the terms and conditions
-
- );
+ return Ariakit ;
}
diff --git a/examples/checkbox-custom/readme.md b/examples/checkbox-custom/readme.md
index f86c6318c0..776fa34cbf 100644
--- a/examples/checkbox-custom/readme.md
+++ b/examples/checkbox-custom/readme.md
@@ -5,3 +5,22 @@
Example
+
+## Components
+
+
+
+- [](/components/checkbox)
+- [](/components/visually-hidden)
+
+
+
+## Related examples
+
+
+
+- [](/examples/checkbox-as-button)
+- [](/examples/checkbox-group)
+- [](/examples/menu-item-checkbox)
+
+
diff --git a/examples/checkbox-custom/site-icon.tsx b/examples/checkbox-custom/site-icon.tsx
new file mode 100644
index 0000000000..738e9afe64
--- /dev/null
+++ b/examples/checkbox-custom/site-icon.tsx
@@ -0,0 +1,23 @@
+export default function Icon() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/examples/checkbox-custom/style.css b/examples/checkbox-custom/style.css
index fca98e5d02..040e6fa542 100644
--- a/examples/checkbox-custom/style.css
+++ b/examples/checkbox-custom/style.css
@@ -1,19 +1,42 @@
-@import url("../checkbox/style.css");
-
.checkbox {
@apply
- w-5
- h-5
- rounded
flex
- justify-center
items-center
- border
- text-blue-900
- bg-blue-200/40
- border-blue-600
- dark:text-blue-100
- dark:bg-blue-600/25
- dark:border-blue-200/40
+ gap-2
+ p-4
+ pr-6
+ rounded-lg
+ select-none
+ border-2
+ border-black/30
+ dark:border-white/30
+ data-[checked=true]:border-blue-600
+ data-[checked=true]:dark:border-blue-600
+ bg-white
+ dark:bg-gray-700
+ shadow
+ dark:shadow-dark
+ data-[focus-visible]:outline
+ data-[focus-visible]:outline-4
+ data-[focus-visible]:outline-blue-600/25
+ ;
+}
+
+.check {
+ @apply
+ block
+ rounded-full
+ p-0.5
+ text-lg
+ bg-gray-150
+ dark:bg-gray-850
+ [border:inherit]
+ [stroke-dasharray:15]
+ [stroke-dashoffset:15]
+ data-[checked=true]:bg-blue-600
+ data-[checked=true]:dark:bg-blue-600
+ data-[checked=true]:text-white
+ data-[checked=true]:[stroke-dashoffset:0]
+ transition-[stroke-dashoffset]
;
}
diff --git a/examples/checkbox-custom/test.ts b/examples/checkbox-custom/test.ts
index 67532d8dac..8098077dbc 100644
--- a/examples/checkbox-custom/test.ts
+++ b/examples/checkbox-custom/test.ts
@@ -1,18 +1,27 @@
import { click, getByRole, press } from "@ariakit/test";
test("check/uncheck on click", async () => {
- expect(getByRole("checkbox")).not.toBeChecked();
- await click(getByRole("checkbox"));
expect(getByRole("checkbox")).toBeChecked();
await click(getByRole("checkbox"));
expect(getByRole("checkbox")).not.toBeChecked();
+ await click(getByRole("checkbox"));
+ expect(getByRole("checkbox")).toBeChecked();
});
test("check/uncheck on space", async () => {
- expect(getByRole("checkbox")).not.toBeChecked();
+ expect(getByRole("checkbox")).toBeChecked();
await press.Tab();
await press.Space();
- expect(getByRole("checkbox")).toBeChecked();
+ expect(getByRole("checkbox")).not.toBeChecked();
await press.Space();
+ expect(getByRole("checkbox")).toBeChecked();
+});
+
+test("check/uncheck on enter", async () => {
+ expect(getByRole("checkbox")).toBeChecked();
+ await press.Tab();
+ await press.Enter();
expect(getByRole("checkbox")).not.toBeChecked();
+ await press.Enter();
+ expect(getByRole("checkbox")).toBeChecked();
});
diff --git a/examples/checkbox-group/index.tsx b/examples/checkbox-group/index.tsx
index 6db7f9c98d..736173c596 100644
--- a/examples/checkbox-group/index.tsx
+++ b/examples/checkbox-group/index.tsx
@@ -1,27 +1,20 @@
-import * as Ariakit from "@ariakit/react";
+import { Checkbox, Group, GroupLabel, useCheckboxStore } from "@ariakit/react";
import "./style.css";
export default function Example() {
- const checkbox = Ariakit.useCheckboxStore({ defaultValue: [] });
+ const checkbox = useCheckboxStore({ defaultValue: [] });
return (
-
- Your favorite fruits
+
+ Your favorite fruits
- {" "}
- Apple
+ Apple
- {" "}
- Orange
+ Orange
- {" "}
- Mango
+ Mango
-
+
);
}
diff --git a/examples/checkbox-group/readme.md b/examples/checkbox-group/readme.md
index bee8ed929f..c0b30ca58a 100644
--- a/examples/checkbox-group/readme.md
+++ b/examples/checkbox-group/readme.md
@@ -5,3 +5,22 @@
Example
+
+## Components
+
+
+
+- [](/components/checkbox)
+- [](/components/group)
+
+
+
+## Related examples
+
+
+
+- [](/examples/checkbox-as-button)
+- [](/examples/checkbox-custom)
+- [](/examples/menu-item-checkbox)
+
+
diff --git a/examples/checkbox-group/site-icon.tsx b/examples/checkbox-group/site-icon.tsx
new file mode 100644
index 0000000000..cdfdd03ba5
--- /dev/null
+++ b/examples/checkbox-group/site-icon.tsx
@@ -0,0 +1,31 @@
+function renderLine(checked = false) {
+ return (
+
+ );
+}
+
+export default function Icon() {
+ return (
+
+
+
+ {renderLine(true)}
+ {renderLine()}
+ {renderLine(true)}
+
+
+
+ );
+}
diff --git a/examples/checkbox-store/readme.md b/examples/checkbox-store/readme.md
deleted file mode 100644
index 132a1fec53..0000000000
--- a/examples/checkbox-store/readme.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# useCheckboxStore
-
-
- Using the useCheckboxStore
hook to control the state of the Checkbox component.
-
-
-Example
diff --git a/examples/checkbox/site-icon.tsx b/examples/checkbox/site-icon.tsx
index b5b8f607b3..b783744a52 100644
--- a/examples/checkbox/site-icon.tsx
+++ b/examples/checkbox/site-icon.tsx
@@ -8,7 +8,7 @@ export default function Icon() {
fill="none"
viewBox="0 0 24 24"
strokeWidth={4}
- className="h-8 w-8 stroke-white dark:stroke-white/90"
+ className="h-8 w-8 stroke-white"
>
diff --git a/examples/checkbox/style.css b/examples/checkbox/style.css
index c653f2eeda..8a4ff5985a 100644
--- a/examples/checkbox/style.css
+++ b/examples/checkbox/style.css
@@ -9,6 +9,6 @@
.checkbox {
@apply
- focus-visible:ariakit-outline
+ data-[focus-visible]:outline-blue-600
;
}
diff --git a/guide/200-styling/readme.md b/guide/200-styling/readme.md
index 61eaa41380..b9ae062028 100644
--- a/guide/200-styling/readme.md
+++ b/guide/200-styling/readme.md
@@ -26,6 +26,17 @@ To safely use CSS selectors, utilize the `className` prop or provide your own `d
+### `[aria-checked]`
+
+The `aria-checked` attribute is applied to the [Checkbox](/components/checkbox) component when the input is checked. It can be either `true` or `false`.
+
+```css
+.checkbox[aria-checked="true"] {
+ background-color: hsl(204 100% 40%);
+ color: hsl(204 20% 100%);
+}
+```
+
### `[aria-disabled]`
Not all HTML elements accept the `disabled` attribute. That's why the [Focusable](/components/focusable) component and all components that use it underneath will apply the `aria-disabled` attribute to the rendered element when the `disabled` prop is set to `true`.
diff --git a/packages/ariakit-react-core/src/checkbox/checkbox.tsx b/packages/ariakit-react-core/src/checkbox/checkbox.tsx
index 9339bbcd6e..06b268227f 100644
--- a/packages/ariakit-react-core/src/checkbox/checkbox.tsx
+++ b/packages/ariakit-react-core/src/checkbox/checkbox.tsx
@@ -1,4 +1,4 @@
-import type { ChangeEvent, MouseEvent } from "react";
+import type { ChangeEvent, InputHTMLAttributes, MouseEvent } from "react";
import { useEffect, useRef, useState } from "react";
import type { CommandOptions } from "../command/command.js";
import { useCommand } from "../command/command.js";
@@ -26,6 +26,13 @@ function isNativeCheckbox(tagName?: string, type?: string) {
return tagName === "input" && (!type || type === "checkbox");
}
+function getNonArrayValue(value: T) {
+ if (Array.isArray(value)) {
+ return value.toString();
+ }
+ return value as Exclude;
+}
+
/**
* Returns props to create a `Checkbox` component. If the element is not a
* native checkbox, the hook will return additional props to make sure it's
@@ -48,8 +55,11 @@ export const useCheckbox = createHook(
const storeChecked = useStoreState(store, (state) => {
if (checkedProp !== undefined) return checkedProp;
if (state.value === undefined) return;
- if (valueProp) {
- if (Array.isArray(state.value)) return state.value.includes(valueProp);
+ if (valueProp != null) {
+ if (Array.isArray(state.value)) {
+ const nonArrayValue = getNonArrayValue(valueProp);
+ return state.value.includes(nonArrayValue);
+ }
return state.value === valueProp;
}
if (Array.isArray(state.value)) return false;
@@ -94,12 +104,13 @@ export const useCheckbox = createHook(
setChecked(elementChecked);
store?.setValue((prevValue) => {
- if (!valueProp) return elementChecked;
+ if (valueProp == null) return elementChecked;
+ const nonArrayValue = getNonArrayValue(valueProp);
if (!Array.isArray(prevValue)) {
- return prevValue === valueProp ? false : valueProp;
+ return prevValue === nonArrayValue ? false : nonArrayValue;
}
- if (elementChecked) return [...prevValue, valueProp];
- return prevValue.filter((v) => v !== valueProp);
+ if (elementChecked) return [...prevValue, nonArrayValue];
+ return prevValue.filter((v) => v !== nonArrayValue);
});
});
@@ -191,7 +202,7 @@ export interface CheckboxOptions
*
* ```
*/
- value?: string | number;
+ value?: InputHTMLAttributes["value"];
/**
* The default `checked` state of the checkbox. This prop is ignored if the
* `checked` or the `store` props are provided.
diff --git a/packages/ariakit-react-core/src/command/command.ts b/packages/ariakit-react-core/src/command/command.ts
index 50c31c2fdc..cb65664195 100644
--- a/packages/ariakit-react-core/src/command/command.ts
+++ b/packages/ariakit-react-core/src/command/command.ts
@@ -17,14 +17,22 @@ function isNativeClick(event: KeyboardEvent) {
if (!event.isTrusted) return false;
// istanbul ignore next: can't test trusted events yet
const element = event.currentTarget;
- return (
- isButton(element) ||
- element.tagName === "SUMMARY" ||
- element.tagName === "INPUT" ||
- element.tagName === "TEXTAREA" ||
- element.tagName === "A" ||
- element.tagName === "SELECT"
- );
+ if (event.key === "Enter") {
+ return (
+ isButton(element) ||
+ element.tagName === "SUMMARY" ||
+ element.tagName === "A"
+ );
+ }
+ if (event.key === " ") {
+ return (
+ isButton(element) ||
+ element.tagName === "SUMMARY" ||
+ element.tagName === "INPUT" ||
+ element.tagName === "SELECT"
+ );
+ }
+ return false;
}
/**
diff --git a/website/app/(main)/[category]/[page]/page.tsx b/website/app/(main)/[category]/[page]/page.tsx
index 43f3cef2e1..59d73b77df 100644
--- a/website/app/(main)/[category]/[page]/page.tsx
+++ b/website/app/(main)/[category]/[page]/page.tsx
@@ -91,6 +91,7 @@ const style = {
underline-offset-[0.25em]
font-medium dark:font-normal
text-blue-700 dark:text-blue-400
+ [&>code]:text-blue-900 [&>code]:dark:text-blue-300
`,
h1: tw`
text-2xl sm:text-4xl md:text-5xl font-extrabold dark:font-bold
@@ -130,7 +131,7 @@ const style = {
data-[type="warn"]:before:bg-amber-500
data-[type="warn"]:before:dark:bg-yellow-600
- data-[type="note"]:bg-blue-50
+ data-[type="note"]:bg-blue-50/70
data-[type="note"]:dark:bg-blue-900/20
data-[type="note"]:before:bg-blue-600
`,
@@ -153,8 +154,8 @@ const style = {
paragraph: tw`
dark:text-white/[85%] leading-7 tracking-[-0.016em] dark:tracking-[-0.008em]
- [p&_code]:rounded [p&_code]:p-1 [p&_code]:text-[0.9375em]
- [p&_code]:bg-black/[6.5%] dark:[p&_code]:bg-white/[6.5%]
+ [p&_code]:rounded [p&_code]:p-[0.25em] [p&_code]:text-[0.9375em]
+ [p&_code]:bg-black/[7.5%] dark:[p&_code]:bg-white/[7.5%]
[p&_code]:font-monospace
`,
strong: tw`
@@ -327,18 +328,9 @@ export default async function Page({ params }: PageProps) {
const nextPageLink = nextPage && (
-
+
{getPageIcon(nextPage.category, nextPage.slug) || }
@@ -423,21 +415,23 @@ export default async function Page({ params }: PageProps) {
))}
{isExamples && (
-
- View all{isComponentPage ? ` ${pageDetail?.title}` : ""}{" "}
- examples
-
+
+
+ View all
+ {isComponentPage ? ` ${pageDetail?.title}` : ""}{" "}
+ examples
+
+
)}
{isComponents && (
-
- View all components
-
+
+
+ View all components
+
+
)}
>
);
@@ -565,6 +559,15 @@ export default async function Page({ params }: PageProps) {
if (!child.props.href) return paragraph;
return <>{props.children}>;
},
+ kbd: ({ node, ...props }) => (
+
+ ),
strong: ({ node, ...props }) => (