URL State Management
Sync component state with TanStack Router search params for shareable, bookmarkable UI state.
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
Cubitt URL state requires TanStack Router context. If your app already uses TanStack Start or TanStack Router, there is no extra adapter to install.
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";
export function App() {
return <RouterProvider router={router} />;
}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
paramNameat compile time, offer autocomplete for known keys, and stay safe through route refactors.
That means this:
<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
If you intentionally need a query key that is not part of your registered route tree, wrap it with untypedUrlStateParamName(...).
import {
Input,
untypedUrlStateParamName,
} from "@tilt-legal/cubitt-components";
<Input paramName={untypedUrlStateParamName("debug-panel")} />;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.
Validation model
Cubitt intentionally splits URL-state responsibilities:
- Cubitt codecs parse and serialize the individual URL parameter value.
- Your route
validateSearchowns full-object validation, transforms, and defaults.
That keeps Cubitt route-agnostic while still giving strong typing and app-level search validation.
Basic usage
Add URL state to any supported component by providing a paramName:
<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
23 components support URL state synchronization, organized by behavior:
Display Components (Read URL State)
These components read open/closed state from the URL and expose matching helpers:
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)
These components only write to the URL when triggered:
Shared URL props
| 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)
These components fully synchronize their value with URL parameters:
Text Inputs
Selection
Select
Dropdown selection
Combobox
Searchable dropdown with autocomplete
RadioGroup
Single choice from options
Navigation
Toggles
Checkbox
Boolean checkbox
Switch
On/off toggle
Toggle
Pressed/unpressed state
ToggleGroup
Multiple toggle selection
Numeric Input
Data Table Composites
DataTable Search
Search rows with URL state
DataTable Column Visibility
Persist visible columns with useDataTableColumnVisibility
Shared URL props
| 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
Most primitives auto-select a codec from the value type:
string→stringCodecboolean→booleanCodecnumber→numberCodecstring[]→stringArrayCodecnumber[]→numberArrayCodec
For custom payloads, pass paramCodec with createJsonCodec.
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
If you're building your own primitives or wrappers, use the shared hooks directly: