Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added Icon
Empty file.
Empty file added base
Empty file.
87 changes: 87 additions & 0 deletions lib/components/Checkbox/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
.Checkbox {
box-sizing: border-box;
display: flex;
width: 1.25rem;
height: 1.25rem;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
outline: 0;
padding: 0;
margin: 0;
border: none;

&[data-unchecked] {
border: 1px solid var(--color-filled-bg-neutral);
background-color: transparent;

&[data-disabled] {
border: 1px solid var(--color-filled-fg-neutral-disabled);
}
}

&[data-checked] {
background-color: var(--color-gray-900);

&[data-disabled] {
background-color: var(--color-filled-fg-neutral-disabled);
}
}

&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
}

.Neutral {
&[data-checked] {
background-color: var(--color-filled-bg-neutral);
}
}

.Brand {
&[data-checked] {
background-color: var(--color-bg-brand);
}
}

.Indicator {
display: flex;
color: var(--color-gray-50);

&[data-unchecked] {
display: none;
}
}

.Icon {
width: 0.75rem;
height: 0.75rem;
}

.Circle {
display: flex;
justify-content: center;
align-items: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background-color: transparent;
}

.Circle:hover {
background-color: var(--color-filled-bg-neutral-disabled);

&[data-disabled] {
background-color: transparent;
}
}

.Circle:active {
background-color: var(--color-tonal-bg-neutral-hover);

&[data-disabled] {
background-color: transparent;
}
}
57 changes: 57 additions & 0 deletions lib/components/Checkbox/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Checkbox as BaseUICheckbox } from "@base-ui-components/react/checkbox";
import { cva, type VariantProps } from "cva";
import { cn } from "../../utils";
import styles from "./index.module.css";

const CheckboxRootStyles = cva({
base: styles.Checkbox,
variants: {
disabled: {
true: "cursor-not-allowed",
false: "",
},
palette: {
neutral: styles.Neutral,
brand: styles.Brand,
},
},
});

function CheckIcon(props: React.ComponentProps<"svg">) {
return (
<svg
role="img"
aria-labelledby="title-id"
fill="currentcolor"
width="10"
height="10"
viewBox="0 0 10 10"
{...props}
>
<title id="title-id">A checkmark icon</title>
<path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
</svg>
);
}

type CheckboxProps = React.ComponentProps<typeof BaseUICheckbox.Root> &
VariantProps<typeof CheckboxRootStyles>;

export const Checkbox = ({
palette = "neutral",
disabled,
...props
}: CheckboxProps) => (
<div className={styles.Circle}>
<BaseUICheckbox.Root
defaultChecked
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The Checkbox component hardcodes the defaultChecked prop, causing all instances to be checked by default instead of inheriting the value from props.
Severity: HIGH

🔍 Detailed Analysis

The Checkbox component at lib/components/Checkbox/index.tsx hardcodes the defaultChecked prop. This causes all instances of the checkbox to be checked by default, which is contrary to standard UI behavior and the Base UI library's own default (false). The component's Storybook stories for Neutral and Brand do not pass defaultChecked, implying they should be unchecked, but they will render as checked. This forces consumers to explicitly and unintuitively pass defaultChecked={false} to achieve the standard unchecked state.

💡 Suggested Fix

Remove the hardcoded defaultChecked prop from the <BaseUICheckbox.Root> component in lib/components/Checkbox/index.tsx. Allow the checked state to be controlled by spreading the props object, which will correctly handle the defaultChecked value if provided by the consumer.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: lib/components/Checkbox/index.tsx#L47

Potential issue: The `Checkbox` component at `lib/components/Checkbox/index.tsx`
hardcodes the `defaultChecked` prop. This causes all instances of the checkbox to be
checked by default, which is contrary to standard UI behavior and the Base UI library's
own default (`false`). The component's Storybook stories for `Neutral` and `Brand` do
not pass `defaultChecked`, implying they should be unchecked, but they will render as
checked. This forces consumers to explicitly and unintuitively pass
`defaultChecked={false}` to achieve the standard unchecked state.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8372246

disabled={disabled}
className={cn(CheckboxRootStyles({ palette, disabled: disabled }))}
{...props}
>
<BaseUICheckbox.Indicator className={styles.Indicator}>
<CheckIcon className={styles.Icon} />
</BaseUICheckbox.Indicator>
</BaseUICheckbox.Root>
</div>
);
1 change: 1 addition & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import "./index.css";
export * from "@untitledui/icons";
export { Banner } from "./components/Banner";
export { Button } from "./components/Button";
export { Checkbox } from "./components/Checkbox";
export { SponsorCarousel } from "./components/SponsorCarousel";
40 changes: 40 additions & 0 deletions src/stories/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Checkbox } from "../../lib/components/Checkbox";

const meta = {
component: Checkbox,
argTypes: {
palette: {
table: { type: { summary: "neutral | brand" } },
options: ["neutral", "brand"],
control: "select",
},
disabled: {
table: { type: { summary: "boolean" } },
control: "boolean",
},
},
tags: ["autodocs"],
} satisfies Meta<typeof Checkbox>;
export default meta;

type Story = StoryObj<typeof meta>;
export const Neutral: Story = {
args: {
palette: "neutral",
disabled: false,
},
};

export const Brand: Story = {
args: {
palette: "brand",
disabled: false,
},
};

export const DefaultChecked: Story = {
args: {
defaultChecked: true,
},
};