Skip to content
Lenso UI

CheckboxGroup

A checkbox group component for managing multiple checkbox selections

Usage

import { CheckboxGroup, Checkbox, Label, Description } from '@lenso/ui';
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { checkboxSupportingStyles } from "@lenso/tokens/checkbox";import { useId } from "react";import * as stylex from "@stylexjs/stylex";
const interests = [  { value: "coding", label: "Coding", description: "Love building software" },  { value: "design", label: "Design", description: "Enjoy creating beautiful interfaces" },  { value: "writing", label: "Writing", description: "Passionate about content creation" },];export function Basic() {  const id = useId();  return (    <TextField name="interests">      <CheckboxGroup aria-labelledby={`${id}-label`} aria-describedby={`${id}-help`}>        <span id={`${id}-label`} {...stylex.props(labelStyles.label)}>          Select your interests        </span>        <span id={`${id}-help`} {...stylex.props(descriptionStyles.description)}>          Choose all that apply        </span>        {interests.map((interest) => (          <Checkbox            key={interest.value}            value={interest.value}            aria-label={interest.label}            aria-describedby={`${id}-${interest.value}`}          >            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              {interest.label}            </Checkbox.Content>            <span              id={`${id}-${interest.value}`}              {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}            >              {interest.description}            </span>          </Checkbox>        ))}      </CheckboxGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import {CheckboxGroup, Checkbox, Label, Description, FieldError} from '@lenso/ui';
export default () => (  <CheckboxGroup name="interests">    <Label />    <Description /> {/* Optional */}    <Checkbox value="option1">      <Checkbox.Content>        <Checkbox.Control>          <Checkbox.Indicator />        </Checkbox.Control>        Label {/* plain text — the clickable label */}      </Checkbox.Content>      <Description /> {/* Optional per-checkbox help text */}    </Checkbox>    <FieldError /> {/* Optional */}  </CheckboxGroup>);

Examples

In Surface

When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, Surface, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { checkboxSupportingStyles } from "@lenso/tokens/checkbox";import { useId } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  surface: { width: "100%", borderRadius: "1.5rem", padding: "1.5rem" },});export function OnSurface() {  const labelId = useId();  return (    <Surface xstyle={styles.surface}>      <TextField name="interests">        <CheckboxGroup          variant="secondary"          aria-labelledby={labelId}          aria-describedby={`${labelId}-help`}        >          <span id={labelId} {...stylex.props(labelStyles.label)}>            Select your interests          </span>          <span id={`${labelId}-help`} {...stylex.props(descriptionStyles.description)}>            Choose all that apply          </span>          <Checkbox value="coding" aria-label="Coding" aria-describedby={`${labelId}-coding`}>            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Coding            </Checkbox.Content>            <span              id={`${labelId}-coding`}              {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}            >              Love building software            </span>          </Checkbox>          <Checkbox value="design" aria-label="Design" aria-describedby={`${labelId}-design`}>            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Design            </Checkbox.Content>            <span              id={`${labelId}-design`}              {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}            >              Enjoy creating beautiful interfaces            </span>          </Checkbox>          <Checkbox value="writing" aria-label="Writing" aria-describedby={`${labelId}-writing`}>            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Writing            </Checkbox.Content>            <span              id={`${labelId}-writing`}              {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}            >              Passionate about content creation            </span>          </Checkbox>        </CheckboxGroup>      </TextField>    </Surface>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Disabled

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { checkboxSupportingStyles } from "@lenso/tokens/checkbox";import { useId } from "react";import * as stylex from "@stylexjs/stylex";export function Disabled() {  const labelId = useId();  return (    <TextField name="disabled-features">      <CheckboxGroup disabled aria-labelledby={labelId} aria-describedby={`${labelId}-help`}>        <span id={labelId} {...stylex.props(labelStyles.label)}>          Features        </span>        <span id={`${labelId}-help`} {...stylex.props(descriptionStyles.description)}>          Feature selection is temporarily disabled        </span>        <Checkbox value="feature1" aria-label="Feature 1" aria-describedby={`${labelId}-feature1`}>          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Feature 1          </Checkbox.Content>          <span            id={`${labelId}-feature1`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            This feature is coming soon          </span>        </Checkbox>        <Checkbox value="feature2" aria-label="Feature 2" aria-describedby={`${labelId}-feature2`}>          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Feature 2          </Checkbox.Content>          <span            id={`${labelId}-feature2`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            This feature is coming soon          </span>        </Checkbox>      </CheckboxGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Indeterminate

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  children: { marginInlineStart: "1.5rem", display: "flex", flexDirection: "column", gap: ".5rem" },});export function Indeterminate() {  const [selected, setSelected] = useState(["coding"]);  const allOptions = ["coding", "design", "writing"];  return (    <div>      <Checkbox        indeterminate={selected.length > 0 && selected.length < allOptions.length}        checked={selected.length === allOptions.length}        name="select-all"        onCheckedChange={(checked) => setSelected(checked ? allOptions : [])}      >        <Checkbox.Content>          <Checkbox.Control>            <Checkbox.Indicator />          </Checkbox.Control>          Select all        </Checkbox.Content>      </Checkbox>      <div {...stylex.props(styles.children)}>        <CheckboxGroup value={selected} onValueChange={setSelected}>          <Checkbox value="coding">            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Coding            </Checkbox.Content>          </Checkbox>          <Checkbox value="design">            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Design            </Checkbox.Content>          </Checkbox>          <Checkbox value="writing">            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Writing            </Checkbox.Content>          </Checkbox>        </CheckboxGroup>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Controlled

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { useId, useState } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { minWidth: "320px" },  summary: { marginBlock: "1rem", fontSize: ".875rem", color: "var(--muted)" },});export function Controlled() {  const labelId = useId();  const [selected, setSelected] = useState(["coding", "design"]);  return (    <TextField name="skills">      <CheckboxGroup        aria-labelledby={labelId}        xstyle={styles.root}        value={selected}        onValueChange={setSelected}      >        <span id={labelId} {...stylex.props(labelStyles.label)}>          Your skills        </span>        <Checkbox value="coding">          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Coding          </Checkbox.Content>        </Checkbox>        <Checkbox value="design">          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Design          </Checkbox.Content>        </Checkbox>        <Checkbox value="writing">          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Writing          </Checkbox.Content>        </Checkbox>        <p {...stylex.props(styles.summary)}>Selected: {selected.join(", ") || "None"}</p>      </CheckboxGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Validation

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Button, Checkbox, CheckboxGroup, FieldError, Form, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { useId } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  form: { display: "flex", flexDirection: "column", gap: "1rem", paddingInline: "1rem" },});export function Validation() {  const labelId = useId();  return (    <Form      xstyle={styles.form}      onSubmit={(e) => {        e.preventDefault();        const values = new FormData(e.currentTarget).getAll("preferences");        alert(`Selected preferences: ${values.join(", ")}`);      }}    >      <TextField        name="preferences"        validate={(value) =>          Array.isArray(value) && value.length > 0            ? null            : "Please select at least one notification method."        }      >        <CheckboxGroup aria-labelledby={labelId}>          <span id={labelId} {...stylex.props(labelStyles.label)}>            Preferences          </span>          <Checkbox value="email">            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Email notifications            </Checkbox.Content>          </Checkbox>          <Checkbox value="sms">            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              SMS notifications            </Checkbox.Content>          </Checkbox>          <Checkbox value="push">            <Checkbox.Content>              <Checkbox.Control>                <Checkbox.Indicator />              </Checkbox.Control>              Push notifications            </Checkbox.Content>          </Checkbox>        </CheckboxGroup>        <FieldError>Please select at least one notification method.</FieldError>      </TextField>      <Button type="submit">Submit</Button>    </Form>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Features and Add-ons Example

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Bell, Comment, Envelope } from "@gravity-ui/icons";import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { useId } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  wrapper: {    display: "flex",    width: "100%",    flexDirection: "column",    alignItems: "center",    gap: "2.5rem",    paddingInline: "1rem",    paddingBlock: "2rem",  },  section: {    display: "flex",    width: "100%",    minWidth: "320px",    flexDirection: "column",    gap: "1rem",  },  items: { display: "flex", flexDirection: "column", gap: ".5rem" },  content: {    position: "relative",    display: "flex",    width: "100%",    flexDirection: "row",    alignItems: "flex-start",    justifyContent: "flex-start",    gap: "1rem",    borderRadius: "1.5rem",    backgroundColor: {      default: "var(--surface)",      ":is([data-checked] *)": "color-mix(in oklab, var(--accent) 10%, transparent)",    },    paddingInline: "1.25rem",    paddingBlock: "1rem",    transition: {      default: "background-color 150ms ease",      "@media (prefers-reduced-motion: reduce)": "none",    },  },  control: {    position: "absolute",    insetInlineEnd: "1rem",    top: ".75rem",    width: "1.25rem",    height: "1.25rem",    borderRadius: "9999px",    "::before": { borderRadius: "9999px" },  },  icon: { width: "1.25rem", height: "1.25rem", color: "var(--accent-soft-foreground)" },  copy: { display: "flex", flexDirection: "column", gap: ".25rem" },});export function FeaturesAndAddOns() {  const labelId = useId();  const addOns = [    {      description: "Receive updates via email",      icon: Envelope,      title: "Email Notifications",      value: "email",    },    {      description: "Get instant SMS notifications",      icon: Comment,      title: "SMS Alerts",      value: "sms",    },    {      description: "Browser and mobile push alerts",      icon: Bell,      title: "Push Notifications",      value: "push",    },  ];  return (    <div {...stylex.props(styles.wrapper)}>      <section {...stylex.props(styles.section)}>        <TextField name="notification-preferences">          <CheckboxGroup aria-labelledby={labelId} aria-describedby={`${labelId}-help`}>            <span id={labelId} {...stylex.props(labelStyles.label)}>              Notification preferences            </span>            <span id={`${labelId}-help`} {...stylex.props(descriptionStyles.description)}>              Choose how you want to receive updates            </span>            <div {...stylex.props(styles.items)}>              {addOns.map((addon) => (                <Checkbox                  key={addon.value}                  value={addon.value}                  variant="secondary"                  aria-label={addon.title}                  aria-describedby={`${labelId}-${addon.value}`}                >                  <Checkbox.Content xstyle={styles.content}>                    <Checkbox.Control xstyle={styles.control}>                      <Checkbox.Indicator />                    </Checkbox.Control>                    <addon.icon {...stylex.props(styles.icon)} aria-hidden="true" />                    <div {...stylex.props(styles.copy)}>                      <span>{addon.title}</span>                      <span                        id={`${labelId}-${addon.value}`}                        {...stylex.props(descriptionStyles.description)}                      >                        {addon.description}                      </span>                    </div>                  </Checkbox.Content>                </Checkbox>              ))}            </div>          </CheckboxGroup>        </TextField>      </section>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Custom Indicator

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { checkboxSupportingStyles } from "@lenso/tokens/checkbox";import { useId } from "react";import * as stylex from "@stylexjs/stylex";export function WithCustomIndicator() {  const labelId = useId();  return (    <TextField name="features">      <CheckboxGroup aria-labelledby={labelId} aria-describedby={`${labelId}-help`}>        <span id={labelId} {...stylex.props(labelStyles.label)}>          Features        </span>        <span id={`${labelId}-help`} {...stylex.props(descriptionStyles.description)}>          Select the features you want        </span>        <Checkbox          value="notifications"          aria-label="Email notifications"          aria-describedby={`${labelId}-notifications`}        >          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator                render={(props, { checked }) => (                  <span {...props}>                    {checked ? (                      <svg                        aria-hidden="true"                        fill="none"                        stroke="currentColor"                        strokeLinecap="round"                        strokeWidth={2}                        viewBox="0 0 24 24"                      >                        <path d="M6 18L18 6M6 6l12 12" />                      </svg>                    ) : null}                  </span>                )}              />            </Checkbox.Control>            Email notifications          </Checkbox.Content>          <span            id={`${labelId}-notifications`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            Receive updates via email          </span>        </Checkbox>        <Checkbox          value="newsletter"          aria-label="Newsletter"          aria-describedby={`${labelId}-newsletter`}        >          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator                render={(props, { checked }) => (                  <span {...props}>                    {checked ? (                      <svg                        aria-hidden="true"                        fill="none"                        stroke="currentColor"                        strokeLinecap="round"                        strokeWidth={2}                        viewBox="0 0 24 24"                      >                        <path d="M6 18L18 6M6 6l12 12" />                      </svg>                    ) : null}                  </span>                )}              />            </Checkbox.Control>            Newsletter          </Checkbox.Content>          <span            id={`${labelId}-newsletter`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            Get weekly newsletters          </span>        </Checkbox>      </CheckboxGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Render Function

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { checkboxSupportingStyles } from "@lenso/tokens/checkbox";import { useId } from "react";import * as stylex from "@stylexjs/stylex";export function RenderFunction() {  const labelId = useId();  return (    <TextField name="interests">      <CheckboxGroup        aria-labelledby={labelId}        aria-describedby={`${labelId}-help`}        render={(props) => <div {...props} data-custom="foo" />}      >        <span id={labelId} {...stylex.props(labelStyles.label)}>          Select your interests        </span>        <span id={`${labelId}-help`} {...stylex.props(descriptionStyles.description)}>          Choose all that apply        </span>        <Checkbox value="coding" aria-label="Coding" aria-describedby={`${labelId}-coding`}>          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Coding          </Checkbox.Content>          <span            id={`${labelId}-coding`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            Love building software          </span>        </Checkbox>        <Checkbox value="design" aria-label="Design" aria-describedby={`${labelId}-design`}>          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Design          </Checkbox.Content>          <span            id={`${labelId}-design`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            Enjoy creating beautiful interfaces          </span>        </Checkbox>        <Checkbox value="writing" aria-label="Writing" aria-describedby={`${labelId}-writing`}>          <Checkbox.Content>            <Checkbox.Control>              <Checkbox.Indicator />            </Checkbox.Control>            Writing          </Checkbox.Content>          <span            id={`${labelId}-writing`}            {...stylex.props(descriptionStyles.description, checkboxSupportingStyles.direct)}          >            Passionate about content creation          </span>        </Checkbox>      </CheckboxGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Customization

Tailwind CSS

"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox, CheckboxGroup, TextField } from "@lenso/ui";import { labelStyles } from "@lenso/tokens/label";import { descriptionStyles } from "@lenso/tokens/description";import { useId } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { gap: ".75rem" },  item: { marginTop: 0 },  control: {    backgroundColor: "var(--success-soft)",    "::before": {      backgroundColor: {        default: "var(--success)",        ":is([data-slot='checkbox']:hover *)": "var(--success)",        ":is([data-slot='checkbox'][data-invalid] *)": "var(--success)",      },    },  },  indicator: { color: "var(--success-foreground)" },});const channels = [  { label: "Email", value: "email" },  { label: "SMS", value: "sms" },  { label: "Push", value: "push" },] as const;export function CustomStyles() {  const labelId = useId();  return (    <TextField name="notification-channels">      <CheckboxGroup        aria-labelledby={labelId}        aria-describedby={`${labelId}-help`}        xstyle={styles.root}        defaultValue={["email"]}      >        <span id={labelId} {...stylex.props(labelStyles.label)}>          Notification channels        </span>        <span id={`${labelId}-help`} {...stylex.props(descriptionStyles.description)}>          Choose how we should reach you for account updates.        </span>        {channels.map(({ label, value }) => (          <Checkbox key={value} value={value} xstyle={styles.item}>            <Checkbox.Content>              <Checkbox.Control xstyle={styles.control}>                <Checkbox.Indicator xstyle={styles.indicator} />              </Checkbox.Control>              {label}            </Checkbox.Content>          </Checkbox>        ))}      </CheckboxGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

To customize the CheckboxGroup component classes, you can use the @layer components directive. Learn more.

@layer components {  .checkbox-group {    @apply flex flex-col gap-2;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

The CheckboxGroup component uses these CSS classes (View source styles):

Base Classes [!toc]

  • .checkbox-group - Base checkbox group container

API Reference

CheckboxGroup

Inherits from React Aria CheckboxGroup.

PropTypeDefaultDescription
valuestring[]-The current selected values (controlled)
defaultValuestring[]-The default selected values (uncontrolled)
onChange(value: string[]) => void-Handler called when the selected values change
isDisabledbooleanfalseWhether the checkbox group is disabled
isRequiredbooleanfalseWhether the checkbox group is required
isReadOnlybooleanfalseWhether the checkbox group is read only
isInvalidbooleanfalseWhether the checkbox group is in an invalid state
namestring-The name of the checkbox group, used when submitting an HTML form
childrenReact.ReactNode | (values: CheckboxGroupRenderProps) => React.ReactNode-Checkbox group content or render prop
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, CheckboxGroupRenderProps>-Overrides the default DOM element with a custom render function.

Render Props

When using the render prop pattern, these values are provided:

PropTypeDescription
valuestring[]The currently selected values
isDisabledbooleanWhether the checkbox group is disabled
isReadOnlybooleanWhether the checkbox group is read only
isInvalidbooleanWhether the checkbox group is in an invalid state
isRequiredbooleanWhether the checkbox group is required