Skip to content

Commit cf02ee7

Browse files
committed
fix: checkbox size, indeterminate and examples
1 parent 6fff943 commit cf02ee7

File tree

2 files changed

+70
-28
lines changed

2 files changed

+70
-28
lines changed

app/components/ui/checkbox.stories.tsx

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,48 @@ export const DefaultValue = () => {
1717
return <Checkbox defaultChecked>I love bears</Checkbox>;
1818
};
1919

20+
export const Sizes = () => {
21+
return (
22+
<div className="flex flex-col gap-2">
23+
<Checkbox size="sm">I love bears</Checkbox>
24+
<Checkbox>I love bears</Checkbox>
25+
<Checkbox size="lg">I love bears</Checkbox>
26+
</div>
27+
);
28+
};
29+
2030
export const Disabled = () => {
2131
return <Checkbox disabled>I love bears</Checkbox>;
2232
};
2333

34+
export const Indeterminate = () => {
35+
return <Checkbox indeterminate>I love bears</Checkbox>;
36+
};
37+
2438
export const CustomCheckbox = () => {
2539
return (
26-
<label className="relative flex cursor-pointer items-center justify-between gap-4 rounded-lg border border-border p-4 transition-colors outline-none focus-within:ring-[3px] focus-within:ring-ring/50 hover:bg-muted/50 has-[&[data-checked]]:bg-primary/5">
40+
<label className="relative flex cursor-pointer items-center justify-between gap-4 rounded-lg border border-border p-4 transition-colors outline-none focus-within:ring-[3px] focus-within:ring-ring/50 hover:bg-muted/50 has-[:checked]:border-none has-[:checked]:bg-primary has-[:checked]:text-primary-foreground">
2741
<Checkbox
2842
noLabel
2943
render={(props, { checked }) => {
3044
return (
31-
<div
45+
<button
46+
type="button"
3247
{...props}
3348
className="flex w-full justify-between outline-none"
3449
>
35-
<div className="flex flex-col">
36-
<span className="font-medium">I love bears</span>
37-
</div>
38-
<div
39-
className={cn('rounded-full bg-primary p-1 opacity-0', {
40-
'opacity-100': checked,
41-
})}
50+
<span className="font-medium">I love bears</span>
51+
<span
52+
className={cn(
53+
'rounded-full bg-primary-foreground p-1 opacity-0',
54+
{
55+
'opacity-100': checked,
56+
}
57+
)}
4258
>
43-
<CheckIcon className="h-4 w-4 text-primary-foreground" />
44-
</div>
45-
</div>
59+
<CheckIcon className="size-4 text-primary" />
60+
</span>
61+
</button>
4662
);
4763
}}
4864
/>

app/components/ui/checkbox.tsx

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,54 @@
11
import { Checkbox as CheckboxPrimitive } from '@base-ui-components/react/checkbox';
2-
import { CheckIcon } from 'lucide-react';
2+
import { cva } from 'class-variance-authority';
3+
import { CheckIcon, MinusIcon } from 'lucide-react';
34
import React from 'react';
45

56
import { cn } from '@/lib/tailwind/utils';
67

8+
const labelVariants = cva('flex items-center gap-2.5 text-primary', {
9+
variants: {
10+
size: {
11+
default: 'text-base',
12+
sm: 'gap-2 text-sm',
13+
lg: 'gap-3 text-lg',
14+
},
15+
},
16+
defaultVariants: {
17+
size: 'default',
18+
},
19+
});
20+
21+
const checkboxVariants = cva(
22+
'flex flex-none cursor-pointer items-center justify-center rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-muted-foreground disabled:opacity-20 data-checked:bg-primary data-checked:text-primary-foreground data-indeterminate:border data-indeterminate:border-primary/20 data-unchecked:border data-unchecked:border-primary/20',
23+
{
24+
variants: {
25+
size: {
26+
default: 'size-5 [&_svg]:size-3.5 [&_svg]:stroke-3',
27+
sm: 'size-4 rounded-[5px] [&_svg]:size-2.5 [&_svg]:stroke-4',
28+
lg: 'size-6 [&_svg]:size-4 [&_svg]:stroke-3',
29+
},
30+
},
31+
defaultVariants: {
32+
size: 'default',
33+
},
34+
}
35+
);
36+
737
export type CheckboxProps = Omit<CheckboxPrimitive.Root.Props, 'type'> & {
838
/**
939
* By default, the checkbox is wrapped in a `<label>`. Set to `false` if you do not want it.
1040
*/
1141
noLabel?: boolean;
1242
labelProps?: React.ComponentProps<'label'>;
43+
size?: 'default' | 'sm' | 'lg';
1344
};
1445

1546
export function Checkbox({
1647
children,
1748
className,
1849
noLabel,
1950
labelProps,
51+
size,
2052
...props
2153
}: CheckboxProps) {
2254
const Comp = noLabel ? React.Fragment : 'label';
@@ -25,33 +57,27 @@ export function Checkbox({
2557
? {}
2658
: {
2759
...labelProps,
28-
className: cn(
29-
'flex items-center gap-2 text-base text-primary',
30-
labelProps?.className
31-
),
60+
className: cn(labelVariants({ size }), labelProps?.className),
3261
};
3362

3463
return (
3564
<Comp {...compProps}>
3665
<CheckboxPrimitive.Root
37-
className={cn(
38-
'flex size-5 cursor-pointer items-center justify-center rounded-sm outline-none',
39-
'focus-visible:ring-[3px] focus-visible:ring-ring/50',
40-
'data-checked:bg-primary data-unchecked:border data-unchecked:border-primary/50',
41-
'disabled:cursor-not-allowed disabled:bg-muted-foreground disabled:opacity-20',
42-
className
43-
)}
66+
className={cn(checkboxVariants({ size }), className)}
4467
{...props}
4568
>
4669
<CheckboxPrimitive.Indicator
4770
keepMounted={true}
4871
className={cn(
4972
'flex transition-transform duration-150 ease-in-out',
50-
'data-checked:scale-100 data-checked:rotate-0 data-unchecked:invisible data-unchecked:scale-50 data-unchecked:rotate-45'
73+
'data-checked:scale-100 data-unchecked:invisible data-unchecked:scale-75'
74+
)}
75+
render={(props, state) => (
76+
<span {...props}>
77+
{state.indeterminate ? <MinusIcon /> : <CheckIcon />}
78+
</span>
5179
)}
52-
>
53-
<CheckIcon className="size-3.5 stroke-3 text-primary-foreground" />
54-
</CheckboxPrimitive.Indicator>
80+
/>
5581
</CheckboxPrimitive.Root>
5682
{children}
5783
</Comp>

0 commit comments

Comments
 (0)