Skip to content

Input group

Wrap a .re-input in .re-input-group to attach prefix/suffix affixes, inline action buttons, or a trailing button — all reading as a single control. The group owns the border and the focus ring (via :focus-within); the inner input’s own frame is stripped so there’s no double outline. Size follows the inner input’s data-size.

.re-input-group__text is a tinted, non-interactive prefix or suffix — units, currency, a protocol, an @.

Live example
<label class="re-field">
  <span class="re-field__label">Website</span>
  <span class="re-input-group">
    <span class="re-input-group__text">https://</span>
    <input class="re-input" type="text" placeholder="example" />
    <span class="re-input-group__text">.com</span>
  </span>
</label>
<label class="re-field">
  <span class="re-field__label">Price</span>
  <span class="re-input-group">
    <span class="re-input-group__text">$</span>
    <input class="re-input" type="number" inputmode="decimal" placeholder="0.00" />
    <span class="re-input-group__text">USD</span>
  </span>
</label>

Drop a .re-button in the group as a trailing control; its corners are clipped to the group.

Live example
<span class="re-input-group" role="search">
  <input class="re-input" type="search" aria-label="Search" placeholder="Search…" />
  <button class="re-button" type="submit">Search</button>
</span>

Disabled and invalid are reflected from the contained control (:disabled, aria-invalid / :user-invalid).

Live example
<label class="re-field">
  <span class="re-field__label">Disabled</span>
  <span class="re-input-group">
    <span class="re-input-group__text">@</span>
    <input class="re-input" type="text" value="handle" disabled />
  </span>
</label>
<label class="re-field">
  <span class="re-field__label">Invalid</span>
  <span class="re-input-group">
    <span class="re-input-group__text">@</span>
    <input class="re-input" type="text" value="bad value" aria-invalid="true" />
  </span>
</label>

The input group is CSS-only — a passive flex wrapper with no behavior of its own. Keyboard, focus order, and announcements all come from the native controls you place inside it; the group only relocates the field’s frame and focus ring.

  • Keyboard — native. Tab reaches the inner .re-input (type to edit) and any attached .re-button (Enter/Space to activate) in source order. The .re-input-group__text affix is non-interactive (user-select: none) and takes no focus.
  • Focus — the group, not the field, shows the ring: :focus-within recolours its border to --re-color-focus-ring and paints --re-shadow-focus outside the border box, while the inner input’s own :focus-visible ring is stripped so there’s no double outline. An optional .re-input-group__action icon button carries its own inset :focus-visible ring so it stays visible against the clipped, rounded group.
  • Semantics — associate a label by wrapping the group in <label class="re-field"> with a .re-field__label span (as the affix and state demos do); a standalone group such as the search example labels its input directly with aria-label. Affix text is real visible text, so it is read in reading order — not hidden. Set aria-invalid="true" on the inner input to recolour the group’s border and ring to --re-color-danger-border / --re-color-danger-500, announced as invalid; :user-invalid styles the same way after the user has interacted. The search demo marks the group role="search" so it’s exposed as a search landmark.
  • Notes — invalid and disabled tinting is reflected with :has(), purely cosmetic: on engines without :has() the styling is skipped but the contained control and its native :disabled / aria-invalid state are unaffected. See the field, input, and button pages and the accessibility guide.