Checkbox
<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>
.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
<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
Name | Default | Type | Description |
---|---|---|---|
state | false | CheckboxState? | Should only be used for non-parent checkboxes. |
disabled | false | boolean? | A parent will also become disabled if any of it's children are disabled. |
readOnly | false | boolean? | A parent will also become readonly if any of it's children are readonly/disabled. |
required | false | boolean? | The component only labels it as required with aria-required and does not deal with any error messages for you. |
Emits
@ | Payload |
---|---|
update:state | CheckboxState |
validate | CheckboxState? |
clear-validation |
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
Selector | Description | For |
---|---|---|
:checked | For a checked checkbox. |
|
:disabled | For a disabled checkbox. |
|
[data-readonly="<boolean>"] | For a read-only checkbox. |
|
[data-state="<CheckboxState>"] | For the state of the checkbox. |
|
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-disabled
to 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
Key | Action |
---|---|
Space | Toggles the checkbox. |
Grouped checkboxes
<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>
.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.
<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>
.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);
}