Skip to content
  •  
  •  
  •  
27 changes: 26 additions & 1 deletion apps/ui/content/docs/(root)/get-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ For new projects, use the **coss style** preset to initialize everything in one
npx shadcn@latest init @coss/style
```

This installs all UI components, the neutral color system, sidebar variables, font variables, and base styles. Components like Dialog and AlertDialog use `font-heading` for titles—the style preset provides sensible fallbacks, and you can customize fonts via `next/font` in your layout.
This installs all UI components, the neutral color system, sidebar variables, and base styles. It also installs **Inter** (`--font-sans`, `--font-heading`) and **Geist Mono** (`--font-mono`) as default fonts, automatically configured in your `layout.tsx`.

### Existing Projects

Expand Down Expand Up @@ -72,6 +72,31 @@ We've introduced a few additional tokens to provide more granular control:

**Important**: If you manually import components, you must also import these additional tokens in your CSS file (e.g., `app/globals.css`). However, if you use the CLI to import components, these tokens will be automatically imported and configured for you.

## Fonts

coss components use three CSS custom properties for typography:

- `--font-sans` — Body text, buttons, labels, and most UI elements
- `--font-mono` — Code blocks, `<kbd>`, monospace text
- `--font-heading` — Dialog and AlertDialog titles (defaults to Inter, same as `--font-sans`)

When you use `@coss/style`, `@coss/fonts` is installed automatically — Inter for `--font-sans` and `--font-heading`, Geist Mono for `--font-mono` — all wired in your `layout.tsx`.

### Custom fonts

To use different fonts, ensure the `variable` option in your `next/font` configuration matches the expected CSS variables:

```tsx
import { Inter } from "next/font/google";
import { JetBrains_Mono } from "next/font/google";

const inter = Inter({ variable: "--font-sans", subsets: ["latin"] });
const interHeading = Inter({ variable: "--font-heading", subsets: ["latin"] });
const jetbrainsMono = JetBrains_Mono({ variable: "--font-mono", subsets: ["latin"] });
```

**Note:** Next.js starters default to variable names like `--font-geist-sans`, which don't match coss's expected `--font-sans`. If fonts appear broken after setup, check that the variable names match.

## Primitive exports

Components that wrap Base UI primitives re-export the underlying primitive. Use the styled component when our defaults work, or the primitive when you need different compositions or styling. In a monorepo with a shared UI package, apps import from that package—Base UI stays in one place, no need to add it to each app.
Expand Down
46 changes: 31 additions & 15 deletions apps/ui/content/docs/(root)/radix-migration.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Migrating from Radix
slug: radix-migration
description: A comprehensive guide for migrating from Radix-based libraries to coss ui components.
description: A practical guide for migrating from Radix-based libraries to coss ui components.
---

This guide is for developers who already have applications built on Radix-based libraries — frameworks that wrap Radix primitives — and **want to adopt coss ui**.
Expand All @@ -22,7 +22,7 @@ This guide is for developers who already have applications built on Radix-based

### The asChild to render Pattern

The most common change across all components is replacing Radix UI's `asChild` prop with Base UI's `render` prop:
One of the most common migration changes is replacing Radix UI's `asChild` prop with Base UI's `render` prop (especially on trigger/close/composed parts):

<span data-lib="radix-ui">
```tsx title="Radix"
Expand All @@ -33,6 +33,8 @@ The most common change across all components is replacing Radix UI's `asChild` p
```
</span>

Not every component uses this pattern. Apply it where the coss component supports `render`.

<span data-lib="base-ui">
```tsx title="coss ui"
// [!code word:render]
Expand Down Expand Up @@ -608,15 +610,15 @@ Base UI introduces separate parts — `ProgressLabel`, `ProgressValue`, `Progres

**Prop Mapping:**

| Component | Radix UI Prop | Base UI Prop |
| ------------- | ---------------------- | --------------- |
| `Select` | `items` | `items` |
| `SelectValue` | `placeholder` | _removed_ |
| `SelectPopup` | `alignItemWithTrigger` | _no equivalent_ |
| Component | Radix UI Prop | coss ui / Base UI Prop |
| ------------- | ---------------------------------------------- | ------------------------------------------- |
| `Select` | options inferred from `SelectItem` children | `items` prop (recommended for SSR/hydration) |
| `SelectValue` | `placeholder` | `placeholder` (supported on `SelectValue`) |
| `SelectPopup` | _no equivalent_ | `alignItemWithTrigger` |

**Quick Checklist:**
- Set `items` prop on `Select`
- Remove `placeholder` from `Select`
- Keep placeholder on `SelectValue` when needed
- Prefer `SelectPopup` instead of `SelectContent`
- If you used `asChild` on parts, switch to the `render` prop

Expand All @@ -634,7 +636,9 @@ So, for example, if you were using the `default` size in shadcn/ui and you want

**Additional Notes:**

Base UI introduces the `alignItemWithTrigger` prop to control whether the `SelectContent` overlaps the `SelectTrigger` so the selected item's text is aligned with the trigger's value text.
`alignItemWithTrigger` controls whether the popup aligns the selected item with the trigger text. In coss ui this prop is available on `SelectPopup` (legacy alias: `SelectContent`).

`SelectValue` supports a `placeholder` prop. If you need in-popup clearing, prefer adding a `null` option item instead of relying on placeholder alone.

**Comparison Example:**

Expand All @@ -658,13 +662,15 @@ Base UI introduces the `alignItemWithTrigger` prop to control whether the `Selec
```tsx title="coss ui"
// [!code word:alignItemWithTrigger={false}]
// [!code word:items:2]
const items = [
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
]

<Select
items={[
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
]}
items={items}
>
<SelectTrigger>
<SelectValue />
Expand Down Expand Up @@ -955,6 +961,16 @@ So, for example, if you were using the `default` size in shadcn/ui and you want
```
</span>

