

Cubitt components can mirror their state to TanStack Router search params through Cubitt's shared URL-state hooks. This gives you shareable URLs, bookmarkable states, and browser navigation that stays aligned with the UI without wiring every component manually.

## Requirements [#requirements]

Cubitt URL state requires TanStack Router context. If your app already uses TanStack Start or TanStack Router, there is no extra adapter to install.

```tsx title="app/root.tsx"
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";

export function App() {
  return <RouterProvider router={router} />;
}
```

### Type-safe `paramName` keys [#type-safe-paramname-keys]

Cubitt narrows `paramName` from your registered TanStack Router route tree.

* **TanStack Start / file-based routing**: route generation usually handles this automatically.
* **Effect in Cubitt**: built-in URL-state components can validate `paramName` at compile time, offer autocomplete for known keys, and stay safe through route refactors.

That means this:

```tsx
<Input paramName="search" />
```

is not just a string prop in a properly registered app. TypeScript can:

* suggest known search keys in autocomplete
* reject invalid param names at compile time
* keep those usages in sync if you rename a search key in the route tree

If `paramName` is only typed as plain `string` in your app, the usual cause is missing TanStack Router registration. In TanStack Start/file-based apps, check that your generated route types are present. In manual router setups, add the standard TanStack Router `Register` declaration once in your app.

### Escape hatch for unregistered keys [#escape-hatch-for-unregistered-keys]

If you intentionally need a query key that is not part of your registered route tree, wrap it with `untypedUrlStateParamName(...)`.

```tsx
import {
  Input,
  untypedUrlStateParamName,
} from "@tilt-legal/cubitt-components";

<Input paramName={untypedUrlStateParamName("debug-panel")} />;
```

<Callout type="warning" title="Lost type safety">
  `untypedUrlStateParamName(...)` opts that callsite out of TanStack Router key
  checking. You lose autocomplete for known keys and TypeScript can no longer
  catch typos or route-search refactors for that parameter. Prefer fixing router
  registration first and use this only for deliberate exceptions.
</Callout>

## Validation model [#validation-model]

Cubitt intentionally splits URL-state responsibilities:

* **Cubitt codecs** parse and serialize the individual URL parameter value.
* **Your route `validateSearch`** owns full-object validation, transforms, and defaults.

That keeps Cubitt route-agnostic while still giving strong typing and app-level search validation.

## Basic usage [#basic-usage]

Add URL state to any supported component by providing a `paramName`:

```tsx
<Input paramName="search" placeholder="Search..." />
```

When the user types, the URL updates to `?search=hello` and the input value stays synchronized with that parameter.

## Supported components [#supported-components]

**23 components** support URL state synchronization, organized by behavior:

### Display Components (Read URL State) [#display-components-read-url-state]

These components read open/closed state from the URL and expose matching helpers:

<Cards columns="3">
  <Card title="Dialog" description="Modal dialogs" href="/primitives/dialog" />

  <Card title="Sheet" description="Side sheets" href="/primitives/sheet" />

  <Card title="AlertDialog" description="Alert dialogs" href="/primitives/alert-dialog" />
</Cards>

#### Shared URL props [#shared-url-props]

| Option                | Type       | Description                                                                      | Default |
| --------------------- | ---------- | -------------------------------------------------------------------------------- | ------- |
| `paramName`           | `string`   | URL parameter name. Narrows to known search keys when your router is registered. | -       |
| `paramMatchValue`     | `string`   | Open only when the URL value matches this string                                 | -       |
| `onUrlValueChange`    | `function` | Callback when the parameter value changes                                        | -       |
| `paramClearOnDefault` | `boolean`  | Remove the parameter when the component returns to default                       | `true`  |
| `paramThrottle`       | `number`   | Throttle URL writes in milliseconds                                              | -       |
| `paramDebounce`       | `number`   | Debounce URL writes in milliseconds                                              | -       |

### Trigger Components (Write URL State) [#trigger-components-write-url-state]

These components only write to the URL when triggered:

<Cards columns="2">
  <Card title="Button" description="Action buttons that set URL parameters" href="/primitives/button" />

  <Card title="MenuItem" description="Menu options that write to the URL" href="/primitives/menu" />
</Cards>

#### Shared URL props [#shared-url-props-1]

