Skip to content
Draft
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
8 changes: 4 additions & 4 deletions apps/ui/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { DocsTableOfContents } from "@/components/docs-toc";
import { SiteFooter } from "@/components/site-footer";
import { source } from "@/lib/source";
import { mdxComponents } from "@/mdx-components";
import { Button } from "@/registry/default/ui/button";
import { ButtonLink } from "@/registry/default/ui/button";

export const revalidate = false;
export const dynamic = "force-static";
Expand Down Expand Up @@ -82,7 +82,7 @@ export default async function Page(props: {
</div>
<div className="flex items-center space-x-2 pt-4">
{links?.doc && (
<Button
<ButtonLink
render={
<Link href={links.doc} rel="noreferrer" target="_blank">
<HugeiconsIcon
Expand All @@ -105,7 +105,7 @@ export default async function Page(props: {
</div>
<div className="hidden items-center gap-2 pt-8 sm:flex">
{neighbours.previous && (
<Button
<ButtonLink
className="shadow-none"
render={
<Link href={neighbours.previous.url}>
Expand All @@ -117,7 +117,7 @@ export default async function Page(props: {
/>
)}
{neighbours.next && (
<Button
<ButtonLink
className="ms-auto shadow-none"
render={
<Link href={neighbours.next.url}>
Expand Down
6 changes: 3 additions & 3 deletions apps/ui/components/main-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";

import { cn } from "@/lib/utils";
import { Button } from "@/registry/default/ui/button";
import { ButtonLink } from "@/registry/default/ui/button";

export function MainNav({
items,
Expand All @@ -18,7 +18,7 @@ export function MainNav({
return (
<nav className={cn("items-center gap-2", className)} {...props}>
{items.map((item) => (
<Button
<ButtonLink
data-pressed={pathname.includes(item.href) || undefined}
key={item.href}
render={
Expand All @@ -30,7 +30,7 @@ export function MainNav({
variant="ghost"
>
{item.label}
</Button>
</ButtonLink>
))}
</nav>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/content/docs/(root)/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ We think Base UI is the best foundation for modern web applications. We've taken

This is the component library we'll be progressively adopting for [cal.com](https://cal.com). We're building it in the open for anyone who wants to create beautiful, reliable user interfaces.

<Alert className="bg-muted/24">
<Alert>
<InfoIcon />
<AlertTitle>Early Access</AlertTitle>
<AlertDescription>
Expand Down
78 changes: 61 additions & 17 deletions apps/ui/content/docs/components/button.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Button
description: A button or a component that looks like a button.
description: A button component that can be rendered as another tag or focusable when disabled.
---

<ComponentPreview name="p-button-1" />
Expand Down Expand Up @@ -62,25 +62,33 @@ npm install @base-ui-components/react
## Usage

```tsx
import { Button } from "@/components/ui/button"
import { Button, ButtonLink } from "@/components/ui/button"
```

```tsx
<Button>Button</Button>
// Render a native button element
<Button>Action</Button>

// Render a div of role="button" that looks like a button
<Button render={<div />} nativeButton={false}>Action</Button>

// Render an anchor tag that looks like a button
<ButtonLink href="/about">About</ButtonLink>

// Render a custom component (e.g., Next.js Link) that looks like a button
<ButtonLink render={<Link href="/about" />}>About</ButtonLink>
```

## Link
`ButtonLink` was introduced because the Base UI `Button` component adds attributes (like `role="button"`) that are incompatible with anchor tags when using the `render` prop.

You can use the [`render`](https://base-ui.com/react/utils/use-render#migrating-from-radix-ui) prop to make another component look like a button. Here's an example of a link that looks like a button.
For links styled as buttons, always use `ButtonLink` instead of `Button` with `render`:

```tsx
import Link from "next/link"
// Correct: Use ButtonLink for links
<ButtonLink render={<Link href="/about" />}>About</ButtonLink>

import { Button } from "@/components/ui/button"

export function LinkAsButton() {
return <Button render={<Link href="/login" />}>Login</Button>
}
// Incorrect: Button adds incompatible attributes to anchor elements
<Button render={<Link href="/about" />}>About</Button>
```

## Examples
Expand Down Expand Up @@ -157,13 +165,43 @@ export function LinkAsButton() {

<ComponentPreview name="p-button-18" />

## API Reference

### Button

`Button` supports the following props:

| Prop | Type | Default | Description |
| :---------- | :------------------------------------------------------------ | :-------- | :--------------------------------------------- |
| `variant` | `"default" \| "destructive" \| "outline" \| "ghost" \| ...` | `"default"` | The visual style variant |
| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl" \| "icon" \| ...` | `"default"` | The size of the button |
| `render` | `ReactElement \| ((props) => ReactElement)` | - | Custom element to render |
| `className` | `string` | - | Additional CSS classes |

All standard button (`<button>`) element props are also supported.

### ButtonLink

`ButtonLink` supports the same `variant` and `size` props as `Button`, plus standard anchor (`<a>`) element props.

| Prop | Type | Default | Description |
| :---------- | :------------------------------------------------------------ | :-------- | :--------------------------------------------- |
| `href` | `string` | - | The URL the link points to |
| `variant` | `"default" \| "destructive" \| "outline" \| "ghost" \| ...` | `"default"` | The visual style variant |
| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl" \| "icon" \| ...` | `"default"` | The size of the button |
| `render` | `ReactElement \| ((props) => ReactElement)` | - | Custom element to render (e.g., Next.js `Link`) |
| `className` | `string` | - | Additional CSS classes |

All standard anchor (`<a>`) element props are also supported. By default, `ButtonLink` renders an `<a>` element. Use the `render` prop to render a different component like Next.js `Link`.

## Comparing with Radix / shadcn

If you’re already familiar with Radix UI and shadcn/ui, this guide highlights the small differences and similarities so you can get started with **coss ui** quickly.

### Quick Checklist

- Replace `asChild` → `render` on `Button`
- For links styled as buttons, use `ButtonLink` instead of `Button` with `render`

### Additional Notes

Expand Down Expand Up @@ -198,16 +236,22 @@ We've added a new `destructive-outline` variant for better UX patterns:
```tsx title="shadcn/ui"
// [!code word:asChild]
<Button asChild>
<Link href="/login">Login</Link>
<a href="/login">Link</a>
</Button>

<Button asChild>
<Link href="/login">Next.js Link</Link>
</Button>
```
</span>

<span data-lib="base-ui">
```tsx title="coss ui"
// [!code word:render]
<Button render={<Link href="/login" />}>Login</Button>
```
</span
// [!code word:ButtonLink]
// [!code word:render:3]
// [!code word:nativeButton:3]
<ButtonLink href="/login">Link</ButtonLink>

>
<ButtonLink render={<Link href="/login" />}>Next.js Link</ButtonLink>
```
</span>
2 changes: 1 addition & 1 deletion apps/ui/content/docs/components/input-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ A container for addons like icons, text, buttons, and other elements. Can be pos
| `className` | `string` | | Additional CSS classes to apply to the component |
| `...props` | `React.ComponentProps<'div'>` | | All standard div attributes are supported |

<Alert className="bg-muted/24">
<Alert>
<InfoIcon />
<AlertDescription>
For proper focus navigation, the `InputGroupAddon` component should be placed
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"dependencies": {
"@base-ui-components/react": "^1.0.0-beta.7",
"@base-ui-components/react": "^1.0.0-rc.0",
"@coss/ui": "workspace:*",
"@hugeicons/core-free-icons": "^2.0.0",
"@hugeicons/react": "^1.1.1",
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
- [Avatar](https://coss.com/ui/docs/components/avatar.md): A visual representation of a user or entity.
- [Badge](https://coss.com/ui/docs/components/badge.md): A small status indicator or label component.
- [Breadcrumb](https://coss.com/ui/docs/components/breadcrumb.md): Displays the path to the current resource using a hierarchy of links.
- [Button](https://coss.com/ui/docs/components/button.md): A button or a component that looks like a button.
- [Button](https://coss.com/ui/docs/components/button.md): A button component that can be rendered as another tag or focusable when disabled.
- [Button Link](https://coss.com/ui/docs/components/button-link.md): A link component that looks like a button.
- [Card](https://coss.com/ui/docs/components/card.md): A content container for grouping related information.
- [Checkbox](https://coss.com/ui/docs/components/checkbox.md): A binary toggle input for selecting one or multiple options.
- [Checkbox Group](https://coss.com/ui/docs/components/checkbox-group.md): A collection of related checkboxes with group-level control.
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/public/r/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"files": [
{
"path": "registry/default/ui/button.tsx",
"content": "import { mergeProps } from \"@base-ui-components/react/merge-props\";\nimport { useRender } from \"@base-ui-components/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border bg-clip-padding font-medium text-sm outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default:\n \"min-h-8 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)]\",\n icon: \"size-8\",\n \"icon-lg\": \"size-9\",\n \"icon-sm\": \"size-7\",\n \"icon-xl\": \"size-10 [&_svg:not([class*='size-'])]:size-4.5\",\n \"icon-xs\":\n \"size-6 rounded-md before:rounded-[calc(var(--radius-md)-1px)]\",\n lg: \"min-h-9 px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2)-1px)]\",\n sm: \"min-h-7 gap-1.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)]\",\n xl: \"min-h-10 px-[calc(--spacing(4)-1px)] py-[calc(--spacing(2)-1px)] text-base [&_svg:not([class*='size-'])]:size-4.5\",\n xs: \"min-h-6 gap-1 rounded-md px-[calc(--spacing(2)-1px)] py-[calc(--spacing(1)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default:\n \"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none\",\n destructive:\n \"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none\",\n \"destructive-outline\":\n \"border-border bg-transparent text-destructive-foreground shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:border-destructive/32 [&:is(:hover,[data-pressed])]:bg-destructive/4\",\n ghost: \"border-transparent hover:bg-accent data-pressed:bg-accent\",\n link: \"border-transparent underline-offset-4 hover:underline\",\n outline:\n \"border-border bg-background shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:bg-accent/50 dark:[&:is(:hover,[data-pressed])]:bg-input/64\",\n secondary:\n \"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90\",\n },\n },\n },\n);\n\ninterface ButtonProps extends useRender.ComponentProps<\"button\"> {\n variant?: VariantProps<typeof buttonVariants>[\"variant\"];\n size?: VariantProps<typeof buttonVariants>[\"size\"];\n}\n\nfunction Button({ className, variant, size, render, ...props }: ButtonProps) {\n const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>[\"type\"] =\n render ? undefined : \"button\";\n\n const defaultProps = {\n className: cn(buttonVariants({ className, size, variant })),\n \"data-slot\": \"button\",\n type: typeValue,\n };\n\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(defaultProps, props),\n render,\n });\n}\n\nexport { Button, buttonVariants };\n",
"content": "\"use client\";\n\nimport { Button as ButtonPrimitive } from \"@base-ui-components/react/button\";\nimport { mergeProps } from \"@base-ui-components/react/merge-props\";\nimport { useRender } from \"@base-ui-components/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border bg-clip-padding font-medium text-sm outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default:\n \"min-h-8 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)]\",\n icon: \"size-8\",\n \"icon-lg\": \"size-9\",\n \"icon-sm\": \"size-7\",\n \"icon-xl\": \"size-10 [&_svg:not([class*='size-'])]:size-4.5\",\n \"icon-xs\":\n \"size-6 rounded-md before:rounded-[calc(var(--radius-md)-1px)]\",\n lg: \"min-h-9 px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2)-1px)]\",\n sm: \"min-h-7 gap-1.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)]\",\n xl: \"min-h-10 px-[calc(--spacing(4)-1px)] py-[calc(--spacing(2)-1px)] text-base [&_svg:not([class*='size-'])]:size-4.5\",\n xs: \"min-h-6 gap-1 rounded-md px-[calc(--spacing(2)-1px)] py-[calc(--spacing(1)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default:\n \"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none\",\n destructive:\n \"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none\",\n \"destructive-outline\":\n \"border-border bg-transparent text-destructive-foreground shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:border-destructive/32 [&:is(:hover,[data-pressed])]:bg-destructive/4\",\n ghost: \"border-transparent hover:bg-accent data-pressed:bg-accent\",\n link: \"border-transparent underline-offset-4 hover:underline\",\n outline:\n \"border-border bg-background shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:bg-accent/50 dark:[&:is(:hover,[data-pressed])]:bg-input/64\",\n secondary:\n \"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90\",\n },\n },\n },\n);\n\nfunction Button({\n className,\n variant,\n size,\n ...props\n}: ButtonPrimitive.Props &\n VariantProps<typeof buttonVariants> &\n React.ComponentPropsWithRef<typeof ButtonPrimitive>) {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-slot=\"button\"\n {...props}\n />\n );\n}\n\nfunction ButtonLink({\n className,\n variant,\n size,\n render,\n ...props\n}: useRender.ComponentProps<\"a\"> & VariantProps<typeof buttonVariants>) {\n const defaultProps = {\n className: cn(buttonVariants({ className, size, variant })),\n \"data-slot\": \"button\",\n };\n\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(defaultProps, props),\n render,\n });\n}\n\nexport { Button, ButtonLink, buttonVariants };\n",
"type": "registry:ui"
}
],
Expand Down
Loading
Loading