HapplyUI

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-context
  • happly-ui-utils
  • polymorphic
  • recursive-clone-children
  • tv

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.

https://
@gmail.com
<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.

This is a hint text to help user.
<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.

This is a hint text to help user.
<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.

Must contain at least;
At least 1 uppercase
At least 1 number
At least 8 characters
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.

Berlin
London
Paris
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.

Date
mmddyyyy
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.

PropTypeDefaultDescription
size'medium' | 'small' | 'xsmall''medium'Size variant
hasErrorbooleanfalseShow error styling
asChildbooleanfalseRender as child element using Slot

Input.Wrapper

Label wrapper with cursor and hover styling.

PropTypeDefaultDescription

Input.Input

The actual input element.

PropTypeDefaultDescription

Input.Icon

Icon displayed in the input wrapper.

PropTypeDefaultDescription
asReact.ElementType-The icon component to render

Input.Affix

Affix section (prefix/suffix) with divider.

PropTypeDefaultDescription

Input.InlineAffix

Inline affix displayed within the input area.

PropTypeDefaultDescription

Input.Composed

A simplified wrapper that composes Root, Wrapper, Input, and Icon into a single component.

PropTypeDefaultDescription
size'medium' | 'small' | 'xsmall''medium'Size variant
hasErrorbooleanfalseShow error styling
leadingIconReact.ElementType-Icon rendered before the input
trailingIconReact.ElementType-Icon rendered after the input
leadingNodeReact.ReactNode-Node rendered before the wrapper (e.g. Affix)
trailingNodeReact.ReactNode-Node rendered after the wrapper (e.g. Affix)
inlineLeadingNodeReact.ReactNode-Node rendered inside the wrapper before icons
inlineTrailingNodeReact.ReactNode-Node rendered inside the wrapper after icons

Previous
Hint
Next
Label