## Verify After Migration

After converting components, run a quick verification pass:

- Typecheck and lint for prop/name mismatches (`*Content` vs `*Popup` / `*Panel`, `onSelect` vs `onClick`).
- Keyboard test overlays (open/close, focus trap, escape handling).
- Confirm accessible names (`aria-label` for icon-only controls, proper label associations in forms).
- For `Select`/`Command`, verify SSR/hydration behavior and selected label rendering.
- Compare visual density and adjust size variants when parity with previous UI is required.

## Additional Resources

- [Base UI Documentation](https://base-ui.com/) - Official Base UI docs
Expand Down
5 changes: 5 additions & 0 deletions apps/ui/content/docs/components/dialog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ npm install @base-ui/react
```tsx
import {
Dialog,
DialogClose,
DialogDescription,
DialogPanel,
DialogFooter,
Expand Down Expand Up @@ -87,6 +88,10 @@ Root component. Alias for `Dialog.Root` from Base UI.

Trigger button that opens the dialog. Alias for `Dialog.Trigger` from Base UI.

### DialogCreateHandle

Creates a handle for detached dialog triggers. Use it when the trigger and popup need to be coordinated across different parts of the tree.

### DialogPopup

Popup container that displays the dialog content. Also exported as `DialogContent`.
Expand Down
5 changes: 5 additions & 0 deletions apps/ui/content/docs/components/drawer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ npm install @base-ui/react
```tsx
import {
Drawer,
DrawerCreateHandle,
DrawerClose,
DrawerContent,
DrawerDescription,
Expand Down Expand Up @@ -101,6 +102,10 @@ Root component. Wraps `Drawer.Root` from Base UI with automatic `swipeDirection`

All other props from `Drawer.Root` are supported, including `open`, `onOpenChange`, `modal`, `snapPoints`, `snapPoint`, `onSnapPointChange`, and `snapToSequentialPoints`. Note: Snap points only work for bottom drawers.

### DrawerCreateHandle

Creates a handle for detached drawer triggers. Use it when triggers and drawer content need to be coordinated across different parts of the tree.

### DrawerTrigger

Trigger button that opens the drawer. Alias for `Drawer.Trigger` from Base UI.
Expand Down
38 changes: 18 additions & 20 deletions apps/ui/lib/registry.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import type { RegistryItem } from "shadcn/schema";
import { Project, ScriptKind } from "ts-morph";
import { Index } from "@/registry/__index__";

type RegistryFile = {
path: string;
type?: string;
target?: string;
content?: string;
};

export function getRegistryComponent(name: string) {
return Index[name]?.component;
}
Expand All @@ -18,15 +24,14 @@ export async function getRegistryItem(name: string) {

// Convert all file paths to object.
// TODO: remove when we migrate to new registry.
item.files = item.files.map((file: unknown) =>
typeof file === "string" ? { path: file } : file,
);

// Type assertion for now - TODO: implement proper validation
const typedItem = item as RegistryItem;
if (Array.isArray(item.files)) {
item.files = item.files.map((file: unknown) =>
typeof file === "string" ? { path: file } : file,
);
}

const files = typedItem.files || [];
const processedFiles = [];
const files: RegistryFile[] = Array.isArray(item.files) ? item.files : [];
const processedFiles: RegistryFile[] = [];

for (const file of files) {
const content = await getFileContent(file);
Expand All @@ -43,12 +48,12 @@ export async function getRegistryItem(name: string) {
const finalFiles = fixFilePaths(processedFiles);

return {
...typedItem,
...item,
files: finalFiles,
};
}

async function getFileContent(file: { path: string; type?: string }) {
async function getFileContent(file: RegistryFile) {
const raw = await fs.readFile(file.path, "utf-8");

const project = new Project({
Expand Down Expand Up @@ -80,7 +85,7 @@ async function getFileContent(file: { path: string; type?: string }) {
return code;
}

function getFileTarget(file: { path: string; type?: string; target?: string }) {
function getFileTarget(file: RegistryFile) {
let target = file.target;

if (!target || target === "") {
Expand Down Expand Up @@ -116,14 +121,7 @@ async function createTempSourceFile(filename: string) {
return path.join(dir, filename);
}

function fixFilePaths(
files: Array<{
path: string;
type?: string;
target?: string;
content?: string;
}>,
) {
function fixFilePaths(files: RegistryFile[]) {
if (!files) {
return [];
}
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"react": "^19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.3",
"shadcn": "^3.5.0",
"shadcn": "^4.1.0",
"tailwind-merge": "^3.4.0",
"ts-morph": "^27.0.2",
"zod": "^4.1.12"
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/public/r/accordion.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "accordion",
"type": "registry:ui",
"dependencies": [
"@base-ui/react"
],
Expand All @@ -11,5 +10,6 @@
"content": "\"use client\";\n\nimport { Accordion as AccordionPrimitive } from \"@base-ui/react/accordion\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport function Accordion(\n props: AccordionPrimitive.Root.Props,\n): React.ReactElement {\n return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nexport function AccordionItem({\n className,\n ...props\n}: AccordionPrimitive.Item.Props): React.ReactElement {\n return (\n <AccordionPrimitive.Item\n className={cn(\"border-b last:border-b-0\", className)}\n data-slot=\"accordion-item\"\n {...props}\n />\n );\n}\n\nexport function AccordionTrigger({\n className,\n children,\n ...props\n}: AccordionPrimitive.Trigger.Props): React.ReactElement {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n className={cn(\n \"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180\",\n className,\n )}\n data-slot=\"accordion-trigger\"\n {...props}\n >\n {children}\n <ChevronDownIcon\n className=\"pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-80 transition-transform duration-200 ease-in-out\"\n data-slot=\"accordion-indicator\"\n />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n}\n\nexport function AccordionPanel({\n className,\n children,\n ...props\n}: AccordionPrimitive.Panel.Props): React.ReactElement {\n return (\n <AccordionPrimitive.Panel\n className=\"h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0\"\n data-slot=\"accordion-panel\"\n {...props}\n >\n <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n </AccordionPrimitive.Panel>\n );\n}\n\nexport { AccordionPrimitive, AccordionPanel as AccordionContent };\n",
"type": "registry:ui"
}
]
],
"type": "registry:ui"
}
4 changes: 2 additions & 2 deletions apps/ui/public/r/alert-dialog.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "alert-dialog",
"type": "registry:ui",
"dependencies": [
"@base-ui/react"
],
Expand All @@ -11,5 +10,6 @@
"content": "\"use client\";\n\nimport { AlertDialog as AlertDialogPrimitive } from \"@base-ui/react/alert-dialog\";\nimport type React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport const AlertDialogCreateHandle: typeof AlertDialogPrimitive.createHandle =\n AlertDialogPrimitive.createHandle;\n\nexport const AlertDialog: typeof AlertDialogPrimitive.Root =\n AlertDialogPrimitive.Root;\n\nexport const AlertDialogPortal: typeof AlertDialogPrimitive.Portal =\n AlertDialogPrimitive.Portal;\n\nexport function AlertDialogTrigger(\n props: AlertDialogPrimitive.Trigger.Props,\n): React.ReactElement {\n return (\n <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />\n );\n}\n\nexport function AlertDialogBackdrop({\n className,\n ...props\n}: AlertDialogPrimitive.Backdrop.Props): React.ReactElement {\n return (\n <AlertDialogPrimitive.Backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0\",\n className,\n )}\n data-slot=\"alert-dialog-backdrop\"\n {...props}\n />\n );\n}\n\nexport function AlertDialogViewport({\n className,\n ...props\n}: AlertDialogPrimitive.Viewport.Props): React.ReactElement {\n return (\n <AlertDialogPrimitive.Viewport\n className={cn(\n \"fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4\",\n className,\n )}\n data-slot=\"alert-dialog-viewport\"\n {...props}\n />\n );\n}\n\nexport function AlertDialogPopup({\n className,\n bottomStickOnMobile = true,\n ...props\n}: AlertDialogPrimitive.Popup.Props & {\n bottomStickOnMobile?: boolean;\n}): React.ReactElement {\n return (\n <AlertDialogPortal>\n <AlertDialogBackdrop />\n <AlertDialogViewport\n className={cn(\n bottomStickOnMobile &&\n \"max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12\",\n )}\n >\n <AlertDialogPrimitive.Popup\n className={cn(\n \"relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg origin-center flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 sm:scale-[calc(1-0.1*var(--nested-dialogs))] sm:data-ending-style:scale-98 sm:data-starting-style:scale-98 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]\",\n bottomStickOnMobile &&\n \"max-sm:max-w-none max-sm:origin-bottom max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none\",\n className,\n )}\n data-slot=\"alert-dialog-popup\"\n {...props}\n />\n </AlertDialogViewport>\n </AlertDialogPortal>\n );\n}\n\nexport function AlertDialogHeader({\n className,\n ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n return (\n <div\n className={cn(\n \"flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left\",\n className,\n )}\n data-slot=\"alert-dialog-header\"\n {...props}\n />\n );\n}\n\nexport function AlertDialogFooter({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> & {\n variant?: \"default\" | \"bare\";\n}): React.ReactElement {\n return (\n <div\n className={cn(\n \"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]\",\n variant === \"default\" && \"border-t bg-muted/72 py-4\",\n variant === \"bare\" && \"pb-6\",\n className,\n )}\n data-slot=\"alert-dialog-footer\"\n {...props}\n />\n );\n}\n\nexport function AlertDialogTitle({\n className,\n ...props\n}: AlertDialogPrimitive.Title.Props): React.ReactElement {\n return (\n <AlertDialogPrimitive.Title\n className={cn(\n \"font-heading font-semibold text-xl leading-none\",\n className,\n )}\n data-slot=\"alert-dialog-title\"\n {...props}\n />\n );\n}\n\nexport function AlertDialogDescription({\n className,\n ...props\n}: AlertDialogPrimitive.Description.Props): React.ReactElement {\n return (\n <AlertDialogPrimitive.Description\n className={cn(\"text-muted-foreground text-sm\", className)}\n data-slot=\"alert-dialog-description\"\n {...props}\n />\n );\n}\n\nexport function AlertDialogClose(\n props: AlertDialogPrimitive.Close.Props,\n): React.ReactElement {\n return (\n <AlertDialogPrimitive.Close data-slot=\"alert-dialog-close\" {...props} />\n );\n}\n\nexport {\n AlertDialogPrimitive,\n AlertDialogBackdrop as AlertDialogOverlay,\n AlertDialogPopup as AlertDialogContent,\n};\n",
"type": "registry:ui"
}
]
],
"type": "registry:ui"
}
4 changes: 2 additions & 2 deletions apps/ui/public/r/alert.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "alert",
"type": "registry:ui",
"files": [
{
"path": "registry/default/ui/alert.tsx",
Expand All @@ -28,5 +27,6 @@
"warning": "var(--color-amber-500)",
"warning-foreground": "var(--color-amber-400)"
}
}
},
"type": "registry:ui"
}
Loading
Loading