Skip to main content
Nexus Design System
GitHub

Checkbox

Checkbox selects one or more options, or toggles one setting. Its indeterminate state marks a partly selected group. Pair it with Label, or use ChoiceRow or ChoiceCard for richer option rows.

'use client';

import * as React from 'react';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import { Label } from '@/components/nxds/components/label/label';

export default function CheckboxWithLabel() {
  const termsId = React.useId();

  return (
    <div className="nx:flex nx:items-center nx:gap-2">
      <Checkbox id={termsId} />
      <Label htmlFor={termsId}>Accept terms and conditions</Label>
    </div>
  );
}

This example also needs: Label.

Installation

npx nxds-cli@latest add checkbox

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';

Examples

Checked

Start checked with defaultChecked.

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';

export default function CheckboxChecked() {
  return <Checkbox aria-label="Checked option" defaultChecked />;
}

Indeterminate

Show a minus for a partly selected group with checked="indeterminate".

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';

export default function CheckboxIndeterminate() {
  return <Checkbox aria-label="Indeterminate option" checked="indeterminate" />;
}

Disabled

Disable the checkbox so it cannot be toggled.

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';

export default function CheckboxDisabled() {
  return <Checkbox aria-label="Disabled checkbox" disabled />;
}

Props

Checkbox

Checkbox adds no props of its own; it accepts the props of the element or primitive it wraps.

Checkbox selects one or more options, or toggles one setting. Its indeterminate state marks a partly selected group. Pair it with Label, or use ChoiceRow or ChoiceCard for richer option rows.

<script setup lang="ts">
import { useId } from 'vue';

import { Checkbox } from '@/components/checkbox';
import { Label } from '@/components/label';

const termsId = useId();
</script>

<template>
  <div class="nx:flex nx:items-center nx:gap-2">
    <Checkbox :id="termsId" />
    <Label :for="termsId">Accept terms and conditions</Label>
  </div>
</template>

This example also needs: Label.

