Skip to content

Commit 89d3a21

Browse files
timphandevclaude
andcommitted
docs(class-recipe): convert a shadcn/ui button built on Base UI
Show the button that shadcn/ui generates on Base UI instead of Radix. Base UI takes a className that may be a function of the state, which a recipe does not take, so the converted button passes Base UI a function that calls the recipe with the className of that state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 8ef4bb6 commit 89d3a21

1 file changed

Lines changed: 36 additions & 33 deletions

File tree

‎docs/src/content/docs/class-recipe/shadcn-ui.md‎

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,12 @@ fits your version of Tailwind CSS.
5454
4. Pass `className` to the recipe, and drop the `cn` call around it. The
5555
recipe adds `className` last, and merges it with its join.
5656

57-
Here is a `button.tsx` before the change. The exact classes depend on
58-
your version of shadcn/ui:
57+
Here is a `button.tsx` built on [Base UI](https://base-ui.com) before the
58+
change. The exact classes depend on your version of shadcn/ui:
5959

6060
```tsx
6161
// src/components/ui/button.tsx
62-
import * as React from "react";
63-
import { Slot } from "@radix-ui/react-slot";
62+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
6463
import { cva, type VariantProps } from "class-variance-authority";
6564

6665
import { cn } from "@/lib/utils";
@@ -95,18 +94,12 @@ const buttonVariants = cva(
9594

9695
function Button({
9796
className,
98-
variant,
99-
size,
100-
asChild = false,
97+
variant = "default",
98+
size = "default",
10199
...props
102-
}: React.ComponentProps<"button"> &
103-
VariantProps<typeof buttonVariants> & {
104-
asChild?: boolean;
105-
}) {
106-
const Comp = asChild ? Slot : "button";
107-
100+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
108101
return (
109-
<Comp
102+
<ButtonPrimitive
110103
data-slot="button"
111104
className={cn(buttonVariants({ variant, size, className }))}
112105
{...props}
@@ -121,9 +114,8 @@ And after:
121114

122115
```tsx
123116
// src/components/ui/button.tsx
124-
import * as React from "react";
117+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
125118
import type { VariantsOf } from "@lynstack/class-recipe";
126-
import { Slot } from "@radix-ui/react-slot";
127119
import { cva } from "@/lib/utils";
128120

129121
const buttonVariants = cva({
@@ -153,20 +145,21 @@ const buttonVariants = cva({
153145

154146
function Button({
155147
className,
156-
variant,
157-
size,
158-
asChild = false,
148+
variant = "default",
149+
size = "default",
159150
...props
160-
}: React.ComponentProps<"button"> &
161-
VariantsOf<typeof buttonVariants> & {
162-
asChild?: boolean;
163-
}) {
164-
const Comp = asChild ? Slot : "button";
165-
151+
}: ButtonPrimitive.Props & VariantsOf<typeof buttonVariants>) {
166152
return (
167-
<Comp
153+
<ButtonPrimitive
168154
data-slot="button"
169-
className={buttonVariants({ variant, size, className })}
155+
className={(state) =>
156+
buttonVariants({
157+
variant,
158+
size,
159+
className:
160+
typeof className === "function" ? className(state) : className,
161+
})
162+
}
170163
{...props}
171164
/>
172165
);
@@ -175,12 +168,22 @@ function Button({
175168
export { Button, buttonVariants };
176169
```
177170

178-
The component keeps its API: `variant`, `size`, `asChild` with `Slot`
179-
from `@radix-ui/react-slot`, and the `buttonVariants` export, which other
180-
components and links call, such as
181-
`<a className={buttonVariants({ variant: "ghost" })}>`. Newer versions of
182-
shadcn/ui import `Slot` from `radix-ui` instead; keep the import that your
183-
component has.
171+
A Base UI component takes a `className` that is either a string or a
172+
function of its state, such as
173+
`className={(state) => (state.disabled ? "opacity-25" : "")}`. A recipe
174+
takes a string, so the component passes Base UI a function, which calls
175+
the recipe with the `className` of that state. class-variance-authority
176+
ignores a function, so the classes it returned were lost; the recipe adds
177+
them.
178+
179+
The component keeps its API: `variant`, `size`, the `render` prop of
180+
Base UI, such as `<Button nativeButton={false} render={<a href="/" />}>`,
181+
and the `buttonVariants` export, which other components and links call,
182+
such as `<a className={buttonVariants({ variant: "ghost" })}>`.
183+
184+
A component built on Radix takes `asChild` and renders `Slot` instead,
185+
and its `className` is a string: pass it to the recipe directly, as in
186+
`className={buttonVariants({ variant, size, className })}`.
184187

185188
Every variant of the button has a default, so its props stay optional. In
186189
a component whose variant has no default, `VariantsOf` makes that variant

0 commit comments

Comments
 (0)