Components
Input
A compound input component with wrapper, icons, affixes, and size/error variants.
Installation
bunx @happlyui/cli@latest add input
Dependencies
npm packages
@radix-ui/react-slot
Registry dependencies
These are automatically installed when you add this component.
form-field-contexthapply-ui-utilspolymorphicrecursive-clone-childrentv
Usage
import * as Input from "@/components/ui/input"
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiSearchLine} />
<Input.Input placeholder="Search..." />
</Input.Wrapper>
</Input.Root>
Examples
With Icon
Input with leading or trailing icons.
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiUser6Line} />
<Input.Input placeholder="Placeholder text..." />
</Input.Wrapper>
</Input.Root>
Sizes
Available size variants: medium, small, and xsmall.
<Input.Root size="small">
<Input.Wrapper>
<Input.Icon as={RiUser6Line} />
<Input.Input placeholder="Placeholder text..." />
</Input.Wrapper>
</Input.Root>
With Affix
Input with prefix or suffix affix sections separated by a divider.
<Input.Root>
<Input.Affix>https://</Input.Affix>
<Input.Wrapper>
<Input.Input placeholder="www.example.com" />
</Input.Wrapper>
</Input.Root>
With Inline Affix
Input with an inline affix displayed within the input area.
<Input.Root>
<Input.Wrapper>
<Input.InlineAffix>€</Input.InlineAffix>
<Input.Input placeholder="0.00" />
</Input.Wrapper>
</Input.Root>
Label and Hint
Input composed with Label and Hint components.
<FormField.Root
label="Email Address"
htmlFor="email"
hint="This is a hint text to help user."
>
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiMailLine} />
<Input.Input
id="email"
type="email"
placeholder="hello@example.com"
/>
</Input.Wrapper>
</Input.Root>
</FormField.Root>
With Kbd
Input with a keyboard shortcut indicator.
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiSearch2Line} />
<Input.Input placeholder="Search..." />
<Kbd.Root>⌘1</Kbd.Root>
</Input.Wrapper>
</Input.Root>
Password
Password input with show/hide toggle.
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiLock2Line} />
<Input.Input type="password" placeholder="••••••••••" />
<button onClick={() => setShowPassword(s => !s)}>Toggle</button>
</Input.Wrapper>
</Input.Root>
Password with Strength Level
Password input with a strength indicator and criteria checklist.
const [showPassword, setShowPassword] = React.useState(false);
const [newPassword, setNewPassword] = React.useState('');
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiLock2Line} />
<Input.Input
type={showPassword ? 'text' : 'password'}
placeholder="••••••••••"
value={newPassword}
onChange={handleNewPasswordChange}
/>
<button onClick={() => setShowPassword((s) => !s)}>
{showPassword ? <RiEyeOffLine /> : <RiEyeLine />}
</button>
</Input.Wrapper>
</Input.Root>
{/* Strength bar and criteria checklist */}
<LevelBar levels={3} level={trueCriteriaCount} />
Disabled
Disabled input state.
<Input.Root>
<Input.Wrapper>
<Input.Input placeholder="Placeholder text..." disabled />
</Input.Wrapper>
</Input.Root>
Error State
Input with error styling.
<Input.Root hasError>
<Input.Wrapper>
<Input.Input placeholder="Placeholder text..." />
</Input.Wrapper>
</Input.Root>
With Button
Input with an action button alongside.
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiLinksLine} />
<Input.Input placeholder="www.example.com" />
</Input.Wrapper>
<button className="inline-flex h-10 items-center justify-center px-3.5">
<RiFileCopyLine className="w-5 h-5" />
</button>
</Input.Root>
With Tags
Input that creates tags on Enter key press.
const [tags, setTags] = React.useState(['Berlin', 'London', 'Paris']);
const [inputValue, setInputValue] = React.useState('');
<Input.Root>
<Input.Wrapper>
<Input.Input
placeholder="Add tags..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && inputValue.trim()) {
setTags([...tags, inputValue.trim()]);
setInputValue('');
}
}}
/>
</Input.Wrapper>
</Input.Root>
<div className="mt-2 flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag.Root key={tag}>
{tag}
<Tag.DismissButton onClick={() => removeTag(tag)} />
</Tag.Root>
))}
</div>
Counter Input
Number field with increment/decrement buttons using React Aria.
import { Button, Group, Input, Label, NumberField } from 'react-aria-components';
import { compactButtonVariants } from '@/components/ui/compact-button';
import { inputVariants } from '@/components/ui/input';
const { root, wrapper, input } = inputVariants();
const { root: btnRoot, icon: btnIcon } = compactButtonVariants({ variant: 'ghost' });
<NumberField defaultValue={16} minValue={0}>
<Label>Counter Input</Label>
<div className={root()}>
<Group className={wrapper()}>
<Button slot="decrement" className={btnRoot()}>
<RiSubtractLine className={btnIcon()} />
</Button>
<Input className={input({ class: 'text-center' })} />
<Button slot="increment" className={btnRoot()}>
<RiAddLine className={btnIcon()} />
</Button>
</Group>
</div>
</NumberField>
Date Field
Date input using React Aria DateField with inputVariants styling.
import { DateField, DateInput, DateSegment, Label } from 'react-aria-components';
import { inputVariants } from '@/components/ui/input';
const { root, wrapper, icon } = inputVariants();
<DateField>
<Label>Date</Label>
<div className={root()}>
<div className={wrapper({ class: 'h-10' })}>
<RiCalendarLine className={icon()} />
<DateInput className="flex">
{(segment) => <DateSegment segment={segment} />}
</DateInput>
</div>
</div>
</DateField>
Payment Input
Credit card number input with auto-detection using react-payment-inputs.
import { usePaymentInputs } from 'react-payment-inputs';
const { getCardNumberProps, meta } = usePaymentInputs();
<Input.Root>
<Input.Wrapper className="pr-2">
<Input.Icon as={RiBankCardLine} />
<Input.Input
{...getCardNumberProps()}
placeholder="0000 0000 0000 0000"
/>
<img src={cardIcon} alt="" className="h-6 w-8 shrink-0" />
</Input.Wrapper>
</Input.Root>
With Select
Input combined with a compact select for currency picking.
<Input.Root>
<Input.Wrapper>
<Input.InlineAffix>€</Input.InlineAffix>
<Input.Input placeholder="0.00" />
</Input.Wrapper>
<Select.Root variant="compactForInput" defaultValue="EUR">
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item value="EUR">EUR</Select.Item>
</Select.Content>
</Select.Root>
</Input.Root>
With Inline Select
Input with an inline select for permissions inside the wrapper.
<Input.Root>
<Input.Wrapper>
<Input.Icon as={RiUser6Line} />
<Input.Input placeholder="Placeholder text..." />
<Select.Root variant="inline" defaultValue="view">
<Select.Trigger>
<Select.TriggerIcon as={RiGlobalLine} />
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item value="view">can view</Select.Item>
<Select.Item value="edit">can edit</Select.Item>
</Select.Content>
</Select.Root>
</Input.Wrapper>
</Input.Root>
Composed
Simplified usage via the Composed wrapper with leadingIcon, trailingIcon, and node slots.
import * as Input from "@/components/ui/input"
<Input.Composed
leadingIcon={RiLock2Line}
type="password"
placeholder="••••••••••"
inlineTrailingNode={<button>Toggle</button>}
/>
API Reference
Input.Root
The root container with ring border and shadow.
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'medium' | 'small' | 'xsmall' | 'medium' | Size variant |
hasError | boolean | false | Show error styling |
asChild | boolean | false | Render as child element using Slot |
Input.Wrapper
Label wrapper with cursor and hover styling.
| Prop | Type | Default | Description |
|---|
Input.Input
The actual input element.
| Prop | Type | Default | Description |
|---|
Input.Icon
Icon displayed in the input wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
as | React.ElementType | - | The icon component to render |
Input.Affix
Affix section (prefix/suffix) with divider.
| Prop | Type | Default | Description |
|---|
Input.InlineAffix
Inline affix displayed within the input area.
| Prop | Type | Default | Description |
|---|
Input.Composed
A simplified wrapper that composes Root, Wrapper, Input, and Icon into a single component.
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'medium' | 'small' | 'xsmall' | 'medium' | Size variant |
hasError | boolean | false | Show error styling |
leadingIcon | React.ElementType | - | Icon rendered before the input |
trailingIcon | React.ElementType | - | Icon rendered after the input |
leadingNode | React.ReactNode | - | Node rendered before the wrapper (e.g. Affix) |
trailingNode | React.ReactNode | - | Node rendered after the wrapper (e.g. Affix) |
inlineLeadingNode | React.ReactNode | - | Node rendered inside the wrapper before icons |
inlineTrailingNode | React.ReactNode | - | Node rendered inside the wrapper after icons |