| Option          | Type     | Description                                                                      | Default |
| --------------- | -------- | -------------------------------------------------------------------------------- | ------- |
| `paramName`     | `string` | URL parameter name. Narrows to known search keys when your router is registered. | -       |
| `paramSetValue` | `string` | Value to set in the URL when triggered                                           | -       |
| `paramThrottle` | `number` | Throttle URL writes in milliseconds                                              | -       |
| `paramDebounce` | `number` | Debounce URL writes in milliseconds                                              | -       |

### State Components (Two-Way URL Sync) [#state-components-two-way-url-sync]

These components fully synchronize their value with URL parameters:

#### Text Inputs [#text-inputs]

<Cards columns="3">
  <Card title="Input" description="Text input fields" href="/primitives/input" />

  <Card title="Textarea" description="Multi-line text input" href="/primitives/textarea" />

  <Card title="TagInput" description="Tag/chip input" href="/primitives/tag-input" />
</Cards>

#### Selection [#selection]

<Cards columns="3">
  <Card title="Select" description="Dropdown selection" href="/primitives/select" />

  <Card title="Combobox" description="Searchable dropdown with autocomplete" href="/primitives/combobox" />

  <Card title="RadioGroup" description="Single choice from options" href="/primitives/radio-group" />
</Cards>

#### Navigation [#navigation]

<Cards columns="4">
  <Card title="Tabs" description="Tab navigation" href="/primitives/tabs" />

  <Card title="Accordion" description="Expandable sections" href="/primitives/accordion" />

  <Card title="Pagination" description="Page navigation" href="/primitives/pagination" />

  <Card title="Stepper" description="Multi-step navigation" href="/primitives/stepper" />
</Cards>

#### Toggles [#toggles]

<Cards columns="4">
  <Card title="Checkbox" description="Boolean checkbox" href="/primitives/checkbox" />

  <Card title="Switch" description="On/off toggle" href="/primitives/switch" />

  <Card title="Toggle" description="Pressed/unpressed state" href="/primitives/toggle" />

  <Card title="ToggleGroup" description="Multiple toggle selection" href="/primitives/toggle-group" />
</Cards>

#### Numeric Input [#numeric-input]

<Cards columns="2">
  <Card title="Slider" description="Range slider for numeric values" href="/primitives/slider" />
</Cards>

#### Data Table Composites [#data-table-composites]

<Cards columns="2">
  <Card title="DataTable Search" description="Search rows with URL state" href="/composites/data-table/search" />

  <Card title="DataTable Column Visibility" description="Persist visible columns with useDataTableColumnVisibility" href="/composites/data-table/column-visibility" />
</Cards>

#### Shared URL props [#shared-url-props-2]

| Option                | Type               | Description                                                                      | Default |
| --------------------- | ------------------ | -------------------------------------------------------------------------------- | ------- |
| `paramName`           | `string`           | URL parameter name. Narrows to known search keys when your router is registered. | -       |
| `paramValue`          | `T`                | Controlled value for the URL parameter                                           | -       |
| `paramCodec`          | `UrlStateCodec<T>` | Custom codec for advanced values or custom parsing                               | auto    |
| `onUrlValueChange`    | `function`         | Callback when the parameter value changes                                        | -       |
| `paramClearOnDefault` | `boolean`          | Remove the parameter when the value equals default                               | `true`  |
| `paramThrottle`       | `number`           | Throttle URL writes in milliseconds                                              | -       |
| `paramDebounce`       | `number`           | Debounce URL writes in milliseconds                                              | -       |

## Custom value shapes [#custom-value-shapes]

Most primitives auto-select a codec from the value type:

* `string` → `stringCodec`
* `boolean` → `booleanCodec`
* `number` → `numberCodec`
* `string[]` → `stringArrayCodec`
* `number[]` → `numberArrayCodec`

For custom payloads, pass `paramCodec` with `createJsonCodec`.

```tsx
import { createJsonCodec } from "@tilt-legal/cubitt-components/utilities/hooks";

const filtersCodec = createJsonCodec<MyFilterState>((value) =>
  isValidFilterState(value) ? value : null
);

<SomeCustomComponent paramName="filters" paramCodec={filtersCodec} />;
```

## Custom components [#custom-components]

If you're building your own primitives or wrappers, use the shared hooks directly:

* [useUrlOpen](/hooks/url-state/use-url-open)
* [useUrlTrigger](/hooks/url-state/use-url-trigger)
* [useUrlState](/hooks/url-state/use-url-state)
* [useUrlControlledString](/hooks/url-state/use-url-controlled-string)
