Checker
Usage
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { Group } from "@/ui/group";
export function CheckerDemo() {
const [checked, setChecked] = useState(false);
return (
<Group>
<Checker label="Switch" />
<Checker type="checkbox" label="Checkbox" />
<Checker type="radio" label="Radio" checked={checked} onClick={() => setChecked(c => !c)} />
</Group>
);
}
Properties
Size
Color
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
export function CheckerDemo() {
const [checked, setChecked] = useState(false);
return <Checker label="I agree to sell my privacy" onLabel="ON" offLabel="OFF" defaultChecked checked={checked} onChange={event => setChecked(event.currentTarget.checked)} />;
}
Change Icon
Change Icon Labels
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { MoonStarIcon, SunIcon } from "@/icons/*";
export function CheckerChangeIconLabels() {
return (
<Checker
type="switch"
color="#543a7b"
label="Icon Labels"
onLabel={<SunIcon size={16} stroke={2} color="#fff" />}
offLabel={<MoonStarIcon size={16} stroke={2} color="rgb(34, 139, 230)" />}
className="cursor-pointer rounded-lg border px-3 py-2.5 font-medium transition-colors [&:has(input:checked)]:bg-[#6e5494]"
/>
);
}
Change Thumb Icon
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { CheckIcon, XIcon } from "@/icons/*";
export function CheckerChangeThumbIcon() {
const [checked, setChecked] = useState(false);
return (
<Checker
type="switch"
color="#930464"
label="Icon Labels"
checked={checked}
onChange={event => setChecked(event.currentTarget.checked)}
icon={checked ? <CheckIcon size={12} stroke={3} color="green" /> : <XIcon size={12} stroke={3} color="red" />}
/>
);
}
Checker.Group
Select your favorite framework/library
This is anonymous
Size
Color
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { Group } from "@/ui/group";
const data = [
{ value: "react", label: "React" },
{ value: "svelte", label: "Svelte" },
{ value: "ng", label: "Angular" },
{ value: "vue", label: "Vue" }
];
export function CheckerGroupDemo() {
const [value, setValue] = useState<string[] | string | null>(["react", "ng"]);
return (
<Checker.Group label="Select your favorite framework/library" description="This is anonymous" value={value} onChange={setValue}>
<Group>
{data.map(i => (
<Checker key={i.value} id={i.value} {...i} />
))}
</Group>
</Checker.Group>
);
}
Group.Card
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { Group } from "@/ui/group";
const data = [
{ value: "react", label: "React" },
{ value: "svelte", label: "Svelte" },
{ value: "ng", label: "Angular" },
{ value: "vue", label: "Vue" }
];
export function CheckerGroupCardDemo() {
const [value, setValue] = useState<string[] | string | null>(["react", "ng"]);
return (
<Checker.Group value={value} onChange={setValue} classNames={{ group: "grid grid-cols-2 gap-2" }}>
{data2.map(i => (
<Checker.Card key={i.value} value={i.value}>
<Checker id={i.value} {...i} />
</Checker.Card>
))}
</Checker.Group>
);
}
Current Selected
CurrentValue: –
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { Group } from "@/ui/group";
import { Stack } from "@/ui/stack";
import { Typography } from "@/ui/typography";
const data = [
{ value: "react", label: "React", description: "A JavaScript library for building user interfaces." },
{ value: "svelte", label: "Svelte", description: "A compiler that generates minimal and efficient JavaScript code." },
{ value: "ng", label: "Angular", description: "A platform for building mobile and desktop web applications." },
{ value: "vue", label: "Vue", description: "A progressive JavaScript framework for building UI.", error: "Compatibility issues with older versions of Internet Explorer." },
];
export function CheckerCurrentSelectedDemo() {
const [value, setValue] = useState<string | string[] | null>(null);
const cards = data.map(i => (
<Checker.Card key={i.value} value={i.value}>
<Checker id={i.value} {...i} />
</Checker.Card>
));
return (
<Stack>
<Checker.Group value={value} onChange={setValue}>
<Group>{cards}</Group>
</Checker.Group>
<Typography prose="p" className="mt-8">
CurrentValue: {value || "–"}
</Typography>
</Stack>
);
}
Multiple Selected
CurrentValue: -
"use client";
import { useState } from "react";
import { Checker } from "@/ui/checker";
import { CheckIcon, XIcon } from "@/icons/*";
const data = [
{ value: "next", label: "Next.js", description: "A React framework for building server-rendered applications with great performance and scalability." },
{ value: "gatsby", label: "Gatsby", description: "A React-based framework optimized for building fast, static websites with GraphQL.", error: "Potential performance issues with large data sources due to build times." },
{ value: "remix", label: "Remix", description: "A full-stack framework for building modern web applications focused on user experience and performance.", error: "Limited ecosystem compared to Next.js and Gatsby." },
];
export function CheckerMultipleSelectedDemo() {
const [value, setValue] = useState<string[] | string | null>([]);
return (
<>
<Checker.Group multiple value={value} onChange={setValue} classNames={{ group: "gap-3" }}>
{data.map(i => (
<Checker.Card key={i.value} value={i.value}>
<Checker
{...i}
id={i.value}
icon={value?.includes(i.value) ? <CheckIcon size={12} stroke={3} color="green" /> : <XIcon size={12} stroke={3} color="red" />}
/>
</Checker.Card>
))}
</Checker.Group>
<p className="mt-8 w-full text-left text-sm text-muted-foreground">CurrentValue: {value?.length ? JSON.stringify(value) : "-"}</p>
</>
);
}
Indeterminate State
"use client";
import { Checker } from "@/ui/checker";
import { useListState } from "@/hooks/use-list-state";
const initialValues = [
{ label: "Receive email notifications", checked: false },
{ label: "Receive sms notifications", checked: false },
{ label: "Receive push notifications", checked: false }
];
export function CheckerIndeterminateStateDemo() {
const [values, handlers] = useListState(initialValues);
const allChecked = values.every(value => value.checked);
const indeterminate = values.some(value => value.checked) && !allChecked;
const items = values.map((value, index) => (
<Checker
type="checkbox"
key={index}
label={value.label}
className="ml-8 mt-2"
checked={value.checked}
onChange={event => handlers.setItemProp(index, "checked", event.currentTarget.checked)}
/>
));
return (
<div className="m-auto grid grid-flow-row">
<Checker
type="checkbox"
checked={allChecked}
indeterminate={indeterminate}
label="Receive all notifications"
onChange={() => handlers.setState(current => current.map(value => ({ ...value, checked: !allChecked })))}
/>
{items}
</div>
);
}
API References
Styles API
Styles API | Type | Default | Annotation |
---|---|---|---|
unstyled? | Partial<Record<T, boolean>> | false | if true , all default styles will be removed |
classNames? | Partial<Record<T, string>> | undefined | Set className for T |
styles? | Partial<Record<T, CSSProperties>> | undefined | Set style for T |
Props API
Checker
type Icon = React.FC<{ indeterminate: boolean | undefined; className: string }>;
Checker Props API | Type | Default | Annotation |
---|---|---|---|
id? | string | Id used to bind input and label, if not passed, unique id will be generated instead | |
label? | React.ReactNode | Content of the label associated with the radio | |
icon? | Icon | Icon displayed when checked is in checked or indeterminate state | |
offLabel? | React.ReactNode | Inner label when the Checker at type="switch" is in unchecked state | |
onLabel? | React.ReactNode | Inner label when the Checker at type="switch" is in checked state | |
color? | CSSProperties["color"] | Key of CSS color to set input color in checked state. | |
size? | string | number | Controls size of all elements | |
round? | string | number | 9999 | Key of CSS value to set `border-radius |
wrapperProps? | ComponentProps<"div"> | Props passed down to the root element | |
labelPosition? | "left" | "right" | right | Position of the label relative to the input |
description? | React.ReactNode | undefined | Description displayed below the label |
error? | React.ReactNode | undefined | Error displayed below the label |
rootRef? | ForwardedRef<HTMLDivElement> | Assigns ref of the root element | |
indeterminate? | boolean | false | Indeterminate state of the checkbox . If set, checked prop at type="checkbox" is ignored. |
CheckerGroup
CheckerGroup Props API | Type | Default | Annotation |
---|---|---|---|
children? | React.ReactNode | Required Checker components | |
value? | string[] | undefined | Controlled component value |
defaultValue? | string[] | undefined | Default value for uncontrolled component |
onChange? | (value: string[]) => void | undefined | Called when value changes |
wrapperProps? | ComponentProps<"div"> | undefined | Props passed down to the wrapper |
size? | (string & {}) | number | 20 | Controls size of the wrapper |
readOnly? | boolean | false | If set, value cannot be changed |
indeterminate? | boolean | false | Indeterminate state of the checked. If set, checked prop at type="checkbox" is ignored. |
Source Codes
checker.tsx
globals.css
/* checker */
@layer base {
.stylelayer-checkergroup {
& [data-checkergroup="card"] {
@apply transition-[border-color] rounded-[--checker-card-round] border data-[checked=true]:border-constructive data-[label-position=left]:justify-end data-[label-position=right]:justify-start focus-visible:ring-0 focus-visible:border-constructive focus-visible:outline-constructive focus-visible:outline-offset-2 focus-visible:outline focus-visible:outline-2 [&_*]:pointer-events-none [-webkit-tap-highlight-color:transparent];
&:has([data-switch="error"], [data-checkbox="error"], [data-radio="error"]) {
@apply border-destructive focus-visible:outline-destructive;
}
& [data-switch="root"],
& [data-checkbox="root"],
& [data-radio="root"] {
@apply w-full;
&[data-label-position="left"] {
& [data-switch="labelWrapper"],
& [data-checkbox="labelWrapper"],
& [data-radio="labelWrapper"] {
@apply mr-auto;
}
}
}
}
}
.stylelayer-switch,
.stylelayer-checkbox,
.stylelayer-radio {
&,
& *,
& * * {
@apply text-left [-webkit-tap-highlight-color:transparent];
}
}
.stylelayer-switch {
&:not(:has([data-switch="description"], [data-switch="error"])) {
@apply items-center;
}
&[data-label-position="left"] {
& [data-switch="track"] {
@apply order-2;
}
& [data-switch="labelWrapper"] {
@apply order-1;
}
}
& input:focus-visible {
& + [data-switch="track"] {
@apply outline outline-2 outline-offset-2 outline-[--switch-bg,hsl(var(--constructive))];
}
}
&:has(input:disabled, input:where([data-disabled])) {
@apply [--switch-bg:hsl(var(--muted)/0.5)] [--switch-bd:unset] [--switch-thumb-bg:hsl(var(--muted-foreground))] [--switch-thumb-bd:--switch-thumb-bg] [--switch-track-label-color:--switch-thumb-bg] [--switch-cursor:not-allowed];
}
&:has(input:not(:checked)) {
&[data-error] {
@apply [--switch-bd:hsl(var(--destructive))];
}
}
&:has(input:checked) {
@apply [--switch-bg:--switch-color] [--switch-bd:--switch-color] [--switch-thumb-transform:--switch-thumb-sz] [--switch-thumb-bg:white] [--switch-thumb-bd:--switch-thumb-bg] [--switch-transform:calc(var(--switch-thumb-transform,0px)+var(--switch-thumb-margin))];
& [data-switch="track"] {
&[data-disabled] {
@apply opacity-50 cursor-not-allowed pointer-events-none;
}
}
& [data-switch="trackLabel"] {
@apply ml-0 mr-[calc(var(--switch-thumb-sz)+var(--switch-thumb-margin))];
}
}
& [data-switch="track"] {
@apply relative overflow-hidden transition-colors appearance-none flex items-center place-content-center select-none z-0 cursor-[--switch-cursor,pointer] rounded-[--switch-round] bg-[--switch-bg,hsl(var(--muted))] [border:0.0625rem_solid_var(--switch-bd,hsl(var(--border)))] h-[--switch-h] min-h-[--switch-h] max-h-[--switch-h] w-[--switch-w] min-w-[--switch-w] max-w-[--switch-w] m-0 leading-[0];
}
& [data-switch="thumb"] {
@apply absolute z-1 flex rounded-[--switch-round] bg-[--switch-thumb-bg,hsl(var(--color))] size-[--switch-thumb-sz] [border:0.0625rem_solid_var(--switch-thumb-bd)] left-0 inset-y-1/2 transition-transform [&>*]:m-auto [transform:translate(calc(var(--switch-transform,0%)+12%),-50%)];
}
& [data-switch="trackLabel"] {
@apply h-full grid place-content-center min-w-[calc((var(--switch-w)-var(--switch-thumb-sz))-((var(--switch-h)-0.125rem)*(15/100)))] px-[--switch-thumb-padding] ml-[calc(var(--switch-thumb-sz)+var(--switch-thumb-margin))] transition-[margin] [font-size:--switch-label-fz] font-semibold text-[--switch-track-label-color,hsl(var(--color))];
}
}
.stylelayer-checkbox {
&:not(:has([data-checkbox="description"], [data-checkbox="error"])) {
@apply items-center;
}
&[data-label-position="left"] {
& [data-checkbox="track"] {
@apply order-2;
}
& [data-checkbox="labelWrapper"] {
@apply order-1;
}
}
& input:focus-visible {
& + [data-checkbox="track"] {
@apply outline outline-2 outline-offset-2 outline-[--switch-bg,hsl(var(--constructive))];
}
}
&:has(input:disabled, input:where([data-disabled])) {
@apply [--checkbox-bg:hsl(var(--muted)/0.5)] [--checkbox-bd:unset] [--checkbox-cursor:not-allowed];
}
&:has(input:not(:checked)) {
&[data-error] {
@apply [--checkbox-bd:hsl(var(--destructive))];
}
}
&:has(input[data-indeterminate]),
&:has(input:checked) {
@apply [--checkbox-bg:--checkbox-color] [--checkbox-bd:--checkbox-color];
& [data-checkbox="track"] {
&[data-disabled] {
@apply opacity-50 cursor-not-allowed pointer-events-none;
}
& > svg {
@apply opacity-100 transform-none;
}
}
}
& [data-checkbox="track"] {
@apply relative overflow-hidden transition-colors appearance-none flex items-center place-content-center select-none z-0 cursor-[--checkbox-cursor,pointer] rounded-[--checkbox-round] bg-[--checkbox-bg,hsl(var(--muted))] [border:0.0625rem_solid_var(--checkbox-bd,hsl(var(--border)))] h-[--checkbox-sz] min-h-[--checkbox-sz] max-h-[--checkbox-sz] w-[--checkbox-sz] min-w-[--checkbox-sz] max-w-[--checkbox-sz] m-0 leading-[0];
& > svg {
@apply absolute inset-0 m-auto pointer-events-none size-[--checkbox-icon-sz,60%] [transition:transform_.1s_ease-in,opacity_.1s_ease-in] opacity-0 [transform:translateY(calc(.3125rem))_scale(.5)];
}
}
}
.stylelayer-radio {
&:not(:has([data-radio="description"], [data-radio="error"])) {
@apply items-center;
}
&[data-label-position="left"] {
& [data-radio="track"] {
@apply order-2;
}
& [data-radio="labelWrapper"] {
@apply order-1;
}
}
& input:focus-visible {
& + [data-radio="track"] {
@apply outline outline-2 outline-offset-2 outline-[--switch-bg,hsl(var(--constructive))];
}
}
&:has(input:disabled, input:where([data-disabled])) {
@apply [--radio-bg:hsl(var(--muted)/0.5)] [--radio-bd:unset] [--radio-thumb-bg:hsl(var(--muted-foreground))] [--radio-thumb-bd:--radio-thumb-bg] [--radio-track-label-color:--radio-thumb-bg] [--radio-cursor:not-allowed];
}
&:has(input:not(:checked)) {
&[data-error] {
@apply [--radio-bd:hsl(var(--destructive))];
}
}
&:has(input:checked) {
@apply [--radio-bg:--radio-color] [--radio-bd:--radio-color] [--radio-thumb-transform:--radio-thumb-sz] [--radio-thumb-bg:white] [--radio-thumb-bd:--radio-thumb-bg] [--radio-transform:calc(var(--radio-thumb-transform,0px)+var(--radio-thumb-margin))];
& [data-radio="track"] {
&[data-disabled] {
@apply opacity-50 cursor-not-allowed pointer-events-none;
}
& > svg {
@apply opacity-100 transform-none;
}
}
}
& [data-radio="track"] {
@apply relative overflow-hidden transition-colors appearance-none flex items-center place-content-center select-none z-0 cursor-[--radio-cursor,pointer] rounded-[--radio-round] bg-[--radio-bg,hsl(var(--muted))] [border:0.0625rem_solid_var(--radio-bd,hsl(var(--border)))] h-[--radio-sz] min-h-[--radio-sz] max-h-[--radio-sz] w-[--radio-sz] min-w-[--radio-sz] max-w-[--radio-sz] m-0 leading-[0];
& > svg {
@apply absolute inset-0 m-auto pointer-events-none size-[--radio-icon-sz,60%] [transition:transform_.1s_ease-in,opacity_.1s_ease-in] opacity-0 [transform:translateY(calc(.3125rem))_scale(.5)];
}
}
& [data-radio="thumb"] {
@apply absolute z-1 flex rounded-[--radio-round] bg-[--radio-thumb-bg,hsl(var(--color))] size-[--radio-thumb-sz] [border:0.0625rem_solid_var(--radio-thumb-bd)] left-0 inset-y-1/2 transition-transform [&>*]:m-auto [transform:translate(calc(var(--radio-transform,0%)+12%),-50%)];
}
}
}