Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
32 changes: 32 additions & 0 deletions apps/ui/content/docs/(root)/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@ title: Changelog
description: Breaking changes, migration guides, and notable updates.
---

## August 17, 2026

### Tabs sizing and segmented control styles

**`TabsList`** now supports a **`size`** prop with **`"sm"`**, **`"default"`**, and **`"lg"`** values. The size propagates to its **`TabsTab`** children, and an individual tab can override the inherited size when necessary.

The default Tabs appearance now uses the same optical item sizing as the [Segmented Control](/ui/docs/components/segmented-control) pattern. The default rendered height is slightly smaller than before so the inset active indicator remains visually balanced beside a Button of the corresponding size.

Tabs now imports its size map from the **`@coss/segmented-control`** registry library. **`@coss/tabs`** declares this as a registry dependency, so the shadcn CLI installs it automatically with a fresh installation or CLI-managed update.

**Migration:**

Existing Tabs JSX requires no changes because **`"default"`** remains the default size. If you replace your local Tabs component through the CLI, review any local customizations before accepting the overwrite:

```bash
npx shadcn@latest add @coss/tabs
```

If you update **`tabs.tsx`** manually, install the shared library first and then merge the latest Tabs implementation. Copying only the new Tabs file without this dependency results in an unresolved import.

```bash
npx shadcn@latest add @coss/segmented-control
```

After updating, remove local height or horizontal-padding utilities from **`TabsList`** and **`TabsTab`** only where they unintentionally override the new **`size`** API. Keep intentional product-specific overrides.

**Agent migration prompt:**

```text
Update the local coss Tabs component to the latest segmented-control sizing while preserving project-specific customizations. Install the shared registry dependency with `npx shadcn@latest add @coss/segmented-control`, then merge the latest @coss/tabs implementation. TabsList now accepts size="sm" | "default" | "lg" (default: "default") and propagates it to TabsTab; a TabsTab size prop overrides the inherited value. Replace the old hard-coded TabsTab height and horizontal-padding classes with segmentedControlItemSizeClassNames from the shared library. Existing Tabs JSX does not need a size prop. Audit local h-* and px-* overrides on TabsList/TabsTab and remove only those that unintentionally conflict with the new size API. Run formatting and typechecking, then visually verify default and underline Tabs in every size used by the app.
```

## July 31, 2026

### Scroll Area — overscroll contain
Expand Down
1 change: 1 addition & 0 deletions apps/ui/content/docs/components/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"radio-group",
"scroll-area",
"select",
"segmented-control",
"separator",
"sheet",
"skeleton",
Expand Down
2 changes: 2 additions & 0 deletions apps/ui/content/docs/components/radio-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Individual radio button. Styled wrapper for `Radio.Root` from Base UI with built

