InputGroup

Display additional information or actions alongside input fields

Installation

pnpm add @wandercom/design-system-web

Usage

18 lines
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupButton,
  InputGroupText,
} from '@wandercom/design-system-web/ui/input-group';

export function Example() {
  return (
    <InputGroup>
      <InputGroupAddon>
        <SearchIcon />
      </InputGroupAddon>
      <InputGroupInput placeholder="Search..." />
    </InputGroup>
  );
}

Examples

Sizes

Input groups support two size variants that match the Input component.

Loading example...

With icon

Add icons to provide visual context for input fields.

Loading example...

With text prefix or hint

Display static text prefixes like protocols (hint) or inline prefix labels.

Loading example...

Block positioning

Position addons above or below the input for vertical layouts.

Loading example...

Character count

Combine the useCharacterLimit hook with the InputGroupCharacterCount addon to add a count/max counter to any input or textarea. The hook is framework-agnostic — its returned value and onValueChange plug into React Hook Form, TanStack Form, or any controlled form library without modification. Place the counter inside an InputGroupAddon aligned inline-end for inputs or block-end for textareas.

Behavior: any input that would push the value past maxLength — typing, paste, drop, IME composition — is rejected, so the value is guaranteed never to exceed the cap. The HTML maxLength attribute is not used because it silently truncates pasted content; this hook rejects the whole paste instead.

Loading example...
22 lines
import {
  InputGroup,
  InputGroupAddon,
  InputGroupCharacterCount,
  InputGroupInput,
  useCharacterLimit,
} from '@wandercom/design-system-web/ui/input-group';

export function Example() {
  const { count, maxLength, inputProps } = useCharacterLimit({
    maxLength: 40,
  });

  return (
    <InputGroup>
      <InputGroupInput {...inputProps} placeholder="Listing title" />
      <InputGroupAddon align="inline-end">
        <InputGroupCharacterCount count={count} maxLength={maxLength} />
      </InputGroupAddon>
    </InputGroup>
  );
}

For a textarea, swap InputGroupInput for InputGroupTextarea and align the addon block-end so the counter sits at the bottom of the field.

Loading example...
26 lines
import {
  InputGroup,
  InputGroupAddon,
  InputGroupCharacterCount,
  InputGroupTextarea,
  useCharacterLimit,
} from '@wandercom/design-system-web/ui/input-group';

export function Example() {
  const { count, maxLength, inputProps } = useCharacterLimit({
    maxLength: 70,
  });

  return (
    <InputGroup>
      <InputGroupTextarea
        {...inputProps}
        placeholder="Tell guests what makes this stay special"
        rows={4}
      />
      <InputGroupAddon align="block-end" className="justify-end">
        <InputGroupCharacterCount count={count} maxLength={maxLength} />
      </InputGroupAddon>
    </InputGroup>
  );
}

Props

InputGroup

size?:

"default" | "sm"
Size variant of the input group. Defaults to "default".

className?:

string
Additional CSS classes to apply to the group container.

InputGroupAddon

align?:

"inline-start" | "inline-end" | "hint" | "block-start" | "block-end"
Position of the addon relative to the input. Use "hint" for flush text prefixes (e.g. "https://"). Defaults to "inline-start".

className?:

string
Additional CSS classes to apply to the addon container.

InputGroupButton

size?:

"xs" | "sm" | "icon-xs" | "icon-sm" | "icon-md"
Button size variant. Defaults to "sm".

variant?:

"primary" | "secondary" | "outline" | "ghost" | "destructive" | "checkout"
Button visual style variant. Defaults to "ghost".

type?:

"button" | "submit" | "reset"
HTML button type attribute. Defaults to "button".

className?:

string
Additional CSS classes to apply to the button.

InputGroupText

className?:

string
Additional CSS classes to apply to the text span.

InputGroupInput

Inherits font size, line height, and padding from the parent InputGroup size variant automatically.

className?:

string
Additional CSS classes to apply to the input element.

type?:

string
HTML input type attribute (text, email, password, etc.).

InputGroupCharacterCount

Renders inline as a <span> — place inside an InputGroupAddon to position it. Switches to text-error when count > maxLength and announces the overflow via aria-live="polite" only while over the limit (kept silent during normal typing to avoid noisy per-keystroke readouts).

count:

number
Current character count of the input value. Pair with the `count` returned from `useCharacterLimit`.

maxLength:

number
Maximum allowed character count. Displayed in the counter as `count/maxLength`.

className?:

string
Additional CSS classes to apply to the counter span.

useCharacterLimit

Framework-agnostic hook that returns spreadable input props. Works with both <input> and <textarea>. Does not use the HTML maxLength attribute (which silently truncates paste) — instead, it rejects any input event (insertText, insertFromPaste, insertFromDrop, insertCompositionText, etc.) that would push the value past maxLength.

maxLength:

number
Maximum allowed character count. Any input that would exceed this cap — typing, paste, drop, IME — is rejected.

value?:

string
Controlled value. When provided, internal state is bypassed.

defaultValue?:

string
Initial value for uncontrolled usage. Ignored when `value` is provided.

onValueChange?:

(value: string) => void
Called whenever the value changes. Wire form libraries (React Hook Form, TanStack Form, etc.) here.

Returns an object with value, count, maxLength, and an inputProps object ready to spread onto InputGroupInput or InputGroupTextarea. The spread includes value, onChange, and a ref callback that attaches a native beforeinput listener — the listener enforces maxLength by rejecting any insertion (typing, paste, drop, IME composition) that would exceed the cap, so consumers don't need to handle truncation themselves. The native listener is required because React's synthetic onBeforeInput doesn't reliably cancel the underlying input event.

Accessibility

The InputGroup component includes proper accessibility features:

  • Uses role="group" to indicate grouped elements
  • Focus states are properly managed across the group
  • Invalid states are visually indicated with aria-invalid
  • Clicking on addons focuses the input for better usability

For proper focus navigation, the InputGroupAddon component should be placed after the input element when using inline-end alignment.

InputGroup