diff --git a/packages/react-components/src/App.tsx b/packages/react-components/src/App.tsx
index e97d86dd..0b38099e 100644
--- a/packages/react-components/src/App.tsx
+++ b/packages/react-components/src/App.tsx
@@ -9,6 +9,7 @@ import useWindowDimensions from "@/hooks/useWindowDimensions";
import {
ButtonPage,
ButtonGroupPage,
+ CheckboxGroupPage,
InlineAlertPage,
SelectPage,
TagGroupPage,
@@ -150,6 +151,7 @@ function App() {
Components
+
diff --git a/packages/react-components/src/components/Checkbox/Checkbox.css b/packages/react-components/src/components/Checkbox/Checkbox.css
new file mode 100644
index 00000000..68fa032c
--- /dev/null
+++ b/packages/react-components/src/components/Checkbox/Checkbox.css
@@ -0,0 +1,118 @@
+.bcds-react-aria-Checkbox {
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ align-items: center;
+}
+
+/* Checkbox */
+.bcds-react-aria-Checkbox > .checkbox {
+ align-self: first baseline;
+ display: flex;
+ margin-top: var(--layout-margin-hair);
+ justify-content: space-around;
+ width: var(--icons-size-small);
+ height: var(--icons-size-small);
+ min-width: var(--icons-size-small);
+ background-color: var(--surface-color-secondary-default);
+ border-color: var(--surface-color-border-medium);
+ border-style: solid;
+ border-width: var(--layout-border-width-small);
+ border-radius: var(--layout-border-radius-small);
+}
+.bcds-react-aria-Checkbox > .checkbox > svg {
+ align-self: center;
+}
+
+/* Label */
+.bcds-react-aria-Checkbox > .label {
+ font: var(--typography-regular-small-body);
+ color: var(--typography-color-secondary);
+}
+
+/* Focus */
+.bcds-react-aria-Checkbox[data-focused] > .checkbox {
+ outline: solid var(--layout-border-width-medium)
+ var(--surface-color-border-active);
+ outline-offset: var(--layout-margin-hair);
+}
+
+/* Hover */
+.bcds-react-aria-Checkbox[data-hovered] > .checkbox {
+ border-color: var(--surface-color-border-dark);
+}
+.bcds-react-aria-Checkbox[data-selected][data-hovered] > .checkbox {
+ background-color: var(--surface-color-primary-hover);
+ border-color: var(--surface-color-primary-hover);
+}
+
+/* Pressed */
+.bcds-react-aria-Checkbox[data-pressed] > .checkbox {
+ background-color: var(--surface-color-primary-default);
+}
+
+/* Selected */
+.bcds-react-aria-Checkbox[data-selected] > .checkbox {
+ background-color: var(--surface-color-primary-default);
+ border-color: var(--surface-color-primary-default);
+}
+.bcds-react-aria-Checkbox[data-selected] > .checkbox > svg {
+ color: var(--icons-color-primary-invert);
+}
+
+/* Disabled */
+.bcds-react-aria-Checkbox[data-disabled] {
+ cursor: not-allowed;
+}
+.bcds-react-aria-Checkbox[data-disabled] > .checkbox {
+ border-color: var(--surface-color-border-medium);
+ background-color: var(--surface-color-primary-disabled);
+}
+.bcds-react-aria-Checkbox[data-disabled] > .label {
+ color: var(--typography-color-disabled);
+}
+.bcds-react-aria-Checkbox[data-selected][data-disabled] > .checkbox {
+ border-color: var(--surface-color-primary-disabled);
+ background-color: var(--surface-color-primary-disabled);
+}
+.bcds-react-aria-Checkbox[data-selected][data-disabled] > .checkbox > svg {
+ color: var(--icons-color-disabled);
+}
+
+/* Indeterminate */
+.bcds-react-aria-Checkbox[data-indeterminate] > .checkbox {
+ background-color: var(--surface-color-primary-default);
+ border-color: var(--surface-color-primary-default);
+}
+.bcds-react-aria-Checkbox[data-indeterminate][data-disabled] > .checkbox {
+ background-color: var(--surface-color-primary-disabled);
+ border-color: var(--surface-color-primary-disabled);
+}
+.bcds-react-aria-Checkbox[data-indeterminate][data-invalid] > .checkbox {
+ background-color: var(--surface-color-primary-danger-button-default);
+}
+.bcds-react-aria-Checkbox[data-indeterminate] > .checkbox > svg {
+ color: var(--icons-color-primary-invert);
+ align-self: center;
+ vertical-align: middle;
+}
+.bcds-react-aria-Checkbox[data-indeterminate][data-disabled] > .checkbox > svg {
+ color: var(--icons-color-disabled);
+}
+
+/* Read-only */
+.bcds-react-aria-Checkbox[data-readonly] {
+ cursor: not-allowed;
+}
+
+/* Invalid */
+.bcds-react-aria-Checkbox[data-invalid] > .checkbox {
+ border-color: var(--support-border-color-danger);
+}
+.bcds-react-aria-Checkbox[data-invalid] > .checkbox > svg {
+ color: var(--icons-color-primary-invert);
+}
+.bcds-react-aria-Checkbox[data-selected][data-invalid] > .checkbox {
+ background-color: var(--surface-color-primary-danger-button-default);
+}
diff --git a/packages/react-components/src/components/Checkbox/Checkbox.test.tsx b/packages/react-components/src/components/Checkbox/Checkbox.test.tsx
new file mode 100644
index 00000000..6c81dc81
--- /dev/null
+++ b/packages/react-components/src/components/Checkbox/Checkbox.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom"; // for matchers like toBeChecked
+
+import Checkbox from "./Checkbox";
+
+describe("Checkbox component", () => {
+ test("Checkbox renders unchecked, user clicks it, checkbox is checked", () => {
+ render(I agree);
+ const checkbox = screen.getByLabelText(/i agree/i);
+ expect(checkbox).not.toBeChecked();
+ fireEvent.click(checkbox);
+ expect(checkbox).toBeChecked();
+ });
+
+ test("Checkbox renders checked, user clicks it, checkbox is unchecked", () => {
+ render(Email me my results);
+ const checkbox = screen.getByLabelText(/email me my results/i);
+ expect(checkbox).toBeChecked();
+ fireEvent.click(checkbox);
+ expect(checkbox).not.toBeChecked();
+ });
+});
diff --git a/packages/react-components/src/components/Checkbox/Checkbox.tsx b/packages/react-components/src/components/Checkbox/Checkbox.tsx
new file mode 100644
index 00000000..5a7ac1ef
--- /dev/null
+++ b/packages/react-components/src/components/Checkbox/Checkbox.tsx
@@ -0,0 +1,32 @@
+import {
+ Checkbox as ReactAriaCheckbox,
+ CheckboxProps,
+ CheckboxRenderProps,
+} from "react-aria-components";
+
+import SvgCheckIcon from "../Icons/SvgCheckIcon";
+import SvgDashIcon from "../Icons/SvgDashIcon";
+
+import "./Checkbox.css";
+
+export default function Checkbox({ value, children, ...props }: CheckboxProps) {
+ return (
+
+ {({ isRequired, isSelected, isIndeterminate }: CheckboxRenderProps) => (
+ <>
+
+ {isSelected && !isIndeterminate && }
+ {isIndeterminate && }
+
+
+ <>{children}> {isRequired && "(required)"}
+
+ >
+ )}
+
+ );
+}
diff --git a/packages/react-components/src/components/Checkbox/index.ts b/packages/react-components/src/components/Checkbox/index.ts
new file mode 100644
index 00000000..c391828b
--- /dev/null
+++ b/packages/react-components/src/components/Checkbox/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./Checkbox";
+export type { CheckboxProps } from "./Checkbox";
diff --git a/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.css b/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.css
new file mode 100644
index 00000000..4f23aa53
--- /dev/null
+++ b/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.css
@@ -0,0 +1,48 @@
+.bcds-react-aria-CheckboxGroup {
+ display: flex;
+ flex-direction: column;
+ gap: var(--layout-margin-small);
+}
+
+/* Orientation */
+.bcds-react-aria-CheckboxGroup--options {
+ display: flex;
+}
+
+.bcds-react-aria-CheckboxGroup--options.vertical {
+ flex-direction: column;
+ gap: var(--layout-margin-small);
+}
+
+.bcds-react-aria-CheckboxGroup--options.horizontal {
+ flex-direction: row;
+ gap: var(--layout-margin-medium);
+}
+.bcds-react-aria-CheckboxGroup--options.horizontal.flex-wrap-nowrap {
+ flex-wrap: nowrap;
+}
+.bcds-react-aria-CheckboxGroup--options.horizontal.flex-wrap-wrap {
+ flex-wrap: wrap;
+}
+.bcds-react-aria-CheckboxGroup--options.horizontal.flex-wrap-wrap-reverse {
+ flex-wrap: wrap-reverse;
+}
+
+/* Parts */
+.bcds-react-aria-CheckboxGroup--label {
+ font: var(--typography-regular-small-body);
+ color: var(--typography-color-primary);
+}
+
+.bcds-react-aria-CheckboxGroup--description {
+ font: var(--typography-regular-small-body);
+ color: var(--typography-color-secondary);
+}
+
+.bcds-react-aria-CheckboxGroup--error {
+ display: inline-flex;
+ flex-direction: row;
+ gap: var(--layout-margin-xsmall);
+ font: var(--typography-regular-small-body);
+ color: var(--typography-color-danger);
+}
diff --git a/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.test.tsx b/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.test.tsx
new file mode 100644
index 00000000..41cb4796
--- /dev/null
+++ b/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom"; // for matchers like toBeChecked
+
+import CheckboxGroup from "./CheckboxGroup";
+import Checkbox from "../Checkbox/Checkbox";
+
+describe("CheckboxGroup component", () => {
+ beforeEach(() => {
+ render(
+
+ Apple
+ Orange
+ Banana
+
+ );
+ });
+
+ test("renders 3 checkboxes with middle one checked", () => {
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3);
+ expect(checkboxes[0]).not.toBeChecked();
+ expect(checkboxes[1]).toBeChecked();
+ expect(checkboxes[2]).not.toBeChecked();
+ });
+
+ test("user clicks 'orange' option, it is no longer checked", () => {
+ const checkboxOrange = screen.getByLabelText(/orange/i);
+ expect(checkboxOrange).toBeChecked();
+ fireEvent.click(checkboxOrange);
+ expect(checkboxOrange).not.toBeChecked();
+ });
+});
diff --git a/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.tsx b/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.tsx
new file mode 100644
index 00000000..aa403c22
--- /dev/null
+++ b/packages/react-components/src/components/CheckboxGroup/CheckboxGroup.tsx
@@ -0,0 +1,74 @@
+import {
+ CheckboxGroup as ReactAriaCheckboxGroup,
+ CheckboxGroupRenderProps,
+ FieldError,
+ Label,
+ Text,
+} from "react-aria-components";
+
+import type {
+ CheckboxGroupProps as ReactAriaCheckboxGroupProps,
+ ValidationResult,
+} from "react-aria-components";
+
+import "./CheckboxGroup.css";
+import SvgExclamationIcon from "../Icons/SvgExclamationIcon";
+
+export interface CheckboxGroupProps extends ReactAriaCheckboxGroupProps {
+ /* Group orientation, defaults to `vertical` */
+ orientation?: "horizontal" | "vertical";
+ /* Group label */
+ label?: string;
+ /* Group description */
+ description?: string;
+ /* Error message */
+ errorMessage?: string | ((validation: ValidationResult) => string);
+ /** `flex-wrap` style property, defaults to `nowrap` */
+ flexWrap?: "nowrap" | "wrap" | "wrap-reverse";
+}
+
+export default function CheckboxGroup({
+ orientation = "vertical",
+ label,
+ description,
+ errorMessage,
+ flexWrap = "nowrap",
+ children,
+ ...props
+}: CheckboxGroupProps) {
+ return (
+
+ {({ isRequired, isInvalid }: CheckboxGroupRenderProps) => (
+ <>
+ {label && (
+
+ )}
+
+ <>{children}>
+
+ {description && (
+
+ {description}
+
+ )}
+ {isInvalid && (
+
+
+ {errorMessage}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/packages/react-components/src/components/CheckboxGroup/index.ts b/packages/react-components/src/components/CheckboxGroup/index.ts
new file mode 100644
index 00000000..59d8af28
--- /dev/null
+++ b/packages/react-components/src/components/CheckboxGroup/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./CheckboxGroup";
+export type { CheckboxGroupProps } from "./CheckboxGroup";
diff --git a/packages/react-components/src/components/Icons/SvgCheckIcon/SvgCheckIcon.tsx b/packages/react-components/src/components/Icons/SvgCheckIcon/SvgCheckIcon.tsx
new file mode 100644
index 00000000..b50ad880
--- /dev/null
+++ b/packages/react-components/src/components/Icons/SvgCheckIcon/SvgCheckIcon.tsx
@@ -0,0 +1,18 @@
+export default function SvgCheckIcon({ id = "check-icon" }) {
+ return (
+
+ );
+}
diff --git a/packages/react-components/src/components/Icons/SvgCheckIcon/index.ts b/packages/react-components/src/components/Icons/SvgCheckIcon/index.ts
new file mode 100644
index 00000000..5726084b
--- /dev/null
+++ b/packages/react-components/src/components/Icons/SvgCheckIcon/index.ts
@@ -0,0 +1 @@
+export { default } from "./SvgCheckIcon";
diff --git a/packages/react-components/src/components/Icons/SvgDashIcon/SvgDashIcon.tsx b/packages/react-components/src/components/Icons/SvgDashIcon/SvgDashIcon.tsx
new file mode 100644
index 00000000..23a4ced6
--- /dev/null
+++ b/packages/react-components/src/components/Icons/SvgDashIcon/SvgDashIcon.tsx
@@ -0,0 +1,16 @@
+export default function SvgDashIcon({ id = "dash-icon" }) {
+ return (
+
+ );
+}
diff --git a/packages/react-components/src/components/Icons/SvgDashIcon/index.ts b/packages/react-components/src/components/Icons/SvgDashIcon/index.ts
new file mode 100644
index 00000000..1c3a68d0
--- /dev/null
+++ b/packages/react-components/src/components/Icons/SvgDashIcon/index.ts
@@ -0,0 +1 @@
+export { default } from "./SvgDashIcon";
diff --git a/packages/react-components/src/components/index.ts b/packages/react-components/src/components/index.ts
index a2efef14..7ad249f0 100644
--- a/packages/react-components/src/components/index.ts
+++ b/packages/react-components/src/components/index.ts
@@ -3,12 +3,16 @@ import "@bcgov/design-tokens/css/variables.css";
export { default as InlineAlert } from "./InlineAlert";
export { default as Button } from "./Button";
export { default as ButtonGroup } from "./ButtonGroup";
+export { default as Checkbox } from "./Checkbox";
+export { default as CheckboxGroup } from "./CheckboxGroup";
export { default as Header } from "./Header";
export { default as Footer, FooterLinks } from "./Footer";
export { default as Form } from "./Form";
export { default as Select } from "./Select";
export { default as SvgBcLogo } from "./Icons/SvgBcLogo";
+export { default as SvgCheckIcon } from "./Icons/SvgCheckIcon";
export { default as SvgCheckCircleIcon } from "./Icons/SvgCheckCircleIcon";
+export { default as SvgDashIcon } from "./Icons/SvgDashIcon";
export { default as SvgExclamationIcon } from "./Icons/SvgExclamationIcon";
export { default as SvgExclamationCircleIcon } from "./Icons/SvgExclamationCircleIcon";
export { default as SvgChevronUpIcon } from "./Icons/SvgChevronUpIcon";
diff --git a/packages/react-components/src/pages/CheckboxGroup/CheckboxGroup.tsx b/packages/react-components/src/pages/CheckboxGroup/CheckboxGroup.tsx
new file mode 100644
index 00000000..7879bd29
--- /dev/null
+++ b/packages/react-components/src/pages/CheckboxGroup/CheckboxGroup.tsx
@@ -0,0 +1,70 @@
+import { useState } from "react";
+
+import { Checkbox, CheckboxGroup } from "@/components";
+
+export default function CheckboxGroupPage() {
+ const [isSelected, setIsSelected] = useState(false);
+
+ return (
+ <>
+ Checkboxes
+
+ Checkbox 1
+ Checkbox 2
+
+ Checkbox 3 is indeterminate
+
+
+ Checkbox 4 is disabled
+
+ Checkbox 5 (same value as 6)
+ Checkbox 6 (same value as 5)
+
+ Horizontal checkboxes
+
+ Checkbox 1
+ Checkbox 2
+ Checkbox 3
+
+ Checkbox 4
+
+
+ Checkbox group with errors
+
+ Checkbox 1
+
+ Checkbox 2 is invalid
+
+ Checkbox 3
+
+ Checkbox 4
+
+
+ Controlled checkbox
+
+ setIsSelected(isSelected)}
+ >
+ Controlled
+
+
+ >
+ );
+}
diff --git a/packages/react-components/src/pages/CheckboxGroup/index.ts b/packages/react-components/src/pages/CheckboxGroup/index.ts
new file mode 100644
index 00000000..2fe15e42
--- /dev/null
+++ b/packages/react-components/src/pages/CheckboxGroup/index.ts
@@ -0,0 +1,3 @@
+import CheckboxGroupPage from "./CheckboxGroup";
+
+export default CheckboxGroupPage;
diff --git a/packages/react-components/src/pages/index.ts b/packages/react-components/src/pages/index.ts
index 1cc2919d..dc7037da 100644
--- a/packages/react-components/src/pages/index.ts
+++ b/packages/react-components/src/pages/index.ts
@@ -1,5 +1,6 @@
import ButtonPage from "./Button";
import ButtonGroupPage from "./ButtonGroup";
+import CheckboxGroupPage from "./CheckboxGroup";
import InlineAlertPage from "./InlineAlert";
import SelectPage from "./Select";
import TagGroupPage from "./TagGroup";
@@ -11,6 +12,7 @@ import TooltipPage from "./Tooltip";
export {
ButtonPage,
ButtonGroupPage,
+ CheckboxGroupPage,
InlineAlertPage,
SelectPage,
TagGroupPage,
diff --git a/packages/react-components/src/stories/Checkbox.stories.tsx b/packages/react-components/src/stories/Checkbox.stories.tsx
new file mode 100644
index 00000000..9028631e
--- /dev/null
+++ b/packages/react-components/src/stories/Checkbox.stories.tsx
@@ -0,0 +1,95 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Checkbox } from "../components";
+import { CheckboxProps } from "../components/Checkbox";
+
+const meta = {
+ title: "Components/CheckboxGroup/Checkbox",
+ component: Checkbox,
+ parameters: {
+ layout: "centered",
+ },
+ argTypes: {
+ value: {
+ control: { type: "text" },
+ description: "Unique identifier used for validation and data submission",
+ },
+ children: {
+ control: { type: "object" },
+ description: "Text label",
+ },
+ defaultSelected: {
+ control: { type: "boolean" },
+ description: "Whether a checkbox is selected by default",
+ },
+ isRequired: {
+ control: { type: "boolean" },
+ description: "Whether a checkbox is mandatory",
+ },
+ isIndeterminate: {
+ control: { type: "boolean" },
+ description: "Locks a checkbox to an 'indeterminate' state",
+ },
+ isInvalid: {
+ control: { type: "boolean" },
+ description: "Whether a checkbox is invalid",
+ },
+ isDisabled: {
+ control: { type: "boolean" },
+ description: "Disables a checkbox",
+ },
+ isReadOnly: {
+ control: { type: "boolean" },
+ description: "Locks a checkbox to its current state",
+ },
+ isSelected: {
+ control: { type: "boolean" },
+ description:
+ "For a controlled component, whether the checkbox is selected",
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const CheckboxTemplate: Story = {
+ args: { children: "This is a checkbox label" },
+ render: ({ ...args }: CheckboxProps) => ,
+};
+
+export const DefaultSelectedCheckbox: Story = {
+ args: {
+ children: "This checkbox is selected by default",
+ defaultSelected: true,
+ },
+};
+
+export const RequiredCheckbox: Story = {
+ args: {
+ children: "This checkbox is mandatory",
+ isRequired: true,
+ },
+};
+
+export const DisabledCheckbox: Story = {
+ args: {
+ children: "This checkbox is disabled",
+ isDisabled: true,
+ },
+};
+
+export const ReadOnlyCheckbox: Story = {
+ args: {
+ children: "This checkbox is set to read-only",
+ isSelected: true,
+ isReadOnly: true,
+ },
+};
+
+export const IndeterminateCheckbox: Story = {
+ args: {
+ children: "This checkbox is neither selected nor deselected",
+ isIndeterminate: true,
+ },
+};
diff --git a/packages/react-components/src/stories/CheckboxGroup.mdx b/packages/react-components/src/stories/CheckboxGroup.mdx
new file mode 100644
index 00000000..3c9cec17
--- /dev/null
+++ b/packages/react-components/src/stories/CheckboxGroup.mdx
@@ -0,0 +1,127 @@
+{/* CheckboxGroup.mdx */}
+
+import {
+ Canvas,
+ Controls,
+ Meta,
+ Primary,
+ Source,
+ Story,
+ Subtitle,
+} from "@storybook/blocks";
+
+import * as CheckboxGroupStories from "./CheckboxGroup.stories";
+import * as CheckboxStories from "./Checkbox.stories";
+
+
+
+# CheckboxGroup
+
+
+ Checkboxes enable the user to select one or more options from a list.
+
+
+
+
+## Usage and resources
+
+Learn more about working with the CheckboxGroup and Checkbox components:
+
+- [Usage and best practice guidance](https://www2.gov.bc.ca/gov/content?id=074FAD78863442AEAF2453BC18B28D2A)
+- [View the select component in Figma](https://www2.gov.bc.ca/gov/content?id=8E36BE1D10E04A17B0CD4D913FA7AC43#designers)
+
+This component is based on the React Aria [Checkbox](https://react-spectrum.adobe.com/react-aria/Checkbox.html) and [CheckboxGroup](https://react-spectrum.adobe.com/react-aria/CheckboxGroup.html) components. Consult the React Aria documentation for additional technical information.
+
+### Validation
+
+Default and custom data validation support is built in, using the [React Aria FieldError subcomponent](https://react-spectrum.adobe.com/react-aria/CheckboxGroup.html#validation).
+
+## Configuration
+
+A checkbox group comprises two components:
+
+- [Checkbox](#checkbox) defines an individual list item
+- [CheckboxGroup](#checkboxgroup-1) provides a wrapper for multiple `Checkbox` components, and handles state management and data validation
+
+Each component has its own set of supported props.
+
+## Controls
+
+### CheckboxGroup controls
+
+
+
+
+### Checkbox controls
+
+
+
+
+## Checkbox
+
+`Checkbox` renders a focusable and selectable checkbox, along with a text label (set via the `children` slot).
+
+Each checkbox should also have a unique identifier, set using the `value` prop. This is used for data validation and submission. If two checkboxes have the same `value`, selecting one will set both to `isSelected`.
+
+Use `defaultSelected` to make a checkbox selected by default:
+
+
+
+Use `isRequired` to mark a checkbox as mandatory and display a "(required)" label:
+
+
+
+### States
+
+Disable a checkbox using `isDisabled`. A disabled checkbox cannot be focused or selected:
+
+
+
+Set `isReadOnly` to lock a checkbox to its current value. A read-only checkbox can be focused, but cannot be selected:
+
+
+
+A checkbox can be put in [an 'indeterminate' (mixed/uncertain) state](https://react-spectrum.adobe.com/react-aria/Checkbox.html#indeterminate) using `isIndeterminate`:
+
+
+
+`isIndeterminate` overrides the checkbox's appearance, until it is set to `false` (for example, conditionally based on other selections.)
+
+## CheckboxGroup
+
+`CheckboxGroup` wraps multiple checkboxes into a group. It expects an array of `Checkbox` components passed as `children`.
+
+Use the `label` and `description` props to explain the purpose of a checkbox group:
+
+
+
+If you do not provide a visible `label`, you must instead pass an `aria-label` prop for use by assistive technologies.
+
+You can toggle the layout of a checkbox group between `vertical` (default) and `horizontal` using the `orientation` prop:
+
+
+
+### Required input
+
+Pass `isRequired` to make input mandatory, and display an additional "(required)" label:
+
+
+
+### Disabled state
+
+You can disable an entire checkbox group using `isDisabled`:
+
+
+
+### Validation and error handling
+
+You can [configure validation behaviour](https://react-spectrum.adobe.com/react-aria/CheckboxGroup.html#validation) at the group or individual checkbox level. Set the `isInvalid` prop to display error messages based on your app's validation logic.
+
+You can also use a [Form wrapper](/docs/utility-form-wrapper--docs) to configure validation and submission behaviour for multiple input components.
+
+
+
+`CheckboxGroup` uses the FieldError subcomponent to render error messages. You can pass custom or dynamically-generated content to the `errorMessage` slot.
diff --git a/packages/react-components/src/stories/CheckboxGroup.stories.tsx b/packages/react-components/src/stories/CheckboxGroup.stories.tsx
new file mode 100644
index 00000000..8b573507
--- /dev/null
+++ b/packages/react-components/src/stories/CheckboxGroup.stories.tsx
@@ -0,0 +1,155 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Checkbox, CheckboxGroup } from "../components";
+import { CheckboxGroupProps } from "../components/CheckboxGroup";
+
+const meta = {
+ title: "Components/CheckboxGroup",
+ component: CheckboxGroup,
+ parameters: {
+ layout: "centered",
+ },
+ argTypes: {
+ label: {
+ control: { type: "text" },
+ description: "Text label for checkbox group",
+ },
+ description: {
+ control: { type: "text" },
+ description: "Additional description or helper text",
+ },
+ orientation: {
+ control: { type: "radio" },
+ options: ["horizontal", "vertical"],
+ description: "Sets layout of checkboxes",
+ },
+ defaultValue: {
+ control: { type: "object" },
+ description:
+ "Array of values which should be selected by default (uncontrolled)",
+ },
+ errorMessage: {
+ control: { type: "object" },
+ description: "Error message displayed when input is invalid",
+ },
+ children: {
+ control: { type: "object" },
+ description: "Expects an array of `Checkbox` components",
+ },
+ isRequired: {
+ control: { type: "boolean" },
+ description: "Whether an input is mandatory or optional",
+ },
+ isInvalid: {
+ control: { type: "boolean" },
+ description: "Set when input values are invalid",
+ },
+ isDisabled: {
+ control: { type: "boolean" },
+ description: "Disables the entire checkbox group",
+ },
+ flexWrap: {
+ control: { type: "radio" },
+ options: ["nowrap", "wrap", "wrap-reverse"],
+ description: "Sets `flex-wrap` property on checkbox items",
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const CheckboxGroupTemplate: Story = {
+ args: {
+ label: "This is a checkbox group",
+ orientation: "vertical",
+ children: [
+
+ Checkbox 1
+ ,
+ Checkbox 2,
+ Checkbox 3,
+
+ Checkbox 4 is disabled
+ ,
+
+ Checkbox 5 is indeterminate
+ ,
+ Checkboxes 6 and 7 are synced,
+ Checkboxes 6 and 7 are synced,
+ ],
+ },
+ render: ({ ...args }: CheckboxGroupProps) => ,
+};
+
+export const CheckboxGroupWithLabelAndDescription: Story = {
+ args: {
+ label: "This is the primary label",
+ description: "This is an additional description field.",
+ children: [
+ Option 1,
+ Option 2,
+ Option 3,
+ ],
+ },
+};
+
+export const HorizontalCheckboxGroup: Story = {
+ args: {
+ orientation: "horizontal",
+ label: "This checkbox group is laid out horizontally",
+ children: [
+ Option 1,
+ Option 2,
+ Option 3,
+ Option 4,
+ ],
+ },
+};
+
+export const RequiredCheckboxGroup: Story = {
+ args: {
+ label: "This checkbox group requires an input",
+ isRequired: true,
+ children: [
+ Option 1,
+ Option 2,
+ Option 3,
+ Option 4,
+ ],
+ },
+};
+
+export const DisabledCheckboxGroup: Story = {
+ args: {
+ label: "This checkbox group has been disabled",
+ description: "None of the options can be focused or selected",
+ isDisabled: true,
+ children: [
+ Option 1,
+ Option 2,
+ Option 3,
+ Option 4,
+ ],
+ },
+};
+
+export const CheckboxGroupWithErrors: Story = {
+ args: {
+ orientation: "vertical",
+ label: "This checkbox group has errors",
+ description: "Description and/or helper text",
+ errorMessage: "Error messages can be customised or passed programmatically",
+ children: [
+
+ Option 1
+ ,
+ Option 2,
+ Option 3,
+ Option 4,
+ ],
+ isInvalid: true,
+ isRequired: true,
+ },
+ render: ({ ...args }: CheckboxGroupProps) => ,
+};