Installation

  1. Install the following dependencies:

    npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/checkbox/Checkbox.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      CheckboxIndicator,
      CheckboxRoot,
      type CheckboxRootEmits,
      type CheckboxRootProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { IconCheck, IconMinus } from '../../lib/icons';
    import { selectionIndicatorMotionClassName } from '../../lib/motion';
    import { cn } from '../../lib/utils';
    
    export interface CheckboxProps extends CheckboxRootProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<CheckboxProps>();
    const emits = defineEmits<CheckboxRootEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <CheckboxRoot
        v-bind="forwarded"
        data-slot="checkbox"
        :class="
          cn(
            'nx:group nx:peer nx:inline-flex nx:size-4 nx:shrink-0 nx:items-center nx:justify-center nx:p-0',
            'nx:rounded-sm nx:border-default nx:border-border-default nx:bg-container',
            'nx:transition-control',
            'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
            'nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error',
            'nx:disabled:cursor-not-allowed nx:disabled:border-border-disabled nx:disabled:bg-disabled',
            'nx:enabled:data-[state=unchecked]:hover:bg-container-hover nx:enabled:data-[state=unchecked]:active:bg-container-active',
            'nx:data-[state=checked]:border-primary-background nx:data-[state=checked]:bg-primary-background nx:data-[state=checked]:text-primary-foreground',
            'nx:enabled:data-[state=checked]:hover:border-primary-background-hover nx:enabled:data-[state=checked]:hover:bg-primary-background-hover',
            'nx:enabled:data-[state=checked]:active:border-primary-background-hover nx:enabled:data-[state=checked]:active:bg-primary-background-active',
            'nx:data-[state=indeterminate]:border-primary-background nx:data-[state=indeterminate]:bg-primary-background nx:data-[state=indeterminate]:text-primary-foreground',
            'nx:enabled:data-[state=indeterminate]:hover:border-primary-background-hover nx:enabled:data-[state=indeterminate]:hover:bg-primary-background-hover',
            'nx:enabled:data-[state=indeterminate]:active:border-primary-background-hover nx:enabled:data-[state=indeterminate]:active:bg-primary-background-active',
            'nx:data-[state=checked]:disabled:border-primary-disabled nx:data-[state=checked]:disabled:bg-primary-disabled',
            'nx:data-[state=indeterminate]:disabled:border-primary-disabled nx:data-[state=indeterminate]:disabled:bg-primary-disabled',
            'nx:aria-invalid:data-[state=checked]:border-error-border nx:aria-invalid:data-[state=checked]:bg-error-background nx:aria-invalid:data-[state=checked]:text-error-foreground',
            'nx:aria-invalid:data-[state=indeterminate]:border-error-border nx:aria-invalid:data-[state=indeterminate]:bg-error-background nx:aria-invalid:data-[state=indeterminate]:text-error-foreground',
            'nx:enabled:aria-invalid:data-[state=checked]:hover:border-error-border nx:enabled:aria-invalid:data-[state=checked]:hover:bg-error-background-hover',
            'nx:enabled:aria-invalid:data-[state=checked]:active:border-error-border nx:enabled:aria-invalid:data-[state=checked]:active:bg-error-background-active',
            'nx:enabled:aria-invalid:data-[state=indeterminate]:hover:border-error-border nx:enabled:aria-invalid:data-[state=indeterminate]:hover:bg-error-background-hover',
            'nx:enabled:aria-invalid:data-[state=indeterminate]:active:border-error-border nx:enabled:aria-invalid:data-[state=indeterminate]:active:bg-error-background-active',
            props.class
          )
        "
      >
        <CheckboxIndicator
          force-mount
          data-slot="checkbox-indicator"
          class="nx:relative nx:flex nx:size-full nx:items-center nx:justify-center nx:text-current"
        >
          <IconCheck
            data-slot="checkbox-check"
            aria-hidden="true"
            :class="
              cn(
                'nx:absolute nx:size-3.5',
                selectionIndicatorMotionClassName,
                'nx:group-data-[state=checked]:scale-100 nx:group-data-[state=checked]:opacity-100'
              )
            "
          />
          <IconMinus
            data-slot="checkbox-minus"
            aria-hidden="true"
            :class="
              cn(
                'nx:absolute nx:size-3.5',
                selectionIndicatorMotionClassName,
                'nx:group-data-[state=indeterminate]:scale-100 nx:group-data-[state=indeterminate]:opacity-100'
              )
            "
          />
        </CheckboxIndicator>
      </CheckboxRoot>
    </template>
    
    components/checkbox/index.ts
    export { default as Checkbox, type CheckboxProps } from './Checkbox.vue';
    
    lib/icons.ts
    /**
     * Internal Icons
     *
     * This barrel file re-exports icons used internally by the design system.
     * These are hardcoded icons for component states (loading, validation, navigation).
     *
     * User-facing icons should be passed via props by consumers.
     *
     * This set draws Phosphor. `nxds init --icons` installs the same names drawn
     * by another library.
     */
    import { PhCircle } from '@phosphor-icons/vue';
    import { type FunctionalComponent, h } from 'vue';
    
    // Loading states
    export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue';
    
    // Navigation
    export {
      PhArrowDown as IconArrowDown,
      PhArrowLeft as IconArrowLeft,
      PhArrowRight as IconArrowRight,
      PhArrowUp as IconArrowUp,
      PhCaretDown as IconChevronDown,
      PhCaretLeft as IconChevronLeft,
      PhCaretRight as IconChevronRight,
      PhCaretUp as IconChevronUp,
      PhDotsThree as IconDots,
      PhSidebarSimple as IconLayoutSidebar,
      PhCaretUpDown as IconSelector,
    } from '@phosphor-icons/vue';
    
    // Actions
    export {
      PhCheck as IconCheck,
      PhCopy as IconCopy,
      PhMinus as IconMinus,
      PhPencilSimple as IconPencil,
      PhPlus as IconPlus,
      PhX as IconX,
    } from '@phosphor-icons/vue';
    
    export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) =>
      h(PhCircle, { weight: 'fill', ...attrs });
    
    // Filter field types
    export {
      PhCalendarBlank as IconCalendar,
      PhHash as IconHash,
      PhTextAa as IconLetterCase,
      PhListBullets as IconList,
    } from '@phosphor-icons/vue';
    
    // Search / input
    export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue';
    
    // Alerts/Status
    export {
      PhWarningCircle as IconAlertCircle,
      PhWarning as IconAlertTriangle,
      PhCheckCircle as IconCircleCheck,
      PhInfo as IconInfoCircle,
    } from '@phosphor-icons/vue';
    
    // Layout
    export {
      PhDotsSixVertical as IconGripVertical,
      PhPalette as IconPalette,
    } from '@phosphor-icons/vue';
    
    lib/motion.ts
    export const selectionIndicatorMotionClassName =
      'nx:scale-50 nx:opacity-0 nx:transition-[opacity,scale] nx:duration-fast nx:ease-enter';
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Checkbox } from '@/components/checkbox';

Examples

Checked

Start checked with :default-value="true".

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
</script>

<template>
  <Checkbox aria-label="Checked option" :default-value="true" />
</template>

Indeterminate

Show a minus for a partly selected group with model-value="indeterminate".

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
</script>

<template>
  <Checkbox aria-label="Indeterminate option" model-value="indeterminate" />
</template>

Disabled

Disable the checkbox so it cannot be toggled.

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
</script>

<template>
  <Checkbox aria-label="Disabled checkbox" disabled />
</template>

Props

Checkbox

Checkbox adds no props of its own; it accepts the props of the element or primitive it wraps.