Input Group
Compose text inputs and textareas with inline add-ons, icons, and buttons inside a single bordered control.
<InputGroup className="max-w-xs"> <InputGroupInput placeholder="Search…" type="search" /> <InputGroupAddon align="inline-end"> <MagnifyingGlass className="text-fg-muted" /> </InputGroupAddon></InputGroup>Usage
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@reva/ui";Use InputGroupInput or InputGroupTextarea inside InputGroup — not the standalone Input / raw <input>, so focus rings and borders stay on the outer shell.
InputGroupButton is a thin wrapper around buttonVariants (same variant, size, and layout as Button). Default is variant="ghost", size="sm", layout="square" for compact trailing actions.
Don't set className="size-4" (or any other size-*) on icon glyphs inside InputGroupButton or InputGroupAddon — both auto-size their direct <svg> children to the correct step (16px / 20px / 24px depending on button size). Just write <Eye /> and the right glyph size follows from the button or addon scale.
Composition
Use the following composition to build an InputGroup:
InputGroup
├── InputGroupInput or InputGroupTextarea
├── InputGroupAddon
│ ├── InputGroupButton
│ └── InputGroupTextPlace InputGroupAddon after the control in the DOM and set align (inline-start | inline-end | block-start | block-end) to position it visually.
Examples
Sizes
Set size on InputGroup so the shell height matches Input (sm / default / lg / xl). InputGroupInput and InputGroupTextarea inherit the group size; pass size on the control to override. The same applies to inline actions — addons sized via the group context keep the same optical inset on top, right, and bottom across every size.
<VStack className="w-full max-w-sm" gap={10}> <VStack className="w-full" gap={3}> {/* Icon-only addon — sized via InputGroup context */} <InputGroup size="sm"> <InputGroupInput placeholder="Small group" type="search" /> <InputGroupAddon align="inline-end"> <MagnifyingGlass className="text-fg-muted" /> </InputGroupAddon> </InputGroup> <InputGroup size="default"> <InputGroupInput placeholder="Default group" type="search" /> <InputGroupAddon align="inline-end"> <MagnifyingGlass className="text-fg-muted" /> </InputGroupAddon> </InputGroup> <InputGroup size="lg"> <InputGroupInput placeholder="Large group (lg)" type="search" /> <InputGroupAddon align="inline-end"> <MagnifyingGlass className="text-fg-muted" /> </InputGroupAddon> </InputGroup> <InputGroup size="xl"> <InputGroupInput placeholder="Extra large group (xl)" type="search" /> <InputGroupAddon align="inline-end"> <MagnifyingGlass className="text-fg-muted" /> </InputGroupAddon> </InputGroup> </VStack> <VStack className="w-full" gap={3}> {/* Inline action — ghost InputGroupButton (see Password example for state wiring) */} <InputGroup size="sm"> <InputGroupInput placeholder="Enter password" type="password" /> <InputGroupAddon align="inline-end"> <InputGroupButton aria-label="Show password" type="button"> <Eye /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup size="default"> <InputGroupInput placeholder="Enter password" type="password" /> <InputGroupAddon align="inline-end"> <InputGroupButton aria-label="Show password" type="button"> <Eye /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup size="lg"> <InputGroupInput placeholder="Enter password" type="password" /> <InputGroupAddon align="inline-end"> <InputGroupButton aria-label="Show password" type="button"> <Eye /> </InputGroupButton> </InputGroupAddon> </InputGroup> <InputGroup size="xl"> <InputGroupInput placeholder="Enter password" type="password" /> <InputGroupAddon align="inline-end"> <InputGroupButton aria-label="Show password" type="button"> <Eye /> </InputGroupButton> </InputGroupAddon> </InputGroup> </VStack></VStack>Password with show / hide
Trailing icon button toggles type between password and text. Implemented as InputGroupPasswordDemo in the docs app (useState + "use client").
"use client";import {InputGroup,InputGroupAddon,InputGroupButton,InputGroupInput,} from "@reva/ui";import { Eye, EyeSlash } from "@phosphor-icons/react/ssr";import { useState } from "react";export function PasswordField() {const [showPassword, setShowPassword] = useState(false);return ( <InputGroup className="max-w-sm"> <InputGroupInput autoComplete="current-password" id="password" name="password" placeholder="Enter password" type={showPassword ? "text" : "password"} /> <InputGroupAddon align="inline-end"> <InputGroupButton aria-label={showPassword ? "Hide password" : "Show password"} aria-pressed={showPassword} type="button" onClick={() => setShowPassword((v) => !v)} > {showPassword ? <EyeSlash /> : <Eye />} </InputGroupButton> </InputGroupAddon> </InputGroup>);}Search with trailing button
import {InputGroup,InputGroupAddon,InputGroupButton,InputGroupInput,} from "@reva/ui";import { MagnifyingGlass } from "@phosphor-icons/react/ssr";<InputGroup className="max-w-sm"><InputGroupInput placeholder="Type to search…" type="search" /><InputGroupAddon align="inline-end"> <InputGroupButton layout="text" size="sm" type="button" variant="secondary"> <MagnifyingGlass /> Search </InputGroupButton></InputGroupAddon></InputGroup>Props
| Part | Notes |
|---|---|
InputGroup | React.ComponentProps<"div"> — single bordered shell. Optional size: sm | default | lg | xl (heights 32px / 40px / 44px / 48px; default default). |
InputGroupInput | Forwards to Input with borderless styles inside the group. Optional size overrides the group’s scale when set. |
InputGroupTextarea | Forwards to Textarea with borderless styles inside the group. Optional size overrides the group’s scale when set. |
InputGroupAddon | align: inline-start (default), inline-end, block-start, block-end. |
InputGroupButton | Native <button> + buttonVariants — variant, size (xs | sm | default | lg), layout (text | square). |
InputGroupText | Inline label / hint text inside an addon. |