Skip to content

Checkbox


vue
<template>
  <PeachyInput.Input class="checkbox">
    <PeachyCheckbox.Input v-model:state="checked">
      <template #after>
        <PeachyCheckbox.Indicator>
          <CheckboxSvg />
        </PeachyCheckbox.Indicator>

        <PeachyInput.Label>Checkbox</PeachyInput.Label>
      </template>
    </PeachyCheckbox.Input>
  </PeachyInput.Input>
</template>
css
.peachy-input__label {
  cursor: pointer;
}

.peachy-input.checkbox {
  display: flex;
  align-items: center;
  gap: var(--gap);
  position: relative;

  cursor: pointer;
}

.peachy-checkbox {
  display: flex;
  align-items: center;
  gap: var(--gap);
  position: relative;

  cursor: pointer;
}

.peachy-checkbox__input,
.peachy-checkbox__indicator {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 4px;
  margin: 0;
}

.peachy-checkbox__input {
  background-color: var(--border);
  transition: background-color 0.25s;
}

.peachy-checkbox__input:checked {
  background-color: var(--brand-button-bg);
}

.peachy-checkbox__indicator {
  position: absolute;
  display: grid;
  place-content: center;

  top: 0;
  left: 0;

  color: var(--brand-button-text);
}

.peachy-checkbox__indicator svg {
  color: var(--brand-button-text);
  width: var(--icon-size);
}

.peachy-checkbox__indicator svg path {
  stroke: var(--brand-button-text);
}

@media (prefers-reduced-motion: no-preference) {
  .peachy-checkbox__indicator svg {
    animation: ScaleIn 0.25s;
  }
}

/* ===== Animation ===== */

@keyframes ScaleIn {
  from {
    transform: scale(0.6);
  }
  to {
    transform: scale(1);
  }
}

Anatomy

vue
<template>
  <!-- Single checkbox --->
  <PeachyInput.Input>
    <PeachyCheckbox.Input>
      <!-- #before or #after -->
      <template #after>
        <PeachyCheckbox.Indicator>
          <template #indeterminate />
        </PeachyCheckbox.Indicator>

        <PeachyInput.Label />
      </template>
    </PeachyCheckbox.Input>
  </PeachyInput.Input>

  <!-- Checkbox group --->
  <PeachyInput.Group>
    <PeachyInput.GroupLabel />

    <PeachyInput.Input>
      <PeachyCheckbox.Input>
        <!-- #before or #after -->
        <template #after>
          <PeachyCheckbox.Indicator>
            <template #indeterminate />
          </PeachyCheckbox.Indicator>

          <PeachyInput.Label />
        </template>
      </PeachyCheckbox.Input>
    </PeachyInput.Input>
  </PeachyInput.Group/>
</template>

<script lang="ts" setup>
  import { PeachyCheckbox, PeachyInput } from "typeach";
</script>

A checkbox can be nested as many times as needed. Indicator has two slots - default and indeterminate, which only renders when the state is true or indeterminate.

Props & Emits

Input

DO NOT CONTROL A PARENT CHECKBOX

You should never manually set a parent checkbox's state - they are there as helpers for mass-edits, but are not intended to be controlled. A parent changes depending on it's children, so you might get unexpected results trying to override this.

Props

NameDefaultTypeDescription
statefalseCheckboxState?Should only be used for non-parent checkboxes.
disabledfalseboolean?A parent will also become disabled if any of it's children are disabled.
readOnlyfalseboolean?A parent will also become readonly if any of it's children are readonly/disabled.
requiredfalseboolean?The component only labels it as required with aria-required and does not deal with any error messages for you.

Emits

@Payload
update:stateCheckboxState
validateCheckboxState?
clear-validation
ts
export type CheckboxState = boolean | "indeterminate";

Styling

INPUT HAS DEFAULT STYLING

The Input element has a style of appearance: none, to allow styling it yourself. You should however style the input and Indicator to be aligned, so that the click-area lines up (see the example).

CSS Selectors

Follows our CSS classes convention, with some extras:

  • .peachy-checkbox__child is added for a checkbox with a parent.

  • .peachy-checkbox__parent is added for a checkbox with children.


State selectors

SelectorDescriptionFor
:checkedFor a checked checkbox.
  • Input
:disabledFor a disabled checkbox.
  • Input
[data-readonly="<boolean>"]For a read-only checkbox.
  • Input
[data-state="<CheckboxState>"]For the state of the checkbox.
  • Indicator
ts
export type CheckboxState = boolean | "indeterminate";

