Form components
Input and form control components for user interaction
Form components handle user input and form submission. All components are built with accessibility in mind and support keyboard navigation.
The category splits into three groups:
- Controls: raw inputs you can drop in standalone or compose inside
<Form> - Composition: the
<Form>provider and the low-level building blocks (FormField,FormItem,FormLabel,FormControl,FormDescription,FormMessage) - Prebuilt fields: one-tag wrappers that bind a control to React hook form
Prerequisites
Before using these components, complete the @tetherto/mdk-core-ui installation.
Controls
Standalone form controls. Manage value and onChange, or compose them inside <Form> with
FormField.
| Component | Description |
|---|---|
Button | Primary action trigger with variants and sizes |
ActionButton | Button that requires confirmation before running an action |
Input | Text input field with label and validation |
TextArea | Multi-line text input |
Select | Dropdown single/multi-select input |
SelectTrigger | Select trigger control |
SelectValue | Displayed select value |
SelectContent | Select dropdown panel |
SelectGroup | Select option group |
SelectLabel | Select group heading |
SelectItem | Select list option |
Cascader | Multi-level dropdown selector for hierarchical data |
Checkbox | Binary toggle input for forms |
Switch | Toggle for on/off settings |
Radio | Single-select option from a group |
RadioGroup | Radio group container |
RadioCard | Card-style radio option |
DatePicker | Calendar-based date selection input |
DateRangePicker | Two-month date range selector with presets and apply/clear actions |
Label | Form field label with optional required indicator |
TagInput | Input for entering multiple tags |
ActionButton
Opens a popover or dialog confirmation around a trigger Button; runs onConfirm after the user agrees.
Triggers an action after confirmation in either an inline popover (default) or a modal dialog. Set variant so the trigger
Button matches severity (for example danger for destructive flows).
After the user confirms, run your side effects there. Consider showing feedback with Toast and Toaster
from the same Toast setup as in overlays. This is the pattern the demo app applies.
Import
import { ActionButton } from '@tetherto/mdk-core-ui'Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | none | Trigger button label |
variant | 'primary' | 'secondary' | 'danger' | 'secondary' | Visual variant of the trigger Button |
loading | boolean | false | Loading state on the trigger |
disabled | boolean | false | Disable the trigger |
className | string | none | Class on the trigger |
mode | 'popover' | 'dialog' | 'popover' | popover: confirmation UI in an anchored popover. dialog: confirmation UI in a modal Dialog. |
confirmation | ActionButtonConfirmation | none | Required: title, body copy, and confirm/cancel handlers. See Confirmation object. |
Confirmation object
Nested inside confirmation:
| Field | Type | Description |
|---|---|---|
title | string | Heading shown in the confirmation UI |
description | ReactNode | Optional body (text or JSX) |
onConfirm | () => void | Called when the user confirms |
onCancel | () => void | Called when the user cancels |
confirmLabel | string | Optional confirm button label (defaults vary by mode) |
cancelLabel | string | Optional cancel button label (defaults to Cancel) |
icon | ReactNode | Optional icon in popover header (popover mode only; dialog layout uses title and body) |
Basic usage (popover)
Default mode is popover: compact confirmation next to the trigger.
<ActionButton
label="Restart service"
variant="danger"
confirmation={{
title: 'Restart service',
description: 'The service will be unavailable briefly.',
onConfirm: () => {
void restartService()
},
}}
/>Dialog mode
Set mode="dialog" when you want a full modal confirmation (for example irreversible or high-impact actions).
<ActionButton
label="Factory reset"
variant="danger"
mode="dialog"
confirmation={{
title: 'Confirm factory reset',
description: 'This cannot be undone.',
onConfirm: () => {
void factoryReset()
},
}}
/>Button
Configurable button with multiple variants, sizes, and loading state.
Import
import { Button } from '@tetherto/mdk-core-ui'Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'danger' | 'tertiary' | 'link' | 'icon' | 'outline' | 'ghost' | 'secondary' | Visual variant |
size | 'sm' | 'md' | 'lg' | none | Button size |
loading | boolean | false | Show loading spinner |
fullWidth | boolean | false | Expand to container width |
icon | ReactNode | none | Icon element |
iconPosition | 'left' | 'right' | 'left' | Icon placement |
disabled | boolean | false | Disable interaction |
Basic usage
<Button>Default Button</Button>
<Button variant="primary">Primary</Button>
<Button variant="danger">Delete</Button>
<Button variant="outline">Outline</Button>With icon
<Button icon={<PlusIcon />}>Add Item</Button>
<Button icon={<ArrowRightIcon />} iconPosition="right">
Continue
</Button>Loading state
<Button loading>Saving...</Button>
<Button variant="primary" loading disabled>
Processing
</Button>Styling
.mdk-button: Root element.mdk-button--variant-{variant}: Variant modifier.mdk-button--size-{size}: Size modifier.mdk-button--full-width: Full width modifier.mdk-button--loading: Loading state
Cascader
Multi-level dropdown selector for hierarchical data.
Import
import { Cascader } from '@tetherto/mdk-core-ui'Basic usage
<Cascader
options={[
{
value: 'na',
label: 'North America',
children: [
{ value: 'us-east', label: 'US East' },
{ value: 'us-west', label: 'US West' },
],
},
]}
value={value}
onChange={setValue}
/>Checkbox
Checkbox input built on Radix UI primitives.
Import
import { Checkbox } from '@tetherto/mdk-core-ui'Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Checkbox size |
color | 'default' | 'primary' | 'success' | 'warning' | 'error' | 'primary' | Color when checked |
radius | 'none' | 'small' | 'medium' | 'large' | 'full' | 'none' | Border radius |
checked | boolean | 'indeterminate' | none | Checked state (controlled) |
onCheckedChange | function | none | Change callback |
Basic usage
<Checkbox checked={checked} onCheckedChange={setChecked} />
<label className="flex items-center gap-2">
<Checkbox checked={terms} onCheckedChange={setTerms} />
I agree to the terms
</label>Sizes and colors
<Checkbox size="xs" />
<Checkbox size="sm" />
<Checkbox size="md" />
<Checkbox size="lg" color="success" />Styling
.mdk-checkbox: Root element.mdk-checkbox--{size}: Size modifier.mdk-checkbox--{color}: Color modifier.mdk-checkbox__indicator: Check mark container
DatePicker
Calendar-based date selection input.
Import
import { DatePicker } from '@tetherto/mdk-core-ui'Basic usage
<DatePicker
value={date}
onChange={setDate}
placeholder="Select date"
/>DateRangePicker
Two-month date range selector built on react-day-picker. Renders a popover modal with presets (Last 7/14/30/90 Days by default), a draft selection summary, and explicit apply/clear actions. Future dates are disabled by default.
Import
import { DateRangePicker } from '@tetherto/mdk-core-ui'
import type { DateRange, PresetItem } from '@tetherto/mdk-core-ui'Props
| Prop | Type | Default | Description |
|---|---|---|---|
selected | DateRange | none | Selected { from, to } range (controlled) |
onSelect | (range: DateRange | undefined) => void | none | Called when the user applies a range |
placeholder | string | 'Pick a date range' | Trigger text when no range is selected |
dateFormat | string | 'MM/dd/yyyy' | date-fns format used in the trigger label |
disabled | boolean | false | Disable interaction |
showPresets | boolean | true | Show the default preset buttons for the last 7, 14, 30, and 90 days |
presets | PresetItem[] | none | Override the default preset list |
allowFutureDates | boolean | false | When false, dates after today are disabled |
triggerClassName | string | none | Extra classes for the trigger button |
calendarClassName | string | none | Extra classes for the calendar |
modalClassName | string | none | Extra classes for the popover modal |
Any other react-day-picker props (excluding mode and selected) are forwarded to the underlying calendar.
Basic usage
const [range, setRange] = useState<DateRange>()
<DateRangePicker
selected={range}
onSelect={setRange}
/>Custom presets
const presets: PresetItem[] = [
{ label: 'This week', value: { from: startOfWeek(new Date()), to: new Date() } },
{ label: 'This month', value: { from: startOfMonth(new Date()), to: new Date() } },
]
<DateRangePicker
selected={range}
onSelect={setRange}
presets={presets}
/>Allowing future dates
<DateRangePicker
selected={range}
onSelect={setRange}
allowFutureDates
showPresets={false}
/>Styling
.mdk-date-picker__trigger: Trigger button.mdk-date-picker__modal: Popover modal container.mdk-date-picker__calendar: Calendar grid.mdk-date-picker__summary: Selected-range summary block.mdk-date-picker__presets: Preset button row
Input
Text input with label support, prefix/suffix, and search variant.
Import
import { Input } from '@tetherto/mdk-core-ui'Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | none | Visible label for the field |
variant | 'default' | 'search' | 'default' | Input variant |
size | 'default' | 'medium' | 'default' | Input size |
error | string | none | Error message (shows red border) |
prefix | ReactNode | none | Element before input |
suffix | ReactNode | none | Element after input |
wrapperClassName | string | none | Wrapper element class |
Basic usage
<Input label="Email" placeholder="Enter email" id="email" />
<Input variant="search" placeholder="Search miners..." />With prefix/suffix
<Input prefix="$" suffix="USD" placeholder="0.00" />
<Input suffix="°C" placeholder="Temperature" />
<Input prefix={<UserIcon />} placeholder="Username" />Error state
<Input
label="MAC Address"
error="Invalid MAC address format"
value={mac}
onChange={(e) => setMac(e.target.value)}
/>Styling
.mdk-input: Input element.mdk-input__wrapper: Wrapper container.mdk-input__wrapper--error: Error state.mdk-input__label: Label element.mdk-input__prefix: Prefix element.mdk-input__suffix: Suffix element.mdk-input__error: Error message
Label
Form field label with optional required indicator.
Import
import { Label } from '@tetherto/mdk-core-ui'Basic usage
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
<Label htmlFor="password" required>Password</Label>
<Input id="password" type="password" />Radio
Radio button for single-select from a group.
Radio composition parts
| Part | Role |
|---|---|
RadioGroup | Root that holds the selected value and calls onValueChange when it changes. |
Radio | Default circular radio item; use label or children for visible text. |
RadioCard | Card-styled option for horizontal or compact layouts; uses the same item primitive as Radio. |
Import
import { Radio, RadioGroup, RadioCard } from '@tetherto/mdk-core-ui'Basic usage
<RadioGroup value={value} onValueChange={setValue}>
<Radio value="small">Small</Radio>
<Radio value="medium">Medium</Radio>
<Radio value="large">Large</Radio>
</RadioGroup>RadioCard suits card-style, horizontal groups:
<RadioGroup value={value} orientation="horizontal" onValueChange={setValue}>
<RadioCard value="a" label="Option A" />
<RadioCard value="b" label="Option B" />
</RadioGroup>Select
Dropdown select built on Radix UI primitives.
Import
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SelectGroup,
SelectLabel,
} from '@tetherto/mdk-core-ui'SelectTrigger props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'lg' | Trigger size |
variant | 'default' | 'colored' | 'default' | Visual variant |
color | string | none | Custom color for colored variant (hex) |
Basic usage
<Select value={value} onValueChange={setValue}>
<SelectTrigger>
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
<SelectItem value="option3">Option 3</SelectItem>
</SelectContent>
</Select>With groups
<Select>
<SelectTrigger size="md">
<SelectValue placeholder="Select pool" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>North America</SelectLabel>
<SelectItem value="us-east">US East</SelectItem>
<SelectItem value="us-west">US West</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Europe</SelectLabel>
<SelectItem value="eu-west">EU West</SelectItem>
</SelectGroup>
</SelectContent>
</Select>Colored variant
<Select>
<SelectTrigger variant="colored" color="#72F59E">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>Styling
.mdk-select__trigger: Trigger button.mdk-select__trigger--{size}: Size modifier.mdk-select__trigger--colored: Colored variant.mdk-select__content: Dropdown content.mdk-select__item: Option item
Switch
Toggle switch for on/off states.
Import
import { Switch } from '@tetherto/mdk-core-ui'Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Switch size |
color | 'default' | 'primary' | 'success' | 'warning' | 'error' | 'default' | Color when checked |
radius | 'none' | 'small' | 'medium' | 'large' | 'full' | 'none' | Border radius |
checked | boolean | none | Checked state (controlled) |
onCheckedChange | function | none | Change callback |
Basic usage
<Switch checked={enabled} onCheckedChange={setEnabled} />
<label className="flex items-center gap-2">
<Switch checked={darkMode} onCheckedChange={setDarkMode} />
Dark Mode
</label>Sizes and colors
<Switch size="sm" />
<Switch size="md" color="primary" />
<Switch size="lg" color="success" />Styling
.mdk-switch: Root element.mdk-switch--{size}: Size modifier.mdk-switch--{color}: Color modifier.mdk-switch__thumb: Toggle thumb
TagInput
Input for entering multiple tags.
Import
import { TagInput } from '@tetherto/mdk-core-ui'Basic usage
<TagInput
value={tags}
onChange={setTags}
placeholder="Add tags..."
/>TextArea
Multi-line text input for longer content.
Import
import { TextArea } from '@tetherto/mdk-core-ui'Basic usage
<TextArea
label="Description"
placeholder="Enter description"
rows={4}
value={value}
onChange={(e) => setValue(e.target.value)}
/>Composition
The <Form> provider plus the low-level compound components for assembling custom form fields. Use these when no prebuilt field fits, or when you need full control over the field's layout.
| Component | Description |
|---|---|
Form | Form wrapper with validation and submission handling |
FormControl | Slot wrapper that wires ARIA attributes onto a control |
FormDescription | Helper text paragraph rendered below a field |
FormField | Controller wrapper that provides field context to descendants |
FormItem | Layout wrapper grouping a label, control, description, and message |
FormLabel | Label that auto-links to the form field input |
FormMessage | Validation message paragraph for a form field |
See the Composition page for <Form>, the seven compound parts, and built-in validators.
Prebuilt fields
Form-bound fields pre-wired to React hook form. Each combines FormField, FormItem, FormLabel, FormControl, FormDescription, and FormMessage so you can render a labelled, validated field from a single component.
| Component | Description |
|---|---|
FormCascader | React hook form `Cascader` wrapper for hierarchical selection |
FormCheckbox | React hook form `Checkbox` wrapper with inline label |
FormDatePicker | React hook form `DatePicker` wrapper with label and description |
FormInput | React hook form `Input` wrapper with label, description, and error |
FormRadioGroup | React hook form `RadioGroup` wrapper accepting an options array |
FormSelect | React hook form `Select` wrapper accepting an options array |
FormSwitch | React hook form `Switch` wrapper with inline label |
FormTagInput | React hook form `TagInput` wrapper for multi-value fields |
FormTextArea | React hook form `TextArea` wrapper with label, description, and error |
See the Prebuilt fields page for the full prop reference and component-specific examples.