# Changelog (/docs/changelog)
# Configuration (/docs/configuration)
## Component Customization [#component-customization]
shuip components are designed to be **easily customizable**. Each component accepts all the standard props of its base component, along with some specific ones.
### Method 1: Props and className [#method-1-props-and-classname]
The simplest way to customize a component:
```tsx
import { SubmitButton } from '@/components/ui/shuip/submit-button'
```
### Method 2: Editing the Source Code [#method-2-editing-the-source-code]
Since the components are copied directly into your project, you can freely modify them:
```tsx
// src/components/ui/shuip/submit-button.tsx
export function SubmitButton({
onClick,
label,
disabled,
loading,
icon = ,
...props
}: SubmitButtonProps) {
return (
);
}
```
# Contribution (/docs/contribution)
## Setup [#setup]
Clone the repository
```bash
git clone https://github.com/plvo/shuip.git
cd shuip
```
Install dependencies
```bash
bun install
```
Create a branch for your changes
```bash
git checkout -b feat/my-component
```
Start the development server
```bash
bun dev
```
## Repository layout [#repository-layout]
shuip is a Bun + turborepo monorepo. Items are published from `packages/registry/items/` through a generator script.
```bash
apps/docs/ # Next.js docs site (registry endpoints, MDX content)
packages/ui/ # shadcn primitives + base.css
packages/registry/ # SOURCE OF TRUTH for published items
items/// # One folder per item — see "Add an item" below
scripts/generate.ts # Scans items/, emits all downstream artifacts
packages/config/ # Shared tsconfig presets
```
The generator emits `registry.json`, `__index__.ts`, stubs, MDX symlinks, and `public/r/*.json` — **never edit those by hand**; they're regenerated from `items/`.
## Add an item [#add-an-item]
### Categories [#categories]
Each item lives at `packages/registry/items///`. Pick the category that matches what you're building:
| Category | Prefix | Used for |
| ----------------- | -------------------------- | ------------------------------------------------ |
| `components` | *(none)* | Standalone UI components |
| `blocks` | *(none, `registry:block`)* | Larger composition components |
| `react-hook-form` | `rhf-` | Fields integrated with react-hook-form |
| `tanstack-form` | `tsf-` | Fields integrated with @tanstack/react-form |
| `tanstack-query` | `tsq-` | Components integrated with @tanstack/react-query |
The folder name is **unprefixed** — the generator adds the prefix from the `CATEGORIES` map in `packages/registry/scripts/generate.ts`. Naming a folder `rhf-my-field` publishes as `rhf-rhf-my-field`.
### File structure [#file-structure]
```bash
packages/registry/items///
├── component.tsx # REQUIRED, exact filename. The published source.
├── default.example.tsx # Primary preview (recommended)
├── .example.tsx # Additional previews (recommended: at least one)
├── index.mdx # Doc page (skip for blocks — see below)
├── extras/ # Optional. Files copied alongside on install:
│ ├── .action.ts # → installs at ./actions/shuip/.ts
│ └── . # → installs at ./components/ui/shuip/.
└── meta.shuip.json # Optional. { "dependsOn": [""] }
```
### Detailed steps [#detailed-steps]
Create the folder `packages/registry/items///` (unprefixed name).
Write `component.tsx`. Import shadcn primitives via `@/components/ui/` (these become `registryDependencies` automatically). Don't import `@/components/ui/shuip/*` unless your item composes another shuip item — in that case, also declare it in `meta.shuip.json` with `dependsOn`.
Add `'use client'` at the top only when the component uses client-only React features (state, refs, effects, browser APIs).
Add `default.example.tsx`. Import your component via the **stub alias**, not via relative `./component`:
```tsx
import { MyComponent } from '@/components/ui/shuip//';
```
The `` segment is:
* `components/` items → no subdir: `@/components/ui/shuip/`
* `react-hook-form/` items → `@/components/ui/shuip/react-hook-form/`
* `tanstack-form/` items → `@/components/ui/shuip/tanstack-form/`
* `tanstack-query/` items → `@/components/ui/shuip/tanstack-query/`
* `blocks/` items → `@/components/block/shuip/` (note: `block`, not `ui`)
Add at least one variant example (`.example.tsx`) showing an alternate use-case. Doc pages with a single preview look thin.
Write `index.mdx` — for components, react-hook-form, tanstack-form, tanstack-query items.
```mdx
---
title: My Component
description: One-line description for SEO and the sidebar.
registryName:
---
Prose explaining what this item does and when to use it.
import { TypeTable } from 'fumadocs-ui/components/type-table';
## Examples
'} />
## Props
```
`` and `` are registered globally (no import needed). `` is from fumadocs-ui and must be imported inline as shown.
**Blocks are different** — do **not** put `index.mdx` in `items/blocks//`. Write the doc as a real MDX file at `apps/docs/content/blocks/.mdx` instead. Blocks live in a separate fumadocs collection (`apps/docs/source.config.ts`).
Regenerate the registry:
```bash
bun registry:generate
```
Confirm the output shows `[generate] N items processed` with N incremented by one. If you see `[generate] skipping /: no component.tsx`, the filename is wrong (must be exactly `component.tsx`).
End-to-end check:
```bash
bun build:docs
```
This chains `registry:generate` → `registry:build` → `next build`. If it passes, the item is installable via `npx shadcn@latest add "https://shuip.plvo.dev/r/"`.
## Coding conventions [#coding-conventions]
* **Biome** is the only linter/formatter. Single quotes, 2-space indent, 120-col, trailing commas. `bun check` runs Biome with auto-fix. Pre-commit hook runs `biome check --write --unsafe` via lint-staged.
* **TypeScript 6.** Use `import type` for type-only imports.
* **Shared external deps** are versioned via Bun catalogs (`workspaces.catalog` and `workspaces.catalogs.{fumadocs,radix,forms}` in root `package.json`). Reference as `"": "catalog:"` or `"catalog:"` — never pin a literal version in a workspace's `package.json`.
* **Conventional commits.** Format: `(): `. Examples: `feat(registry): add rhf-phone-field`, `fix(theme-button): add 'use client'`, `chore(dx): bump deps`.
## Submit your contribution [#submit-your-contribution]
```bash
git add .
git commit -m "feat(registry): add my-component"
git push origin feat/my-component
```
Then open a Pull Request on [github.com/plvo/shuip](https://github.com/plvo/shuip).
*Thank you for your contribution to shuip!* 🚀
# Introduction (/docs)
## Why shuip? [#why-shuip]
* **🏗️ Built on shadcn/ui**: Constructed on the solid foundation of shadcn/ui
* **🛡️ Native TypeScript**: Comes with types for a better developer experience
* **🎨 Customizable**: Styled with Tailwind CSS, easily adaptable to your design system
* **📦 Flexible Installation**: CLI to install only the components you need
* **🔧 Ready to Use**: Add, copy-paste, tweak, and use immediately
* **🎯 Concise Code**: Clear syntax and less boilerplate
## Who is shuip for? [#who-is-shuip-for]
shuip is perfect for:
* **React/Next.js developers** looking to speed up their workflow
* **Teams** aiming to standardize their UI components
* **Freelancers** who need to deliver high-quality projects quickly
* **Anyone** already using shadcn/ui and wanting to go further
## Philosophy [#philosophy]
shuip is built on a philosophy of **efficiency and optimization**. Eliminate repetitive tasks to focus on what truly matters in your application.
As developers, we spend too much time recreating the same UI components project after project — a submit button with loading state,
an input field with built-in validation, a confirmation dialog — these elements keep coming back, and implementing them takes valuable time that could be spent on the unique business logic of your app.
This is where the symbiotic relationship between **shuip** and **shadcn/ui** comes in. While **shadcn/ui** excels at providing solid, accessible UI primitives (``, ``, ``),
shuip turns these primitives into business-ready components. A `` becomes a `` with automatic loading state management.
An `` combined with form components becomes an `` with built-in Zod validation and error handling. A `` becomes a full-fledged `` with predefined title, description, and actions.
This approach offers a major advantage: you retain full control. Unlike traditional component libraries that lock you into their ecosystem, shuip copies the code directly into your project.
You can modify, adapt, and customize each component to your specific needs without being constrained by an external library. If you need a specific behavior for your ``,
just open the file and make the changes.
shuip integrates naturally into your existing **shadcn/ui** workflow. You continue using all the **shadcn/ui** primitives you're familiar with,
and progressively add shuip business components as needed.
Need a quick form? Install input-field and submit-button. Need user confirmations? Add confirmation-dialog.
This granularity keeps your bundle light by only including what you actually need.
*The ultimate goal of shuip is to help you **ship** faster.* 🚀
## Open Source [#open-source]
shuip is **100% open source** under the MIT license. You are free to:
* Use the components in your commercial projects
* Modify the code as needed
* Contribute to the project on GitHub
* Suggest new components
# Installation (/docs/installation)
## Prerequisites [#prerequisites]
Before installing shuip, make sure you have:
* [**Node.js 18+**](https://nodejs.org/en/download)
* [**Next.js 13+**](https://nextjs.org/docs/getting-started/installation)
* [**React 18+**](https://react.dev/learn/start-a-new-react-project)
* [**TypeScript**](https://www.typescriptlang.org/) *(recommended)*
* [**shadcn/ui**](https://ui.shadcn.com/docs/installation) installed and configured in your project
## Installing Components [#installing-components]
### Method 1: Install via CLI [#method-1-install-via-cli]
Use the shadcn/ui CLI to add the shuip components you want to your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://shuip.plvo.dev/r/input-field
```
```bash
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/input-field
```
```bash
yarn dlx shadcn@latest add https://shuip.plvo.dev/r/input-field
```
```bash
bun x shadcn@latest add https://shuip.plvo.dev/r/input-field
```
### Method 2: Manual Copy-Paste [#method-2-manual-copy-paste]
You can also copy and paste the code directly from each component’s documentation page.
## File Structure [#file-structure]
After installation, shuip components will be added to your project like this:
```bash
src/
├── components/
│ ├── block/
│ │ └── shuip/ # 🆕 shuip blocks
│ │ ├── title-section.tsx
│ ├── ui/
│ │ ├── button.tsx # shadcn/ui
│ │ ├── input.tsx # shadcn/ui
│ │ └── shuip/ # 🆕 shuip components
│ │ ├── input-field.tsx
│ │ ├── submit-button.tsx
│ │ └── confirmation-dialog.tsx
│ │ └── ...
```
## TypeScript Configuration [#typescript-configuration]
If you're using TypeScript, make sure your `tsconfig.json` includes the appropriate path aliases:
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"] // or "@/*": ["./*"] if your project is rooted at the top level
}
}
}
```
## Common Issues [#common-issues]
### Module Not Found Error [#module-not-found-error]
If you get a `Module not found` error, check the following:
1. That shadcn/ui is correctly installed
2. That path aliases are configured properly in `tsconfig.json`
3. That all required dependencies are installed
### Style Conflicts [#style-conflicts]
If you encounter style conflicts:
1. Ensure Tailwind CSS is properly configured
2. Verify that your `globals.css` includes shadcn/ui styles
3. Restart your development server
### Support [#support]
If you run into issues:
* [GitHub Issues](https://github.com/plvo/shuip/issues)
* [shadcn/ui Documentation](https://ui.shadcn.com/docs)
# Overview (/blocks)
# Kanban (/blocks/kanban)
`Kanban` is a generic board (`Kanban`) that renders any array of typed items into draggable columns. Columns, the property fields shown on each card, and the searchable fields are all configured against your own data shape — nothing is hardcoded to a domain.
### Built-in features [#built-in-features]
* **Generic data model**: works with any item type `T` via `idField` / `columnField`.
* **Structured cards**: the title is rendered from `title(item)` (as a `CardTitle`, so it can be a string, a link, or any element); the content area is driven by a `fields` config or fully customized with `cardContent`.
* **Inter and intra-column drag-and-drop**: move cards between columns and reorder within a column.
* **Hybrid state**: works standalone from `defaultData`, or controlled via `data` + `onDataChange`.
* **Global search**: filter cards across `searchableFields`.
* **Per-column extras**: count, a `renderColumnSummary` slot, color accent, and an add button.
> The card order is implicit in the `data` array order. On any move, the board emits the reordered array via `onDataChange` (and a semantic `onCardMove` event); map that to your own persistence.
> Define your item type with a `type` alias, not an `interface` — `Kanban` requires `T extends Record`, which a TypeScript `interface` does not satisfy.
## Examples [#examples]
## Props [#props]
# Responsive Dialog (/blocks/responsive-dialog)
## About [#about]
`ResponsiveDialog` is a smart dialog component that automatically adapts to the user's screen size:
* **Desktop (≥1024px)**: Renders as a `SideDialog` with flexible positioning
* **Mobile (\<1024px)**: Renders as a `Drawer` for better mobile UX
Unlike traditional dialogs that force the same experience across all devices, `ResponsiveDialog` provides the optimal interface for each screen size while maintaining a consistent API.
### Built-in features [#built-in-features]
* **Automatic responsive behavior**: Switches between side dialog and drawer based on screen size
* **6 positions (desktop)**: Choose from `top-left`, `top-right`, `bottom-left`, `bottom-right`, `left`, `right`
* **Flexible sizes (desktop)**: Predefined sizes (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`)
* **Scrollable body**: `ResponsiveDialogBody` keeps the header and footer anchored while the content scrolls on both desktop and mobile
* **Mobile-optimized**: Uses native drawer with swipe gestures on mobile
* **Lightweight**: Combines existing components without additional overhead
* **Accessible**: Inherits accessibility features from both SideDialog and Drawer
## Usage [#usage]
### Basic responsive dialog [#basic-responsive-dialog]
```tsx
Settings
Manage your preferences and account settings.
```
### With scrollable content [#with-scrollable-content]
Use `ResponsiveDialogBody` when the dialog content may overflow. The header and footer stay anchored while the body scrolls — on both desktop and mobile.
```tsx
Activity LogRecent actions across your workspace.
{/* long list, form, or any overflowing content */}
```
On mobile, `ResponsiveDialogBody` automatically adds horizontal padding (`px-4 py-3`) to match the drawer's layout. On desktop, padding comes from `SideDialogContent`.
> **Wrapper elements (e.g. `
);
}
```
`lens.focus('name')` only accepts real keys of `Values`, and the field's value type is inferred from there. Validation is whatever you wire on the form (here, a Zod resolver) — the field reads `fieldState.invalid` and renders the error for you.
## What you get [#what-you-get]
* **Typed field paths** — autocomplete from your form's value type, caught at compile time.
* **Consistent UI** — every field renders shadcn's `Field`, `FieldLabel`, `FieldDescription` and `FieldError` primitives.
* **Less boilerplate** — the field owns its `useController` wiring, error display and accessibility attributes.
Browse the individual fields in the sidebar under **React Hook Form fields**.
# TanStack Form (/components/tanstack-form)
## What is this? [#what-is-this]
TanStack Form is a headless UI library for building forms with strong TypeScript support and framework-agnostic validation. Unlike traditional form libraries that couple form state with UI components, TanStack Form handles only the state management—leaving you free to choose your UI layer.
These shuip components bridge TanStack Form with shadcn/ui, providing pre-built field components that reduce boilerplate and speed up development. They handle the repetitive setup work while preserving TanStack Form's type safety and validation capabilities.
## How it works under the hood [#how-it-works-under-the-hood]
When you use a field component like [`InputField`](/components/tanstack-form/input-field), it:
1. **Creates a `form.Field`** instance for your field name
2. **Accesses field state** via `field.state.value` and `field.state.meta`
3. **Wires up event handlers** (`field.handleChange`, `field.handleBlur`)
4. **Renders shadcn UI components** (`Field`, `FieldLabel`, `FieldError`, etc.)
5. **Manages validation** through the `formProps.validators` configuration
The field's `state.meta` object contains:
* `errors`: array of validation error messages
* `isTouched`: whether the user has interacted with the field
* `isDirty`: whether the value differs from initial
* `isValidating`: whether async validation is in progress
* `isValid`: whether all validators pass
## Integration with shadcn/ui [#integration-with-shadcnui]
All field components are built on shadcn's primitives:
* **`Field`**: Wrapper component that manages invalid states
* **`FieldLabel`, `FieldDescription`, `FieldError`**: Consistent field UI
* **`InputGroup`**: For password toggle buttons, tooltips, and addons
* **`Select`, `RadioGroup`, `Checkbox`**: Radix UI form primitives
This ensures visual consistency with your shadcn-based application while adding TanStack Form's powerful state management.
## Type safety [#type-safety]
Full TypeScript support with automatic type inference:
```tsx
type UserForm = {
user: {
email: string
profile: {
age: number
}
}
}
const form = useForm({
defaultValues: {
user: { email: '', profile: { age: 0 } }
}
})
// TypeScript autocompletes nested paths: 'user.email', 'user.profile.age'
```
The `name` prop uses `DeepKeys` for compile-time validation of field paths, and values are typed as `DeepValue` for correct inference.
## Validation system [#validation-system]
TanStack Form supports multiple validation strategies:
```tsx
!value.includes('@') ? 'Invalid email' : undefined,
// Runs when field loses focus
onBlur: ({ value }) =>
!value ? 'Email is required' : undefined,
// Async validation with debounce
onChangeAsync: async ({ value }) => {
const available = await checkEmailAvailability(value)
return available ? undefined : 'Email already taken'
},
onChangeAsyncDebounceMs: 500,
// Runs on form submission
onSubmit: ({ value }) =>
!value ? 'This field is required' : undefined
}
}}
/>
```
Validators receive `{ value, fieldApi }` and must return `undefined` for valid or an error string for invalid.
## Linked field validation [#linked-field-validation]
Use `onChangeListenTo` to validate one field based on another's value:
```tsx
{
const password = fieldApi.form.getFieldValue('password')
return value !== password ? 'Passwords do not match' : undefined
}
}
}}
/>
```
When `password` changes, `confirmPassword` is automatically re-validated.
## Form submission [#form-submission]
Handle form submission with the `onSubmit` callback:
```tsx
const form = useForm({
defaultValues: { email: '', password: '' },
onSubmit: async ({ value }) => {
// value is fully typed based on defaultValues
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(value),
})
if (!response.ok) {
// Handle server errors
}
},
// Optional: handle submit errors
onSubmitInvalid: ({ formApi }) => {
toast.error('Please fix form errors')
}
})
```
Use `` to get automatic loading states and disabled handling.
# TanStack Query (/components/tanstack-query)
These utilities integrate [TanStack Query](https://tanstack.com/query) with shuip. Instead of manually nesting an error boundary and a Suspense boundary around every data-fetching component, you wrap them once with [`QueryBoundary`](/components/tanstack-query/query-boundary).
## How it works [#how-it-works]
`QueryBoundary` combines an error boundary with React Suspense, so the children can use suspense-enabled queries and let the boundary handle the loading and error UI:
```tsx
import { QueryBoundary } from '@/components/ui/shuip/tanstack-query/query-boundary';
export default function Page() {
return (
Loading users…}>
);
}
function UsersList() {
const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
return
{data.map((user) =>
{user.name}
)}
;
}
```
On error the boundary shows a retry fallback that resets the failing query; you can replace it with your own `errorFallback`. This keeps data components focused on the happy path while loading and failure states stay consistent across the app.
Browse the component in the sidebar under **TanStack Query**.
# Confirmation Dialog (/components/confirmation-dialog)
## Simple integration [#simple-integration]
```tsx
```
With `ConfirmationDialog`, the same result in a clean, reusable component:
```tsx
Delete}
title="Are you sure?"
description="This action cannot be undone. This will permanently delete the item."
labelConfirmButton="Delete"
onConfirm={handleDelete}
/>
```
## Common use cases [#common-use-cases]
### Delete confirmation [#delete-confirmation]
```tsx
Delete Account}
title="Delete Account"
description="This will permanently delete your account and all associated data. This action cannot be undone."
labelConfirmButton="Delete Account"
onConfirm={() => deleteAccount()}
/>
```
### Save confirmation [#save-confirmation]
```tsx
Save Changes}
title="Save Changes"
description="Are you sure you want to save these changes?"
labelConfirmButton="Save"
onConfirm={() => saveChanges()}
/>
```
### Custom trigger with icon [#custom-trigger-with-icon]
```tsx
}
title="Delete Item"
description="This item will be permanently removed."
labelConfirmButton="Delete"
onConfirm={() => deleteItem(id)}
/>
```
## Examples [#examples]
## Props [#props]
# Copy Button (/components/copy-button)
## Examples [#examples]
## Props [#props]
# Hover Reveal (/components/hover-reveal)
## Common use cases [#common-use-cases]
```tsx
// Help text on hover
```
### User profile preview [#user-profile-preview]
```tsx
// User profile preview
{user.initials}
This feature is available for premium subscribers.
Upgrade your plan to access advanced analytics.
}
>
```
### Quick preview [#quick-preview]
```tsx
}
>
View image preview
```
## Examples [#examples]
## Props [#props]
# Side Dialog (/components/side-dialog)
## Contextual positioning [#contextual-positioning]
Unlike traditional centered dialogs, `SideDialog` provides a more contextual user experience by opening from the edges of the screen, perfect for notifications, user menus, and contextual actions.
### Built-in features [#built-in-features]
* **6 positions**: Choose from `top-left`, `top-right`, `bottom-left`, `bottom-right`, `left`, `right`
* **Flexible sizes**: Predefined sizes (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`)
* **Scrollable body**: `SideDialogBody` keeps the header and footer anchored while the content scrolls
* **Lightweight**: Custom implementation without depending on shadcn dialog
* **Accessible**: Keyboard support (Escape), focus management, and ARIA compliance
* **Portal rendering**: Renders in `document.body` to avoid z-index issues
* **Smooth animations**: Entry/exit animations with Tailwind CSS
* **Controlled & Uncontrolled**: Works with or without state management
## Position options [#position-options]
The `position` prop controls where the dialog appears:
```tsx
// Corner positions
{/* Top left corner */}
{/* Top right corner */}
{/* Bottom left corner */}
{/* Bottom right corner (default) */}
// Side positions (vertically centered)
{/* Left side, centered */}
{/* Right side, centered */}
```
## Size options [#size-options]
The `size` prop controls the dialog size with predefined responsive values:
```tsx
// Predefined sizes (all automatically responsive)
{/* 320px max-width, adapts to small screens */}
{/* 384px max-width (default) */}
{/* 448px max-width */}
{/* 512px max-width */}
{/* 576px max-width */}
{/* 672px max-width */}
// For custom sizes, use className with responsive constraints
{/* Custom size with responsive protection */}
```
### Responsive behavior [#responsive-behavior]
All predefined sizes automatically include responsive constraints using CSS `min()` function:
* **Desktop**: Uses the specified max-width (e.g. `lg` = 32rem/512px)
* **Small screens**: Automatically reduces to `calc(100vw - 2rem)` when viewport is smaller
* **Never overflows**: The dialog will always fit within the viewport with 1rem margin on each side
* **CSS native**: Uses `min(targetSize, calc(100vw - 2rem))` for optimal performance
## Scrollable content [#scrollable-content]
Use `SideDialogBody` when the dialog content may overflow. The header and footer stay anchored while the body scrolls independently.
```tsx
Activity LogRecent actions across your workspace.
{/* long list or form content */}
```
Without `SideDialogBody`, content simply stacks inside the dialog — fine for short dialogs. Add it as soon as the content might exceed the viewport height.
## Common use cases [#common-use-cases]
### Notifications and alerts [#notifications-and-alerts]
```tsx
// Success notification - top right
Success!Your changes have been saved.
// Error notification - top left
ErrorSomething went wrong. Please try again.
```
### Different sizes for different content [#different-sizes-for-different-content]
```tsx
// Small notification - xs size
Saved!
// User profile - medium size
User Profile
```
### Help and information [#help-and-information]
```tsx
// Help panel - bottom left
Need Help?
Find answers to common questions or contact support.
```
## State management [#state-management]
The component supports both controlled and uncontrolled modes, automatically detecting which mode to use based on the props provided:
```tsx
// Uncontrolled: Component manages its own state
Self-managed State
// Controlled: You manage the state
const [open, setOpen] = useState(false);
Controlled State
```
## Advanced customization [#advanced-customization]
### Custom close behavior [#custom-close-behavior]
```tsx
// Disable the default close button
Custom Controls
```
### Custom styling [#custom-styling]
```tsx
// Custom dialog appearance
Custom Style
```
## Examples [#examples]
## Props [#props]
## SideDialogContent Props [#sidedialogcontent-props]
## SideDialogBody Props [#sidedialogbody-props]
## SideDialogTrigger Props [#sidedialogtrigger-props]
# Submit Button (/components/submit-button)
### Built-in features [#built-in-features]
* **Automatic loading state**: Shows spinner icon when `loading` prop is true
* **Full-width by default**: Optimized for form layouts
* **Customizable icon**: Can override the default loading spinner
* **Form integration**: Native `type="submit"` behavior
## Common use cases [#common-use-cases]
### Basic form submission [#basic-form-submission]
```tsx
Send Message
```
### With custom icon [#with-custom-icon]
```tsx
}
/>
```
### With JSX label [#with-jsx-label]
```tsx
Send Message
}
loading={isSending}
/>
```
### Custom styling [#custom-styling]
```tsx
```
## Examples [#examples]
## Props [#props]
# Theme Button (/components/theme-button)
## Ready-to-use theme toggle [#ready-to-use-theme-toggle]
The button automatically cycles through these states:
* **System**: Uses the user's OS preference (🖥️ Laptop icon)
* **Light**: Forces light mode (☀️ Sun icon)
* **Dark**: Forces dark mode (🌙 Moon icon)
With `ThemeButton`, a complete theme switcher in one line:
### With ThemeButton - simple and complete [#with-themebutton---simple-and-complete]
```tsx
```
### With text label [#with-text-label]
```tsx
```
## Setup with next-themes [#setup-with-next-themes]
First, make sure you have [`next-themes`](https://ui.shadcn.com/docs/dark-mode/next) installed and configured:
## Examples [#examples]
## Props [#props]
# Address Field (/components/react-hook-form/address-field)
`AddressField` is a compound field component that manages a full `AddressData` sub-tree (street, city, postal code, country, full address, place ID) under a single name prefix. It wires Google Places autocomplete to the form so that selecting a suggestion writes every sub-field at once.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses). The parent passes `lens={lens.focus('address')}` — a `Lens` — and the component focuses internally on each sub-field. No string paths, no `useFormContext`, no name-prefix hacks.
#### Built-in features [#built-in-features]
* **Compound field**: manages `street`, `city`, `postalCode`, `country`, `fullAddress`, and `placeId` under a single sub-tree
* **Typed lens binding**: `lens.focus('address')` autocompletes from your form's value type — no `` generic at the call site
* **Google Places autocomplete**: server action, popover dropdown, keyboard navigation, debounced search
* **Zod schema export**: `addressSchema` is published alongside the component for easy composition (`z.object({ address: addressSchema })`)
## Configuration [#configuration]
This component requires a Google Places API key. Add `GOOGLE_PLACES_API_KEY` to your `.env` file:
```bash
GOOGLE_PLACES_API_KEY=your_api_key_here
```
To customize the default country, language, or debounce, edit the constants in the component:
```tsx
const DEFAULT_COUNTRY = 'US'; // Country code for suggestions
const LANGUAGE_RESULT = 'en'; // Language code for results
const DEBOUNCE_TIME = 300; // API call delay in milliseconds
```
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form, then focus on the address sub-tree:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Form } from '@/components/ui/form';
import { AddressField, addressSchema } from '@/components/ui/shuip/react-hook-form/address-field';
const schema = z.object({ address: addressSchema });
const form = useForm>({
resolver: zodResolver(schema),
defaultValues: {
address: { street: '', city: '', postalCode: '', country: '', fullAddress: '' },
},
});
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides shadcn's `FormProvider` which `FormLabel` and `FormMessage` use internally.
## Compound field [#compound-field]
Unlike a single-input field, `AddressField` writes to every sub-field of the `AddressData` sub-tree when the user picks a place suggestion. Internally, it derives one controller per sub-field from the lens:
```tsx
const fullAddress = useController(lens.focus('fullAddress').interop());
const street = useController(lens.focus('street').interop());
const city = useController(lens.focus('city').interop());
const postalCode = useController(lens.focus('postalCode').interop());
const country = useController(lens.focus('country').interop());
const placeId = useController(lens.focus('placeId').interop());
```
When Google Places returns details for a selected suggestion, each `field.onChange(...)` populates its sub-field — no global `setValue` call against string paths.
## Less boilerplate [#less-boilerplate]
Traditional address forms require multiple fields and manual validation:
```tsx
// ...manual validation and state management
```
With `AddressField`, this reduces to a single component with automatic validation and place lookup:
```tsx
```
## Schema composition [#schema-composition]
`addressSchema` is exported alongside the component so consumers can compose it into a larger form schema:
```tsx
import { addressSchema } from '@/components/ui/shuip/react-hook-form/address-field';
const schema = z.object({
customer: z.string().min(1),
shipping: addressSchema,
billing: addressSchema,
});
```
The component returns structured address data on submit:
```tsx
{
address: {
street: '123 Main St',
city: 'Paris',
postalCode: '75001',
country: 'France',
fullAddress: '123 Main St, 75001 Paris, France',
placeId: 'abcdef123456',
}
}
```
## Examples [#examples]
## Props [#props]
# Autocomplete Field (/components/react-hook-form/autocomplete-field)
`AutocompleteField` is a text input that proposes matching strings as the user types, while still allowing any free-text value. It binds to React Hook Form through `@hookform/lenses` (`lens` prop) and stores a plain `string`.
Use it when a field has a set of common values worth suggesting (sources, tags, cities…) but you don't want to force the user to pick from the list.
#### Built-in features [#built-in-features]
* **Two suggestion modes**: a static `suggestions` array (filtered client-side) or an async `onSearch` function (debounced fetch).
* **Free text**: the typed value is always kept — closing the dropdown without selecting commits what was typed.
* **Keyboard navigation**: ArrowUp/ArrowDown to move, Enter to select, Escape to close.
* **Custom matching**: override the default case-insensitive substring match via `filter` (static mode).
## Examples [#examples]
## Props [#props]
# Checkbox Field (/components/react-hook-form/checkbox-field)
`CheckboxField` is a boolean input component that encapsulates React Hook Form's field management with shadcn/ui's design system. It combines Radix UI's Checkbox with an inline clickable label, making the entire label area interactive — not just the checkbox itself.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('agree')` autocompletes from your form's value type — no `` generic at the call site
* **Clickable inline label**: positioned next to the checkbox for a larger hit target
* **Boolean field type**: bound via `checked` / `onCheckedChange` — no manual event wiring
* **Required validation**: pair with Zod's `.refine()` for terms acceptance flows
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { CheckboxField } from '@/components/ui/shuip/react-hook-form/checkbox-field';
const form = useForm({ defaultValues: { agree: false } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides shadcn's `FormProvider` which `FormLabel` and `FormMessage` use internally.
## Less boilerplate [#less-boilerplate]
React Hook Form's standard approach uses render props to access field state:
```tsx
(
I accept the terms and conditions
)}
/>
```
With `CheckboxField`, this reduces to a single declarative component:
```tsx
```
## Examples [#examples]
## Required checkbox [#required-checkbox]
For terms acceptance, validate that the value is `true` with Zod's `.refine()`:
```tsx
const schema = z.object({
terms: z.boolean().refine((val) => val === true, {
message: 'You must accept the terms and conditions',
}),
});
const form = useForm>({
defaultValues: { terms: false },
resolver: zodResolver(schema),
});
const lens = useLens({ control: form.control });
```
## Props [#props]
# Date Field (/components/react-hook-form/date-field)
`DateField` is a single-date picker that encapsulates React Hook Form's field management with the shadcn `Calendar` primitive. The trigger is an outline `Button` (full-width) that shows the selected date formatted via [`date-fns`](https://date-fns.org); clicking it opens a `Popover` containing the calendar.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses). The value type is `Date | undefined` — `undefined` represents an empty selection (matching `react-day-picker`'s convention).
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('dueDate')` — `Lens` is enforced at compile time
* **Locale-aware formatting**: defaults to `en-US`; pass any `date-fns/locale` via the `locale` prop
* **Bounded selection**: `minDate` / `maxDate` disable days outside the range in the calendar UI
* **Accessible trigger**: a real `
```
The `
` wrapper is required — it provides React Hook Form's `FormProvider` context.
## Bounded selection [#bounded-selection]
Use `minDate` and `maxDate` to disable out-of-range days directly in the calendar:
```tsx
```
Disabled days remain visible but cannot be selected. Validate the same bounds in your schema so submission also rejects out-of-range values.
## Examples [#examples]
## Props [#props]
# Date Range Field (/components/react-hook-form/date-range-field)
`DateRangeField` is a from–to date picker bound to React Hook Form. It wraps the shadcn `Calendar` primitive in `mode='range'` inside a `Popover`, and binds via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no string paths, no `` generic at the call site.
The bound value is a `DateRange` from `react-day-picker`:
```ts
type DateRange = { from?: Date; to?: Date } | undefined;
```
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('range')` autocompletes from your form's value type
* **Two-month side-by-side display**: canonical UX for picking a range that crosses months
* **Bounded selection**: `minDate` / `maxDate` translate to the Calendar `disabled` matcher
* **Locale aware**: optional `date-fns` `Locale` passed through to both label formatting and the underlying Calendar
## Setup [#setup]
The field binds via `@hookform/lenses`. Declare the value as an optional `DateRange` in your schema, then focus the lens on the range field:
```tsx
import type { DateRange } from 'react-day-picker';
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { DateRangeField } from '@/components/ui/shuip/react-hook-form/date-range-field';
type Values = { stay: DateRange | undefined };
const form = useForm({ defaultValues: { stay: undefined } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides React Hook Form's `FormProvider` context.
## Trigger label [#trigger-label]
The trigger button renders a label derived from the current value:
* both ends set → `" – "` using `date-fns` `format(date, 'PPP', { locale })`
* only `from` set → just the `from` date
* neither set → the `placeholder` prop
## Validation [#validation]
For a required range, pair the field with a Zod schema that enforces both ends and the ordering. See the `validation` example below.
```tsx
const schema = z.object({
range: z
.object({
from: z.date({ message: 'Start date is required' }),
to: z.date({ message: 'End date is required' }),
})
.refine((value) => value.to >= value.from, {
message: 'End date must be on or after the start date',
path: ['to'],
}),
});
```
## Examples [#examples]
## Props [#props]
# Datetime Field (/components/react-hook-form/datetime-field)
`DatetimeField` is a single-value date + time picker that stores its selection as one `Date` carrying both the day and the hours/minutes. It wraps shadcn's `Calendar` and a native `` inside a `Popover`, and binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses).
For date-only fields, use `Calendar` directly. This component is for cases where the time of day matters as much as the date — meeting start, deadline, deliverable, etc.
#### Built-in features [#built-in-features]
* **Single `Date` value**: day + hours + minutes live in one field — no separate date/time pieces to reconcile
* **Typed lens binding**: `lens.focus('scheduledAt')` autocompletes from your form's value type
* **Locale-aware label**: trigger renders via `date-fns` `format(value, 'PPp', { locale })`
* **Bounds**: `minDate` / `maxDate` disable out-of-range days on the calendar
* **Time step**: native `` for minute or second granularity
## Setup [#setup]
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form } from '@/components/ui/form';
import { DatetimeField } from '@/components/ui/shuip/react-hook-form/datetime-field';
const schema = z.object({ scheduledAt: z.date() });
type Values = z.infer;
const form = useForm({ defaultValues: { scheduledAt: undefined } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides React Hook Form's `FormProvider` context.
## Behavior [#behavior]
* **First date pick:** when the field is empty and the user picks a day, hours and minutes default to `00:00`. When the field already has a value, the existing hours/minutes are preserved.
* **Time change:** only the hours/minutes are updated; the date portion is preserved.
* **Trigger label:** `format(value, 'PPp', { locale })` — e.g. `May 22, 2026, 2:30 PM`. When empty, falls back to `placeholder`.
## Examples [#examples]
## Props [#props]
# Inline Edit Field (/components/react-hook-form/inline-edit)
A click-to-edit field for React Hook Form. The value displays as text and turns into an
editable input on click; read and edit share one typography scale, so editing is seamless.
On each commit the field writes its value to the form and validates it — an invalid value
keeps the field in edit mode with the error shown inline.
Persisting is the form's job, not the field's: subscribe to the form with `form.watch` (or
submit it). The example below autosaves the whole form to `localStorage` from a single
`watch` subscription, so one handler covers every field.
## Examples [#examples]
## Props [#props]
# Input Field (/components/react-hook-form/input-field)
`InputField` is a text input component that encapsulates React Hook Form's field management with shadcn/ui's design system. It handles all the boilerplate of connecting form state to an input element: wiring event handlers, displaying errors, managing touched states, and rendering consistent UI.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
For numeric inputs, use [`NumberField`](/components/react-hook-form/number-field) instead.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('name')` autocompletes from your form's value type — no `` generic at the call site
* **Tooltip integration**: optional InfoIcon button with tooltip content via `tooltip` prop
* **InputGroup ready**: built on shadcn InputGroup for seamless addon integration
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
const form = useForm({ defaultValues: { email: '' } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides React Hook Form's `FormProvider` context.
## Less boilerplate [#less-boilerplate]
React Hook Form's standard approach uses render props to access field state:
```tsx
(
EmailYour email address
{fieldState.invalid && }
)}
/>
```
With `InputField`, this reduces to a single declarative component:
```tsx
```
## Examples [#examples]
## Props [#props]
# Month Field (/components/react-hook-form/month-field)
`MonthField` is a month picker that wraps the shadcn `Calendar` primitive inside a `Popover`. The user picks any day in a month and the field stores the **first day of that month** as a `Date` — perfect for "billing month", "report period", "subscription start" use cases where the day component is noise.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses): `lens.focus('billingMonth')` autocompletes from your form's value type — no `` generic at the call site.
#### Built-in features [#built-in-features]
* **First-day normalization**: any day the user clicks is internally rewritten to `new Date(year, month, 1)`, so the stored value behaves as a month bucket
* **Dropdown caption**: month and year dropdowns in the calendar header (`captionLayout='dropdown'`)
* **Bounded range**: `minDate` / `maxDate` drive both the calendar's disabled days and its dropdown bounds via `startMonth` / `endMonth`
* **Locale aware**: `locale` is forwarded to `date-fns` `format` and to the calendar
* **Typed lens binding**: `lens.focus('month')` autocompletes from your form's value type
## Setup [#setup]
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { MonthField } from '@/components/ui/shuip/react-hook-form/month-field';
type Values = { billingMonth: Date | undefined };
const form = useForm({ defaultValues: { billingMonth: undefined } });
const lens = useLens({ control: form.control });
```
## Value shape [#value-shape]
The stored value is always either `undefined` or a `Date` whose day component is `1`. Consumers can compare months by `value.getTime()` without worrying about time-of-day drift:
```ts
const a = new Date(2026, 4, 1); // May 2026
const b = new Date(2026, 4, 1); // May 2026
a.getTime() === b.getTime(); // true
```
To format the stored value elsewhere, use the same `'MMMM yyyy'` pattern from `date-fns`:
```ts
import { format } from 'date-fns';
format(values.billingMonth, 'MMMM yyyy'); // 'May 2026'
```
## Examples [#examples]
## Props [#props]
# Number Field (/components/react-hook-form/number-field)
`NumberField` binds a numeric form field (`Lens`) to a `type='number'` or `type='range'` input. The input writes back `e.target.valueAsNumber` so the form state stays correctly typed as `number`. Empty input becomes `NaN`, which validators can check with `Number.isNaN(value)`.
For text fields, use [`InputField`](/components/react-hook-form/input-field) instead.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('quantity')` requires a numeric form field — typing is enforced
* **`valueAsNumber` write-back**: form state stays `number`, not `string`
* **Range slider support**: pass `type='range'` to render a slider
* **Tooltip integration**: optional InfoIcon button with tooltip content via `tooltip` prop
* **InputGroup ready**: built on shadcn InputGroup for seamless addon integration
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { NumberField } from '@/components/ui/shuip/react-hook-form/number-field';
const form = useForm<{ quantity: number; ratio: number }>({
defaultValues: { quantity: 1, ratio: 0.5 },
});
const lens = useLens({ control: form.control });
```
## Examples [#examples]
## Props [#props]
# Password Field (/components/react-hook-form/password-field)
`PasswordField` is a password input component that encapsulates React Hook Form's field management with shadcn/ui's design system. It handles all the boilerplate of connecting form state to an input element: wiring event handlers, displaying errors, managing touched states, and rendering consistent UI — with a built-in visibility toggle on top.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('password')` autocompletes from your form's value type — no `` generic at the call site
* **One-click visibility toggle**: Eye/EyeOff icons switch between masked and plain text
* **Tooltip integration**: optional InfoIcon button with tooltip content via `tooltip` prop
* **InputGroup ready**: built on shadcn InputGroup for seamless addon integration
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';
const form = useForm({ defaultValues: { password: '' } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides shadcn's `FormProvider` which `FormLabel` and `FormMessage` use internally.
## Less boilerplate [#less-boilerplate]
React Hook Form's standard approach uses render props to access field state, plus extra state to wire the visibility toggle:
```tsx
const [showPassword, setShowPassword] = useState(false);
(
Password setShowPassword((v) => !v)}>
{showPassword ? : }
)}
/>
```
With `PasswordField`, this reduces to a single declarative component:
```tsx
```
## Examples [#examples]
## Props [#props]
# Radio Field (/components/react-hook-form/radio-field)
`RadioField` is a radio button group component that encapsulates React Hook Form's field management with shadcn/ui's design system. It handles all the boilerplate of connecting form state to a radio group: rendering each option, wiring event handlers, displaying errors, and providing accessible labels.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('plan')` autocompletes from your form's value type — no `` generic at the call site
* **Automatic option rendering**: pass an array of strings and get fully functional radio buttons
* **Consistent layout**: pre-configured spacing and alignment for radio groups
* **Accessibility support**: proper ARIA attributes, label associations, and keyboard navigation
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { RadioField } from '@/components/ui/shuip/react-hook-form/radio-field';
const form = useForm({ defaultValues: { plan: 'free' } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides shadcn's `FormProvider` which `FormLabel` and `FormMessage` use internally.
## Less boilerplate [#less-boilerplate]
React Hook Form's standard approach uses render props to access field state:
```tsx
(
Select a planfreeproenterprise
)}
/>
```
With `RadioField`, this reduces to a single declarative component:
```tsx
```
## Examples [#examples]
## Props [#props]
# Select Field (/components/react-hook-form/select-field)
`SelectField` is a single-choice dropdown component that encapsulates React Hook Form's field management with shadcn/ui's design system. It handles wiring the controlled value, change handler, validation state, and option rendering from a key-value map.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('country')` autocompletes from your form's value type — no `` generic at the call site
* **Automatic option rendering**: pass a `Record` — keys become labels, values become form data
* **Controlled value**: bound through `value`/`onValueChange`, kept in sync with form state
* **Zod validation**: native integration with react-hook-form and Zod via resolver
* **Accessibility**: full keyboard navigation and screen reader support via Radix Select
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { SelectField } from '@/components/ui/shuip/react-hook-form/select-field';
const form = useForm({ defaultValues: { country: 'us' } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides shadcn's `FormProvider` which `FormLabel` and `FormMessage` use internally. Default selections belong on `useForm({ defaultValues })`, not on the field component.
## Less boilerplate [#less-boilerplate]
React Hook Form's standard approach uses render props to access field state:
```tsx
(
Country
Choose your country
)}
/>
```
With `SelectField`, this reduces to a single declarative component:
```tsx
```
## Examples [#examples]
## Props [#props]
# Textarea Field (/components/react-hook-form/textarea-field)
`TextareaField` is a multi-line text input component that encapsulates React Hook Form's field management with shadcn/ui's design system. It handles all the boilerplate of connecting form state to a textarea element: wiring event handlers, displaying errors, managing touched states, and rendering consistent UI.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
#### Built-in features [#built-in-features]
* **Typed lens binding**: `lens.focus('bio')` autocompletes from your form's value type — no `` generic at the call site
* **Bottom-right tooltip**: optional InfoIcon button with tooltip content via `tooltip` prop, positioned via InputGroup `align='block-end'`
* **Native textarea props**: full support for `rows`, `maxLength`, `placeholder`, and other native attributes
* **InputGroup ready**: built on shadcn InputGroup for seamless addon integration
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { TextareaField } from '@/components/ui/shuip/react-hook-form/textarea-field';
const form = useForm({ defaultValues: { bio: '' } });
const lens = useLens({ control: form.control });
```
The `
` wrapper is required — it provides shadcn's `FormProvider` which `FormLabel` and `FormMessage` use internally.
## Less boilerplate [#less-boilerplate]
React Hook Form's standard approach uses render props to access field state:
```tsx
(
Bio
Tell us about yourself
)}
/>
```
With `TextareaField`, this reduces to a single declarative component:
```tsx
```
## Examples [#examples]
## Props [#props]
# Time Field (/components/react-hook-form/time-field)
`TimeField` is a time-only picker built from two shadcn/ui `Select` inputs — hours `00`–`23` and minutes in 5-minute steps. It binds to a `string` in the `HH:mm` 24-hour format, encapsulating React Hook Form's field management.
The field binds to the form via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses) — no call-site generic, just `lens.focus('fieldName')` with full autocomplete from your form's value type.
#### Built-in features [#built-in-features]
* **Two-select picker**: hours and minutes as shadcn `Select` inputs — consistent styling, full keyboard support, no native-input inconsistencies across browsers
* **Typed lens binding**: `lens.focus('meetingTime')` autocompletes from your form's value type
* **Range constraints**: pass `min` / `max` (as `HH:mm` strings) to disable out-of-range hour and minute options
* **Zod validation**: native integration with react-hook-form and Zod via resolver
## Setup [#setup]
Field components bind via `@hookform/lenses`. Create a lens once per form:
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { TimeField } from '@/components/ui/shuip/react-hook-form/time-field';
const form = useForm({ defaultValues: { meetingTime: '' } });
const lens = useLens({ control: form.control });
```
The value is a `string` in `HH:mm` format (or empty string when unset). Picking only the hour or only the minute defaults the other part to `00`.
## Constraining the range [#constraining-the-range]
`min` and `max` accept `HH:mm` strings. Out-of-range hour and minute options are disabled in the dropdowns. Always validate via Zod (and server-side) for safety:
```tsx
const schema = z.object({
appointment: z
.string()
.min(1, 'Required')
.refine((v) => v >= '09:00' && v <= '18:00', 'Outside office hours'),
});
```
## Examples [#examples]
## Props [#props]
# Time Range (/components/react-hook-form/time-range)
`TimeRangeField` is two coupled time pickers — a start and an end — bound to a single `{ start, end }` object via a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses). Both values are `HH:mm` strings.
#### Built-in features [#built-in-features]
* **Coupled pickers**: choosing a start time sets the end to one hour later automatically
* **`start < end` enforced**: end options at or before the start are disabled in the dropdowns
* **Single object binding**: `lens.focus('slot')` binds the whole `{ start, end }` value
* **Zod validation**: refine the object to require a valid ordered range
## Setup [#setup]
```tsx
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { TimeRangeField } from '@/components/ui/shuip/react-hook-form/time-range';
type MyForm = { slot: { start: string; end: string } };
const form = useForm({ defaultValues: { slot: { start: '', end: '' } } });
const lens = useLens({ control: form.control });
```
The value is `{ start: string; end: string }`, each a `HH:mm` string (empty when unset). Changing the start always resets the end to one hour after the new start.
## Examples [#examples]
## Props [#props]
# Autocomplete Field (/components/tanstack-form/autocomplete-field)
`AutocompleteField` is a text input that proposes matching strings as the user types, while still allowing any free-text value. It reads the surrounding field via `useFieldContext`, so you compose it inside a `` rather than passing a `form` instance by prop. It stores a plain `string`.
#### Built-in features [#built-in-features]
* **Two suggestion modes**: a static `suggestions` array (filtered client-side) or an async `onSearch` function (debounced fetch).
* **Free text**: the typed value is always kept — closing the dropdown without selecting commits what was typed.
* **Keyboard navigation**: ArrowUp/ArrowDown to move, Enter to select, Escape to close.
* **Custom matching**: override the default case-insensitive substring match via `filter` (static mode).
## Setup [#setup]
Field components are bound via React context. In your project, create `lib/form.ts` once:
```tsx
// lib/form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { AutocompleteField } from '@/components/ui/shuip/tanstack-form/autocomplete-field';
import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { AutocompleteField },
formComponents: { SubmitButton },
});
```
See the [`form-context`](/components/tanstack-form/form-context) item for details.
## Examples [#examples]
## Props [#props]
# Checkbox Field (/components/tanstack-form/checkbox-field)
Checkboxes are binary inputs for opting in or accepting terms. `CheckboxField` combines Radix UI's Checkbox with an inline clickable label, making the entire label area interactive — not just the checkbox itself.
The component reads the surrounding field via `useFieldContext`, so you compose it inside a ``. Common patterns include required checkboxes for terms acceptance (validated with `!value` check) or grouped checkboxes for multi-select options (using nested field paths like `features.notifications`).
### Built-in features [#built-in-features]
* **Context-bound field state** via `useFieldContext` — no prop drilling
* **Clickable inline label** positioned next to checkbox
* **Boolean field type** for true/false state
* **Required validation** for terms acceptance
* **Nested paths** for grouped checkboxes
## Setup [#setup]
Field components are bound via React context. In your project, create `lib/form.ts` once:
```tsx
// lib/form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { CheckboxField } from '@/components/ui/shuip/tanstack-form/checkbox-field';
import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { CheckboxField },
formComponents: { SubmitButton },
});
```
See the [`form-context`](/components/tanstack-form/form-context) item for details.
## Required checkbox [#required-checkbox]
```tsx
import { useAppForm } from '@/lib/form';
const form = useAppForm({
defaultValues: { terms: false },
onSubmit: async ({ value }) => {
await saveData(value);
},
});
!value ? 'You must accept the terms' : undefined,
}}
children={(field) => (
)}
/>
```
## Examples [#examples]
## Props [#props]
# Date Field (/components/tanstack-form/date-field)
`DateField` is a single-date picker that encapsulates TanStack Form's field management with the shadcn `Calendar` primitive. The trigger is an outline `Button` (full-width) that shows the selected date formatted via [`date-fns`](https://date-fns.org); clicking it opens a `Popover` containing the calendar.
It reads the surrounding field via `useFieldContext()`, so you compose it inside a `` rather than passing a `form` instance down. `undefined` represents an empty selection (matching `react-day-picker`'s convention).
#### Built-in features [#built-in-features]
* **Context-bound field state**: reads the field via `useFieldContext` — no prop drilling
* **Locale-aware formatting**: defaults to `en-US`; pass any `date-fns/locale` via the `locale` prop
* **Bounded selection**: `minDate` / `maxDate` disable days outside the range in the calendar UI
* **Accessible trigger**: a real `