Accessibility

Resources: Styled HTML Checkboxes, One last time: custom styling radio buttons and checkboxes, APG Checkbox (Mixed State)

READONLY

Native checkbox inputs do not offer a readonly state, and aria-readonly has spotty support. So setting the checkbox to readonly makes the input disabled with aria-disabledto keeps it in the tab order, and manually prevents the input from being edited.

There is some disagreements if disabled conveys the right intent as a replacement for readonly, however, usually the solution people arrive at when they want a readonly checkbox is to make it disabled - so I figure this is a reasonable compromise.

References: Why is aria-readonly allowed for checkboxes and radios?, Provide a way to have disabled form controls to be submitted (was: readonly attribute)

Keyboard interactions

KeyAction
SpaceToggles the checkbox.

Grouped checkboxes

vue
<template>
  <PeachyInput.Group>
    <PeachyInput.GroupLabel>Grouped checkboxes</PeachyInput.GroupLabel>

    <PeachyInput.Input class="checkbox">
      <PeachyCheckbox.Input v-model:state="checkboxOne">
        <template #after>
          <PeachyCheckbox.Indicator>
            <CheckboxSvg />
          </PeachyCheckbox.Indicator>

          <PeachyInput.Label>Checkbox 1</PeachyInput.Label>
        </template>
      </PeachyCheckbox.Input>
    </PeachyInput.Input>

    <PeachyInput.Input class="checkbox">
      <PeachyCheckbox.Input v-model:state="checkboxTwo">
        <template #after>
          <PeachyCheckbox.Indicator>
            <CheckboxSvg />
          </PeachyCheckbox.Indicator>

          <PeachyInput.Label>Checkbox 2</PeachyInput.Label>
        </template>
      </PeachyCheckbox.Input>
    </PeachyInput.Input>
  </PeachyInput.Group>
</template>
css
.peachy-input__group {
  display: grid;
  gap: var(--gap);
  border: none;
}

.peachy-input__group-label {
  min-width: max-content;
  padding-bottom: 0.5rem;
  font-weight: var(--fw-bold);
}

.checkbox-children-wrapper {
  padding-left: 2rem;
  display: grid;
  gap: var(--gap);
}

Indeterminate checkboxes

DO NOT CONTROL A PARENT CHECKBOX

You should never manually set a parent checkbox's state - they are there as helpers for mass-edits, but are not intended to be controlled. A parent changes depending on it's children, so you might get unexpected results trying to override this.

vue
<template>
  <PeachyInput.Group class="indeterminate-checkbox-wrapper">
    <PeachyInput.GroupLabel>Indeterminate checkboxes</PeachyInput.GroupLabel>

    <PeachyInput.Input class="checkbox">
      <PeachyCheckbox.Input>
        <template #after>
          <PeachyCheckbox.Indicator>
            <template #indeterminate>-</template>

            <CheckboxSvg />
          </PeachyCheckbox.Indicator>

          <PeachyInput.Label>All checkboxes</PeachyInput.Label>

          <div class="checkbox-children-wrapper">
            <PeachyInput.Input class="checkbox">
              <PeachyCheckbox.Input @v-model:state="checkboxOne">
                <template #after>
                  <PeachyCheckbox.Indicator>
                    <CheckboxSvg />
                  </PeachyCheckbox.Indicator>

                  <PeachyInput.Label>Checkbox 1</PeachyInput.Label>
                </template>
              </PeachyCheckbox.Input>
            </PeachyInput.Input>

            <PeachyInput.Input class="checkbox">
              <PeachyCheckbox.Input @v-model:state="checkboxTwo">
                <template #after>
                  <PeachyCheckbox.Indicator>
                    <CheckboxSvg />
                  </PeachyCheckbox.Indicator>

                  <PeachyInput.Label>Checkbox 2</PeachyInput.Label>
                </template>
              </PeachyCheckbox.Input>
            </PeachyInput.Input>
          </div>
        </template>
      </PeachyCheckbox.Input>
    </PeachyInput.Input>
  </PeachyInput.Group>
</template>
css
.peachy-input__group.indeterminate-checkbox-wrapper {
  width: 12rem;
}

.peachy-input.checkbox:has(.peachy-checkbox__parent) {
  flex-wrap: wrap;
  display: flex;
  gap: var(--gap);
}

.peachy-input.checkbox:not(:has(.peachy-checkbox-parent)) {
  width: 100%;
  flex-shrink: 0;
}

.peachy-checkbox__input:indeterminate {
  background-color: var(--warning-color);
}