Input Group
Icons, prefixes, suffixes, and actions inside one shared control boundary.
Preview
Use Input Group when a control needs an icon, unit, prefix, or related action inside its border. Keep its visible label, description, and error in Field, with the same 4px gap and caption-sized supporting text as Input.
Type “website” to filter the four sample projects locally.
Search four sample projects. No data leaves this page.
Installation
pnpm dlx shadcn@latest add https://migoy.stanleyaf.com/r/input-group.jsonInstalls Input Group and its Migoy Input, Textarea, and Button dependencies. Review any existing customizations to those components before accepting updates. It reuses your cn utility and semantic theme and does not change global CSS. Field, icons, and Spinner are separate items used in these examples. See the registry guide for registry endpoints and updates.
Composition
import {
InputGroup, InputGroupAddon, InputGroupButton,
InputGroupInput, InputGroupText, InputGroupTextarea,
} from '@/components/ui/input-group';| Part | Responsibility |
|---|---|
InputGroup | Shared border, surface, size, and focus ring |
InputGroupInput | Migoy Input with its individual border and outline removed |
InputGroupTextarea | Migoy Textarea styled for the shared boundary |
InputGroupAddon | Icon, text, or action region, positioned by align |
InputGroupText | Muted inline text such as a prefix or unit |
InputGroupButton | Migoy Button sized to fit inside the group |
Place the control first and addons after it in DOM order. align determines their visual position. Clicking a non-interactive addon focuses the input or textarea; clicking its button or link keeps that action's behavior. Place only one editing control in each group. Use FieldGroup for multiple fields.
Alignment
Set align on InputGroupAddon to position it beside, above, or below the editing control. Keep addons after the control in DOM order.
Inline alignment
inline-start is the default addon position; use inline-end for trailing content. Logical alignment follows the reading direction. Decorative icons sit 8px from their outer edge, with 8px between the addon and input text. Text-only outer edges use the size’s standard padding: 8px small, 12px default, and 16px large. Each side is handled independently. Embedded buttons retain their own padding and focus treatment.
Enter the host only. The prefix is visual; it is not part of the input value.
Prefixes and suffixes are visual content, not part of the submitted value. Include units and meaning in the accessible label or description, then hide duplicate decoration with aria-hidden. A visual https:// prefix does not turn a host-only value into a valid type="url" value; use a text input and assemble or validate the URL in your application.
Block alignment
Use block-start or block-end to place supporting content above or below the editing area. Block addons stack the group vertically; use them together or separately, without mixing inline addons in the same group.
0 of 280 characters. This demo does not save your note.
The grouped textarea is vertically resizable and supports native textarea props, including rows, maxLength, and ref. It now composes Migoy Textarea, installed as a registry dependency. Both versions share their base editing styles; the group owns the border, outline, radius, and padding. Its visible Field label remains outside the group; block content supplements that label.
Actions
Try the password toggle and clear button. Actions remain keyboard-focusable and have their own visible focus treatment. InputGroupButton defaults to type="button", so it does not accidentally submit a surrounding form.
Name icon-only actions independently. Toggle actions use a stable name plus aria-pressed. Use a filled circle with an X to clear an editable value. Show the action when the field has a value and return focus to the input after clearing. For a submit action, set type="submit" explicitly. Button variants such as ghost, secondary, and default are available; the group determines the embedded button's height.
Clear a value
Use one clear affordance. The demo uses a text input with a custom clear button. If you use a search input with a browser-provided clear button, avoid showing a duplicate.
<InputGroup>
<InputGroupInput aria-label="Project filter" ref={filterRef}
value={query} onChange={event => setQuery(event.target.value)} />
{query && (
<InputGroupAddon align="inline-end">
<InputGroupButton aria-label="Clear project filter" onClick={() => {
setQuery('');
filterRef.current?.focus();
}}>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM8 9.4 9.4 8l2.6 2.6L14.6 8 16 9.4 13.4 12l2.6 2.6-1.4 1.4-2.6-2.6L9.4 16 8 14.6l2.6-2.6L8 9.4Z" />
</svg>
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>Keyboard shortcut
Press Alt + Shift + P to focus the field. Use Kbd to display the shortcut and expose the binding on the input with aria-keyshortcuts.
Search scope
Use a Dropdown Menu radio group for a choice that changes what the input searches. Tab from the input to the scope button, open it with Enter, and use the arrow keys to move between choices. Escape closes the menu and returns focus to its trigger.
3 matching projects
Sizes
Set controlSize on the group, not its input. Heights and radii follow the same shared tokens and fallbacks as Input and Button.
| Size | Group height | Radius |
|---|---|---|
sm | 24px | 3px |
default | 32px | 4px |
lg | 44px | 6px |
Small editing controls use 12px / 16px text on desktop; their addons and embedded button text also use the caption role. Editing controls retain 16px text on smaller screens. Labels remain 14px / 20px. Icons and spinners are 14px in small groups and 16px in default and large groups; embedded button icons inherit the group scale. Small is for dense desktop layouts. Prefer large controls for touch; keep important actions outside the group if they need larger targets. Grouped textareas and block addons grow vertically instead of using a fixed height. Long input values scroll within the control.
Interaction states
The group has no decorative shadow. Keyboard focus on the editing control adds a shared 3px ring at 50% opacity around the entire group; embedded actions retain their own inset ring. Hover and focus examples below are pinned with the same utilities as the live interactions.
Static loading example; no request is running.
Set aria-invalid on the editing control to give the shared border and focus ring the destructive color. Keep the label neutral and explain the error below the group with FieldError. Connect descriptions and errors using aria-describedby.
<Field data-invalid>
<FieldLabel htmlFor="email">Email address</FieldLabel>
<InputGroup>
<InputGroupInput id="email" aria-invalid aria-describedby="email-error" />
<InputGroupAddon><Mail aria-hidden="true" /></InputGroupAddon>
</InputGroup>
<FieldError id="email-error">This email address is already in use.</FieldError>
</Field>disabled and readOnly belong on the input or textarea. A disabled editing control dims the group. Disable embedded actions explicitly too; a disabled input does not disable sibling buttons. For pending work, use a visible status and aria-busy; the loading example is static and sends no request.
<Field>
<FieldLabel htmlFor="workspace-search">Search workspace</FieldLabel>
<InputGroup aria-busy={isSearching}>
<InputGroupInput id="workspace-search" type="search" />
{isSearching && (
<InputGroupAddon align="inline-end">
<Spinner aria-hidden="true" />
</InputGroupAddon>
)}
</InputGroup>
<FieldDescription role="status">
{isSearching ? 'Searching workspace…' : resultSummary}
</FieldDescription>
</Field>Accessibility
- Keep the editing control labeled with
htmlForand a matching ID. - Decorative icons use
aria-hidden="true"; they are not extra Tab stops. - Tab reaches the input first, then interactive addons in DOM order. Shift+Tab reverses that order.
- Preserve native editing keys. Buttons support Enter and Space.
- Keep helper and error text outside the group and explicitly associated with the control.
- Use prefixes and units that fit the available width; keep longer explanations in FieldDescription.
See the Icons foundation for the shared size scale and icon accessibility guidelines.
API
| Part | Props |
|---|---|
InputGroup | Native div props, controlSize: sm, default (default), lg |
InputGroupAddon | Native div props, align: inline-start (default), inline-end, block-start, block-end |
InputGroupInput | Native input props; sizing comes from the group |
InputGroupTextarea | Native textarea props |
InputGroupButton | Button props except size; defaults to variant="ghost", type="button" |
InputGroupText | Native span props |
Based on shadcn Input Group, adapted to Migoy's native Input, Button, and shared control tokens.