For accessible labelling and validation, prefer using the `Field` component to wrap radio buttons. See the related example: [Radio Group field](/ui/docs/components/field#radio-group-field).

For tabs-style mutually exclusive choices, see the [Segmented Control](/ui/docs/components/segmented-control) pattern.

### Disabled

<ComponentPreview name="p-radio-group-2" />
Expand Down
114 changes: 114 additions & 0 deletions apps/ui/content/docs/components/segmented-control.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: Segmented Control
description: A visual pattern for presenting related choices, navigation destinations, filters, or content views.
---

<ComponentPreview name="p-radio-group-8" />

## About

A segmented control is a visual pattern, not a standalone behavior. COSS uses the same presentation across several components while preserving the semantics, keyboard interactions, and state model of each underlying primitive.

## Choose the right primitive

| Intent | Use | Why |
| ------ | --- | --- |
| Choose one value in a form | [Radio Group](/ui/docs/components/radio-group) | Represents a mutually exclusive value and participates in form state. |
| Navigate to another URL or route | Navigation links | Preserves link behavior, browser history, and `aria-current`. |
| Apply an exclusive filter or mode | [Toggle Group](/ui/docs/components/toggle-group) | Represents the pressed state of an action that may be cleared. |
| Switch between related panels | [Tabs](/ui/docs/components/tabs) | Connects each tab to an associated content panel. |

Choose the primitive from the interaction first, then apply the segmented-control styling. Visual similarity alone is not a reason to use Tabs or Toggle Group.

## Installation

Segmented controls are provided as particles. Install the implementation and size that match your interaction:

| Implementation | Small | Default | Large |
| -------------- | ----- | ------- | ----- |
| Radio Group | `@coss/p-radio-group-7` | `@coss/p-radio-group-8` | `@coss/p-radio-group-9` |
| Navigation | `@coss/p-navigation-2` | `@coss/p-navigation-1` | `@coss/p-navigation-3` |

For example, install the default Radio Group version with:

```bash
npx shadcn@latest add @coss/p-radio-group-8
```

The CLI installs the shared `segmented-control` styling library and the required primitive automatically.

## Shared styling

For a custom composition, install the styling library directly:

```bash
npx shadcn@latest add @coss/segmented-control
```

The library exports a root class and an item recipe:

```tsx
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control"
```

```tsx
const itemClassName = segmentedControlItemVariants({
size: "default",
state: "checked",
})
```

| Option | Values | Description |
| ------ | ------ | ----------- |
| `size` | `"sm" \| "default" \| "lg"` | Controls item height and horizontal padding. |
| `state` | `"checked" \| "current" \| "pressed"` | Selects the state attribute used by the underlying element. |

Use `checked` with Radio Group, `current` with navigation links, and `pressed` with Toggle Group. Tabs retain their own animated indicator and do not use the shared state recipe.

At the outside edges, the item padding and the surface's `p-0.5` inset combine to match the horizontal padding of the corresponding Button size. The outer segmented surface is slightly taller than that Button to optically balance its inset selected item when the controls appear next to each other.

<ComponentSource
name="segmented-control"
title="lib/segmented-control.ts"
/>

## Radio options

Use Radio Group when the selected segment represents a mutually exclusive value, especially in forms.

### Small Radio Group

<ComponentPreview name="p-radio-group-7" />

### Default Radio Group

<ComponentPreview name="p-radio-group-8" />

### Large Radio Group

<ComponentPreview name="p-radio-group-9" />

## Navigation

Use links when each segment points to a different destination. Apply `aria-current="page"` to the active link.

### Small Navigation

<ComponentPreview name="p-navigation-2" />

### Default Navigation

<ComponentPreview name="p-navigation-1" />

### Large Navigation

<ComponentPreview name="p-navigation-3" />

## Related content

Use Tabs when each segment controls an associated content panel. Tabs share the visual language of segmented controls but keep their animated indicator, orientation support, and panel semantics.

<ComponentPreview name="p-tabs-1" />
35 changes: 30 additions & 5 deletions apps/ui/content/docs/components/tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,25 @@ links:
npx shadcn@latest add @coss/tabs
```

The CLI installs the shared `@coss/segmented-control` registry dependency automatically.

</TabsPanel>

<TabsPanel value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>
<Step>Install the following dependencies:</Step>

```bash
npm install @base-ui/react class-variance-authority
```

<Step>Copy the shared segmented control styling into your project.</Step>

<ComponentSource name="segmented-control" title="lib/segmented-control.ts" />

<Step>Copy and paste the Tabs component into your project.</Step>

<ComponentSource name="tabs" title="components/ui/tabs.tsx" />

Expand Down Expand Up @@ -65,14 +77,15 @@ import { Tabs, TabsList, TabsPanel, TabsTab } from "@/components/ui/tabs"

Root component. Styled wrapper for `Tabs.Root` from Base UI.

| Prop | Type | Default | Description |
| --------- | ---------------------------- | ----------- | ------------------------------------------------ |
| `variant` | `"default" \| "underline"` | `"default"` | Controls the tabs styling |

### TabsList

Container for tab triggers. Styled wrapper for `Tabs.List` from Base UI.

| Prop | Type | Default | Description |
| --------- | -------------------------- | ----------- | ----------------------------------- |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Controls the size of all tab items. |
| `variant` | `"default" \| "underline"` | `"default"` | Controls the tabs styling. |

### TabsTab

Individual tab trigger. Styled wrapper for `Tabs.Tab` from Base UI.
Expand All @@ -87,6 +100,18 @@ Visual indicator for the active tab. Styled wrapper for `Tabs.Indicator` from Ba

## Examples

For guidance on choosing between Tabs, Radio Group, Toggle Group, and navigation links, see the [Segmented Control](/ui/docs/components/segmented-control) pattern.

Tabs use the same optical sizing as segmented controls. The default surface is slightly taller than a Button of the same size so its inset active indicator remains visually balanced alongside adjacent controls.

### Small

<ComponentPreview name="p-tabs-14" />

### Large

<ComponentPreview name="p-tabs-15" />

### Underline Variant

<ComponentPreview name="p-tabs-2" />
Expand Down
3 changes: 1 addition & 2 deletions apps/ui/lib/docs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const PAGES_NEW = [
// "/docs/components/{component-name}",
"/docs/components/context-menu",
"/docs/components/otp-field",
"/docs/components/segmented-control",
];
20 changes: 20 additions & 0 deletions apps/ui/public/r/p-navigation-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-navigation-1",
"description": "Segmented navigation built with links",
"registryDependencies": [
"@coss/segmented-control"
],
"files": [
{
"path": "registry/default/particles/p-navigation-1.tsx",
"content": "import {\n segmentedControlItemVariants,\n segmentedControlRootClassName,\n} from \"@/registry/default/lib/segmented-control\";\n\nconst itemClassName = segmentedControlItemVariants({ state: \"current\" });\n\nexport default function Particle() {\n return (\n <nav aria-label=\"Project sections\">\n <div className={segmentedControlRootClassName}>\n <a aria-current=\"page\" className={itemClassName} href=\"#overview\">\n Overview\n </a>\n <a className={itemClassName} href=\"#activity\">\n Activity\n </a>\n <a className={itemClassName} href=\"#settings\">\n Settings\n </a>\n </div>\n </nav>\n );\n}\n",
"type": "registry:block"
}
],
"categories": [
"navigation",
"segmented control"
],
"type": "registry:block"
}
20 changes: 20 additions & 0 deletions apps/ui/public/r/p-navigation-2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-navigation-2",
"description": "Small segmented navigation built with links",
"registryDependencies": [
"@coss/segmented-control"
],
"files": [
{
"path": "registry/default/particles/p-navigation-2.tsx",
"content": "import {\n segmentedControlItemVariants,\n segmentedControlRootClassName,\n} from \"@/registry/default/lib/segmented-control\";\n\nconst itemClassName = segmentedControlItemVariants({\n size: \"sm\",\n state: \"current\",\n});\n\nexport default function Particle() {\n return (\n <nav aria-label=\"Project sections\">\n <div className={segmentedControlRootClassName}>\n <a aria-current=\"page\" className={itemClassName} href=\"#overview\">\n Overview\n </a>\n <a className={itemClassName} href=\"#activity\">\n Activity\n </a>\n <a className={itemClassName} href=\"#settings\">\n Settings\n </a>\n </div>\n </nav>\n );\n}\n",
"type": "registry:block"
}
],
"categories": [
"navigation",
"segmented control"
],
"type": "registry:block"
}
20 changes: 20 additions & 0 deletions apps/ui/public/r/p-navigation-3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-navigation-3",
"description": "Large segmented navigation built with links",
"registryDependencies": [
"@coss/segmented-control"
],
"files": [
{
"path": "registry/default/particles/p-navigation-3.tsx",
"content": "import {\n segmentedControlItemVariants,\n segmentedControlRootClassName,\n} from \"@/registry/default/lib/segmented-control\";\n\nconst itemClassName = segmentedControlItemVariants({\n size: \"lg\",\n state: \"current\",\n});\n\nexport default function Particle() {\n return (\n <nav aria-label=\"Project sections\">\n <div className={segmentedControlRootClassName}>\n <a aria-current=\"page\" className={itemClassName} href=\"#overview\">\n Overview\n </a>\n <a className={itemClassName} href=\"#activity\">\n Activity\n </a>\n <a className={itemClassName} href=\"#settings\">\n Settings\n </a>\n </div>\n </nav>\n );\n}\n",
"type": "registry:block"
}
],
"categories": [
"navigation",
"segmented control"
],
"type": "registry:block"
}
21 changes: 21 additions & 0 deletions apps/ui/public/r/p-radio-group-7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-radio-group-7",
"description": "Small segmented control built with a radio group",
"registryDependencies": [
"@coss/radio-group",
"@coss/segmented-control"
],
"files": [
{
"path": "registry/default/particles/p-radio-group-7.tsx",
"content": "\"use client\";\n\nimport {\n segmentedControlItemVariants,\n segmentedControlRootClassName,\n} from \"@/registry/default/lib/segmented-control\";\nimport {\n RadioGroupPrimitive,\n RadioPrimitive,\n} from \"@/registry/default/ui/radio-group\";\n\nconst itemClassName = segmentedControlItemVariants({\n className: \"grow\",\n size: \"sm\",\n state: \"checked\",\n});\n\nexport default function Particle() {\n return (\n <RadioGroupPrimitive\n aria-label=\"Billing period\"\n className={segmentedControlRootClassName}\n defaultValue=\"monthly\"\n >\n <RadioPrimitive.Root className={itemClassName} value=\"monthly\">\n Monthly\n </RadioPrimitive.Root>\n <RadioPrimitive.Root className={itemClassName} value=\"yearly\">\n Yearly\n </RadioPrimitive.Root>\n </RadioGroupPrimitive>\n );\n}\n",
"type": "registry:block"
}
],
"categories": [
"radio group",
"segmented control"
],
"type": "registry:block"
}
21 changes: 21 additions & 0 deletions apps/ui/public/r/p-radio-group-8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-radio-group-8",
"description": "Segmented control built with a radio group",
"registryDependencies": [
"@coss/radio-group",
"@coss/segmented-control"
],
"files": [
{
"path": "registry/default/particles/p-radio-group-8.tsx",
"content": "\"use client\";\n\nimport {\n segmentedControlItemVariants,\n segmentedControlRootClassName,\n} from \"@/registry/default/lib/segmented-control\";\nimport {\n RadioGroupPrimitive,\n RadioPrimitive,\n} from \"@/registry/default/ui/radio-group\";\n\nconst itemClassName = segmentedControlItemVariants({\n className: \"grow\",\n state: \"checked\",\n});\n\nexport default function Particle() {\n return (\n <RadioGroupPrimitive\n aria-label=\"Billing period\"\n className={segmentedControlRootClassName}\n defaultValue=\"monthly\"\n >\n <RadioPrimitive.Root className={itemClassName} value=\"monthly\">\n Monthly\n </RadioPrimitive.Root>\n <RadioPrimitive.Root className={itemClassName} value=\"yearly\">\n Yearly\n </RadioPrimitive.Root>\n </RadioGroupPrimitive>\n );\n}\n",
"type": "registry:block"
}
],
"categories": [
"radio group",
"segmented control"
],
"type": "registry:block"
}
21 changes: 21 additions & 0 deletions apps/ui/public/r/p-radio-group-9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-radio-group-9",
"description": "Large segmented control built with a radio group",
"registryDependencies": [
"@coss/radio-group",
"@coss/segmented-control"
],
"files": [
{
"path": "registry/default/particles/p-radio-group-9.tsx",
"content": "\"use client\";\n\nimport {\n segmentedControlItemVariants,\n segmentedControlRootClassName,\n} from \"@/registry/default/lib/segmented-control\";\nimport {\n RadioGroupPrimitive,\n RadioPrimitive,\n} from \"@/registry/default/ui/radio-group\";\n\nconst itemClassName = segmentedControlItemVariants({\n className: \"grow\",\n size: \"lg\",\n state: \"checked\",\n});\n\nexport default function Particle() {\n return (\n <RadioGroupPrimitive\n aria-label=\"Billing period\"\n className={segmentedControlRootClassName}\n defaultValue=\"monthly\"\n >\n <RadioPrimitive.Root className={itemClassName} value=\"monthly\">\n Monthly\n </RadioPrimitive.Root>\n <RadioPrimitive.Root className={itemClassName} value=\"yearly\">\n Yearly\n </RadioPrimitive.Root>\n </RadioGroupPrimitive>\n );\n}\n",
"type": "registry:block"
}
],
"categories": [
"radio group",
"segmented control"
],
"type": "registry:block"
}
2 changes: 1 addition & 1 deletion apps/ui/public/r/p-tabs-1.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-tabs-1",
"description": "Basic tabs",
"description": "Segmented control built with tabs",
"registryDependencies": [
"@coss/tabs"
],
Expand Down
Loading
Loading