Skip to content
Lenso UI

InputGroup

Group related input controls with prefix and suffix elements for enhanced form fields

Usage

import { InputGroup } from '@lenso/ui';
"use client";
import { Envelope } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
// Adapted from HeroUI v3.2.6, Apache-2.0.const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  icon: { width: 16, height: 16, color: "var(--muted)" },});
export function Default() {  return (    <TextField xstyle={styles.field} name="email">      <Label>Email address</Label>      <InputGroup>        <InputGroup.Prefix>          <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />        </InputGroup.Prefix>        <InputGroup.Input xstyle={styles.field} placeholder="[email protected]" />      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import {InputGroup, TextField, Label} from '@lenso/ui';
export default () => (  <TextField>    <Label />    <InputGroup>      <InputGroup.Prefix />      <InputGroup.Input /> {/* Or use InputGroup.TextArea for multiline input */}      <InputGroup.Suffix />    </InputGroup>  </TextField>)

InputGroup wraps an input field with optional prefix and suffix elements, creating a visually cohesive group. It's typically used within TextField to add icons, text, buttons, or other elements before or after the input. Use InputGroup.Input for single-line inputs or InputGroup.TextArea for multiline text inputs.

Examples

Variants

The InputGroup component supports two visual variants:

  • primary (default) - Standard styling with shadow, suitable for most use cases
  • secondary - Lower emphasis variant without shadow, suitable for use in Surface components
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  stack: { display: "flex", flexDirection: "column", gap: 16 },  field: { width: 280 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function Variants() {  return (    <div {...stylex.props(styles.stack)}>      <TextField xstyle={styles.field} name="primary">        <Label>Primary variant</Label>        <InputGroup variant="primary">          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input placeholder="[email protected]" />        </InputGroup>      </TextField>      <TextField xstyle={styles.field} name="secondary">        <Label>Secondary variant</Label>        <InputGroup variant="secondary">          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input placeholder="[email protected]" />        </InputGroup>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

In Surface

When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { Description, InputGroup, Label, Surface, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  surface: { borderRadius: 16, padding: 24 },  field: { width: "100%", maxWidth: 280 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function OnSurface() {  return (    <Surface xstyle={styles.surface}>      <TextField xstyle={styles.field} name="email">        <Label>Email address</Label>        <InputGroup variant="secondary">          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input xstyle={styles.field} placeholder="[email protected]" />        </InputGroup>        <Description>We'll never share this with anyone else</Description>      </TextField>    </Surface>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Loading State

Show a loading spinner in the suffix to indicate processing.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { InputGroup, Spinner, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  spinner: { width: 16, height: 16 },});export function WithLoadingSuffix() {  return (    <TextField xstyle={styles.field} name="status">      <InputGroup>        <InputGroup.Input xstyle={styles.field} defaultValue="Sending..." />        <InputGroup.Suffix>          <Spinner xstyle={styles.spinner} />        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Required Field

InputGroup respects the required state from its parent TextField.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { Description, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  stack: { display: "flex", flexDirection: "column", gap: 16 },  field: { width: "100%", maxWidth: 280 },  price: { width: "100%", maxWidth: 200 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function Required() {  return (    <div {...stylex.props(styles.stack)}>      <TextField xstyle={styles.field} name="email">        <Label required>Email address</Label>        <InputGroup>          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input required xstyle={styles.field} placeholder="[email protected]" />        </InputGroup>      </TextField>      <TextField xstyle={styles.field} name="price">        <Label required>Set a price</Label>        <InputGroup>          <InputGroup.Prefix>$</InputGroup.Prefix>          <InputGroup.Input required xstyle={styles.price} placeholder="0" type="number" />          <InputGroup.Suffix>USD</InputGroup.Suffix>        </InputGroup>        <Description>What customers would pay</Description>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Disabled State

InputGroup respects the disabled state from its parent TextField.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  stack: { display: "flex", flexDirection: "column", gap: 16 },  field: { width: "100%", maxWidth: 280 },  price: { width: "100%", maxWidth: 200 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function Disabled() {  return (    <div {...stylex.props(styles.stack)}>      <TextField disabled xstyle={styles.field} name="email">        <Label>Email address</Label>        <InputGroup>          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input xstyle={styles.field} defaultValue="[email protected]" />        </InputGroup>      </TextField>      <TextField disabled xstyle={styles.field} name="price">        <Label>Set a price</Label>        <InputGroup>          <InputGroup.Prefix>$</InputGroup.Prefix>          <InputGroup.Input xstyle={styles.price} defaultValue="10" type="number" />          <InputGroup.Suffix>USD</InputGroup.Suffix>        </InputGroup>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Full Width

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope, Eye } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  stack: { width: 400, display: "flex", flexDirection: "column", gap: 16 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function FullWidth() {  return (    <div {...stylex.props(styles.stack)}>      <TextField fullWidth name="email">        <Label>Email address</Label>        <InputGroup fullWidth>          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input placeholder="[email protected]" />        </InputGroup>      </TextField>      <TextField fullWidth name="password">        <Label>Password</Label>        <InputGroup fullWidth>          <InputGroup.Input placeholder="Enter password" type="password" />          <InputGroup.Suffix>            <Eye aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Suffix>        </InputGroup>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Text Prefix

Use text as a prefix, such as currency symbols or protocol prefixes.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 280 } });export function WithTextPrefix() {  return (    <TextField xstyle={styles.field} name="website">      <Label>Website</Label>      <InputGroup>        <InputGroup.Prefix>https://</InputGroup.Prefix>        <InputGroup.Input xstyle={styles.field} defaultValue="heroui.com" />      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Text Suffix

Use text as a suffix, such as domain extensions or units.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 280 } });export function WithTextSuffix() {  return (    <TextField xstyle={styles.field} name="website">      <Label>Website</Label>      <InputGroup>        <InputGroup.Input xstyle={styles.field} defaultValue="heroui" />        <InputGroup.Suffix>.com</InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Icon Prefix and Text Suffix

Combine an icon prefix with a text suffix.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Globe } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function WithIconPrefixAndTextSuffix() {  return (    <TextField xstyle={styles.field} name="website">      <Label>Website</Label>      <InputGroup>        <InputGroup.Prefix>          <Globe aria-hidden="true" {...stylex.props(styles.icon)} />        </InputGroup.Prefix>        <InputGroup.Input xstyle={styles.field} defaultValue="heroui" />        <InputGroup.Suffix>.com</InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Copy Button Suffix

Add an interactive button in the suffix, such as a copy button.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Copy } from "@gravity-ui/icons";import { Button, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  suffix: { paddingInlineEnd: 0 },  icon: { width: 16, height: 16 },});export function WithCopySuffix() {  return (    <TextField xstyle={styles.field} name="website">      <Label>Website</Label>      <InputGroup>        <InputGroup.Input xstyle={styles.field} defaultValue="heroui.com" />        <InputGroup.Suffix xstyle={styles.suffix}>          <Button isIconOnly aria-label="Copy" size="sm" variant="ghost">            <Copy aria-hidden="true" {...stylex.props(styles.icon)} />          </Button>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Icon Prefix and Copy Button

Combine an icon prefix with an interactive button suffix.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Copy, Globe } from "@gravity-ui/icons";import { Button, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  suffix: { paddingInlineEnd: 0 },  icon: { width: 16, height: 16 },  muted: { color: "var(--muted)" },});export function WithIconPrefixAndCopySuffix() {  return (    <TextField xstyle={styles.field} name="website">      <Label>Website</Label>      <InputGroup>        <InputGroup.Prefix>          <Globe aria-hidden="true" {...stylex.props(styles.icon, styles.muted)} />        </InputGroup.Prefix>        <InputGroup.Input xstyle={styles.field} defaultValue="heroui.com" />        <InputGroup.Suffix xstyle={styles.suffix}>          <Button isIconOnly aria-label="Copy" size="sm" variant="ghost">            <Copy aria-hidden="true" {...stylex.props(styles.icon)} />          </Button>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Password Toggle

Use a button in the suffix to toggle password visibility.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Eye, EyeSlash } from "@gravity-ui/icons";import { Button, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useState } from "react";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  suffix: { paddingInlineEnd: 0 },  icon: { width: 16, height: 16 },});export function PasswordWithToggle() {  const [isVisible, setIsVisible] = useState(false);  return (    <TextField xstyle={styles.field} name="password">      <Label>Password</Label>      <InputGroup>        <InputGroup.Input          xstyle={styles.field}          type={isVisible ? "text" : "password"}          value={isVisible ? "87$2h.3diua" : "••••••••"}          readOnly        />        <InputGroup.Suffix xstyle={styles.suffix}>          <Button            isIconOnly            aria-label={isVisible ? "Hide password" : "Show password"}            size="sm"            variant="ghost"            onClick={() => setIsVisible(!isVisible)}          >            {isVisible ? (              <Eye aria-hidden="true" {...stylex.props(styles.icon)} />            ) : (              <EyeSlash aria-hidden="true" {...stylex.props(styles.icon)} />            )}          </Button>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Keyboard Shortcut

Display keyboard shortcuts using the Kbd component.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { InputGroup, Kbd, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  suffix: { paddingInlineEnd: 8 },});export function WithKeyboardShortcut() {  return (    <TextField xstyle={styles.field} name="command">      <InputGroup>        <InputGroup.Input aria-label="Command" xstyle={styles.field} placeholder="Command" />        <InputGroup.Suffix xstyle={styles.suffix}>          <Kbd>            <Kbd.Abbr keyValue="command" />            <Kbd.Content>K</Kbd.Content>          </Kbd>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Badge Suffix

Add a badge or chip in the suffix to show status or labels.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Chip, InputGroup, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  suffix: { paddingInlineEnd: 8 },});export function WithBadgeSuffix() {  return (    <TextField xstyle={styles.field} name="email">      <InputGroup>        <InputGroup.Input          aria-label="Email address"          xstyle={styles.field}          placeholder="Email address"        />        <InputGroup.Suffix xstyle={styles.suffix}>          <Chip color="accent" size="md" variant="soft">            Pro          </Chip>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Validation

InputGroup automatically reflects invalid state from its parent TextField.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { FieldError, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  stack: { display: "flex", flexDirection: "column", gap: 16 },  field: { width: "100%", maxWidth: 280 },  price: { width: "100%", maxWidth: 200 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function Invalid() {  return (    <div {...stylex.props(styles.stack)}>      <TextField invalid xstyle={styles.field} name="email">        <Label required>Email address</Label>        <InputGroup>          <InputGroup.Prefix>            <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />          </InputGroup.Prefix>          <InputGroup.Input required xstyle={styles.field} placeholder="[email protected]" />        </InputGroup>        <FieldError match>Please enter a valid email address</FieldError>      </TextField>      <TextField invalid xstyle={styles.field} name="price">        <Label required>Set a price</Label>        <InputGroup>          <InputGroup.Prefix>$</InputGroup.Prefix>          <InputGroup.Input required xstyle={styles.price} placeholder="0" type="number" />          <InputGroup.Suffix>USD</InputGroup.Suffix>        </InputGroup>        <FieldError match>Price must be greater than 0</FieldError>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Prefix Icon

Add an icon before the input field.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { Description, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function WithPrefixIcon() {  return (    <TextField xstyle={styles.field} name="email">      <Label>Email address</Label>      <InputGroup>        <InputGroup.Prefix>          <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />        </InputGroup.Prefix>        <InputGroup.Input xstyle={styles.field} placeholder="[email protected]" />      </InputGroup>      <Description>We'll never share this with anyone else</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Suffix Icon

Add an icon after the input field.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { Description, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function WithSuffixIcon() {  return (    <TextField xstyle={styles.field} name="email">      <Label>Email address</Label>      <InputGroup>        <InputGroup.Input xstyle={styles.field} placeholder="[email protected]" />        <InputGroup.Suffix>          <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />        </InputGroup.Suffix>      </InputGroup>      <Description>We don't send spam</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Prefix and Suffix

Combine both prefix and suffix elements.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Description, InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 280 },  input: { width: "100%", maxWidth: 200 },});export function WithPrefixAndSuffix() {  return (    <TextField xstyle={styles.field} name="price">      <Label>Set a price</Label>      <InputGroup>        <InputGroup.Prefix>$</InputGroup.Prefix>        <InputGroup.Input xstyle={styles.input} defaultValue="10" type="number" />        <InputGroup.Suffix>USD</InputGroup.Suffix>      </InputGroup>      <Description>What customers would pay</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With TextArea

Use InputGroup.TextArea for multiline text inputs with prefix and suffix elements. When a textarea is present, the container automatically adjusts its height to accommodate the content and aligns prefix/suffix elements to the top.

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { ArrowUp, At, Microphone, PlugConnection, Plus } from "@gravity-ui/icons";import { Button, InputGroup, Kbd, Spinner, TextField, Tooltip } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useState } from "react";const styles = stylex.create({  field: {    display: "flex",    flexDirection: "column",    width: { default: 384, "@media (min-width: 640px)": 512 },  },  group: { display: "flex", flexDirection: "column", gap: 8, borderRadius: 24, paddingBlock: 8 },  prefix: { paddingInline: 12, paddingBlock: 0 },  input: { width: "100%", resize: "none", paddingInline: 14, paddingBlock: 0 },  suffix: {    display: "flex",    width: "100%",    alignItems: "center",    gap: 6,    paddingInline: 12,    paddingBlock: 0,  },  actions: { marginInlineStart: "auto", display: "flex", alignItems: "center", gap: 6 },  tooltip: { display: "flex", alignItems: "center", gap: 4 },  hint: { fontSize: 12, lineHeight: "16px" },  key: { height: 16, borderRadius: "var(--radius-sm)", paddingInline: 4 },});export function WithTextArea() {  const [value, setValue] = useState("");  const [isSubmitting, setIsSubmitting] = useState(false);  const handleSubmit = () => {    if (!value.trim()) return;    setIsSubmitting(true);    setTimeout(() => {      setIsSubmitting(false);      setValue("");    }, 1000);  };  return (    <TextField fullWidth xstyle={styles.field} name="prompt">      <InputGroup fullWidth xstyle={styles.group}>        <InputGroup.Prefix xstyle={styles.prefix}>          <Button aria-label="Add context" size="sm" variant="outline">            <At aria-hidden="true" />            Add Context          </Button>        </InputGroup.Prefix>        <InputGroup.TextArea          aria-label="Prompt input"          xstyle={styles.input}          placeholder="Assign tasks or ask anything..."          rows={5}          value={value}          onChange={(event) => setValue(event.target.value)}        />        <InputGroup.Suffix xstyle={styles.suffix}>          <Tooltip>            <Tooltip.Trigger              delay={0}              render={<Button isIconOnly aria-label="Attach file" size="sm" variant="tertiary" />}            >              <Plus aria-hidden="true" />            </Tooltip.Trigger>            <Tooltip.Portal>              <Tooltip.Positioner>                <Tooltip.Popup>                  <p {...stylex.props(styles.hint)}>Add a files and more</p>                </Tooltip.Popup>              </Tooltip.Positioner>            </Tooltip.Portal>          </Tooltip>          <Tooltip>            <Tooltip.Trigger              delay={0}              render={<Button isIconOnly aria-label="Connect Apps" size="sm" variant="tertiary" />}            >              <PlugConnection aria-hidden="true" />            </Tooltip.Trigger>            <Tooltip.Portal>              <Tooltip.Positioner>                <Tooltip.Popup>                  <p {...stylex.props(styles.hint)}>Connect apps</p>                </Tooltip.Popup>              </Tooltip.Positioner>            </Tooltip.Portal>          </Tooltip>          <div {...stylex.props(styles.actions)}>            <Tooltip>              <Tooltip.Trigger                delay={0}                render={<Button isIconOnly aria-label="Voice input" size="sm" variant="ghost" />}              >                <Microphone aria-hidden="true" />              </Tooltip.Trigger>              <Tooltip.Portal>                <Tooltip.Positioner>                  <Tooltip.Popup>                    <p {...stylex.props(styles.hint)}>Voice input</p>                  </Tooltip.Popup>                </Tooltip.Positioner>              </Tooltip.Portal>            </Tooltip>            <Tooltip>              <Tooltip.Trigger                delay={0}                render={                  <Button                    isIconOnly                    aria-label="Send prompt"                    disabled={!value.trim()}                    isLoading={isSubmitting}                    onClick={handleSubmit}                  />                }              >                {isSubmitting ? (                  <Spinner color="current" size="sm" />                ) : (                  <ArrowUp aria-hidden="true" />                )}              </Tooltip.Trigger>              <Tooltip.Portal>                <Tooltip.Positioner>                  <Tooltip.Popup xstyle={styles.tooltip}>                    <p {...stylex.props(styles.hint)}>Send</p>                    <Kbd xstyle={styles.key}>                      <Kbd.Abbr keyValue="enter" />                    </Kbd>                  </Tooltip.Popup>                </Tooltip.Positioner>              </Tooltip.Portal>            </Tooltip>          </div>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Usage Example

import {InputGroup, TextField, Label, Button} from '@lenso/ui';import {Icon} from '@iconify/react';
function Example() {  return (    <TextField>      <Label>Email</Label>      <InputGroup>        <InputGroup.Prefix>          <Icon icon="gravity-ui:envelope" />        </InputGroup.Prefix>        <InputGroup.Input placeholder="[email protected]" />        <InputGroup.Suffix>          <Button isIconOnly aria-label="Confirm" size="sm" variant="ghost">            <Icon icon="gravity-ui:check" />          </Button>        </InputGroup.Suffix>      </InputGroup>    </TextField>  );}

TextArea Usage Example

import {Envelope} from "@gravity-ui/icons";import {Description, FieldError, InputGroup, Label, TextField} from "@lenso/ui";import {useState} from "react";
function TextAreaExample() {  const [feedback, setFeedback] = useState("");
  return (    <TextField fullWidth isInvalid={feedback.length > 500} name="feedback" onChange={setFeedback}>      <Label>Your Feedback</Label>      <InputGroup fullWidth>        <InputGroup.Prefix>          <Envelope className="size-4 text-muted" />        </InputGroup.Prefix>        <InputGroup.TextArea          className="resize-none"          placeholder="Share your thoughts, suggestions, or issues..."          rows={5}          value={feedback}        />      </InputGroup>      <Description className="flex w-full items-center justify-between px-1">        <span>Maximum 500 characters.</span>        <span className="ml-auto">{feedback.length}/500</span>      </Description>      <FieldError>Feedback must be less than 500 characters</FieldError>    </TextField>  );}

Customization

Tailwind CSS

"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  field: { width: "100%", maxWidth: 320 },  group: {    borderRadius: 12,    borderWidth: 1,    borderStyle: "solid",    borderColor: "color-mix(in oklab, var(--border) 80%, transparent)",    backgroundColor: "var(--default)",    boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",  },  icon: { width: 16, height: 16, color: "var(--muted)" },});export function CustomStyles() {  return (    <TextField xstyle={styles.field} name="email">      <Label>Work email</Label>      <InputGroup xstyle={styles.group}>        <InputGroup.Prefix>          <Envelope aria-hidden="true" {...stylex.props(styles.icon)} />        </InputGroup.Prefix>        <InputGroup.Input placeholder="[email protected]" />      </InputGroup>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

InputGroup uses CSS classes that can be customized. Override the component classes to match your design system.

@layer components {  .input-group {    @apply bg-field text-field-foreground shadow-field rounded-field inline-flex min-h-9 items-center overflow-hidden border text-sm outline-none;  }
  .input-group__input {    @apply flex-1 rounded-none border-0 bg-transparent px-3 py-2 shadow-none outline-none;  }
  .input-group__prefix {    @apply text-field-placeholder rounded-l-field flex h-full items-center justify-center rounded-r-none bg-transparent px-3;  }
  .input-group__suffix {    @apply text-field-placeholder rounded-r-field flex h-full items-center justify-center rounded-l-none bg-transparent px-3;  }
  /* Secondary variant */  .input-group--secondary {    @apply shadow-none;    background-color: var(--color-default);  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

Base Classes [!toc]

  • .input-group – Root container with border, background, and flex layout. Uses min-h-9 for flexible height and items-center by default, switching to items-start when a textarea is present.
  • .input-group__input – Input element with transparent background and no border. Also used as the base class for textarea elements.
  • .input-group__prefix – Prefix container with left border radius. Aligns to top when used with textarea.
  • .input-group__suffix – Suffix container with right border radius. Aligns to top when used with textarea.

Variant Classes [!toc]

  • .input-group--primary – Primary variant with shadow (default)
  • .input-group--secondary – Secondary variant without shadow, suitable for use in surfaces

Note: When using InputGroup.TextArea, the container automatically switches from items-center to items-start alignment and uses height: auto instead of a fixed height. Prefix and suffix elements align to the top with additional padding to match the textarea's vertical padding. The textarea uses the same .input-group__input base class with textarea-specific styles (minimum height and vertical resize) applied via the [data-slot="input-group-textarea"] attribute selector.

Interactive States

InputGroup automatically manages these data attributes based on its state:

  • Hover: [data-hovered] - Applied when hovering over the group
  • Focus Within: [data-focus-within] - Applied when the input is focused
  • Invalid: [data-invalid] - Applied when parent TextField is invalid
  • Disabled: [data-disabled] or [aria-disabled] - Applied when parent TextField is disabled

API Reference

InputGroup

InputGroup inherits all props from React Aria's Group component.

Base Props

PropTypeDefaultDescription
childrenReact.ReactNode | (values: GroupRenderProps) => React.ReactNode-Child components (Input, TextArea, Prefix, Suffix) or render function.
classNamestring | (values: GroupRenderProps) => string-CSS classes for styling, supports render props.
styleReact.CSSProperties | (values: GroupRenderProps) => React.CSSProperties-Inline styles, supports render props.
fullWidthbooleanfalseWhether the input group should take full width of its container
idstring-The element's unique identifier.

Variant Props

PropTypeDefaultDescription
variant"primary" | "secondary""primary"Visual variant of the component. primary is the default style with shadow. secondary is a lower emphasis variant without shadow, suitable for use in surfaces.

Accessibility Props

PropTypeDefaultDescription
aria-labelstring-Accessibility label when no visible label is present.
aria-labelledbystring-ID of elements that label this group.
aria-describedbystring-ID of elements that describe this group.
aria-detailsstring-ID of elements with additional details.
role'group' | 'region' | 'presentation''group'Accessibility role for the group. Use 'region' for important content, 'presentation' for visual-only grouping.

Composition Components

InputGroup works with these subcomponents:

  • InputGroup.Root - Root container (also available as InputGroup)
  • InputGroup.Input - Single-line input element component
  • InputGroup.TextArea - Multiline textarea element component
  • InputGroup.Prefix - Prefix container component
  • InputGroup.Suffix - Suffix container component

InputGroup.Input Props

InputGroup.Input inherits all props from React Aria's Input component.

PropTypeDefaultDescription
classNamestring-CSS classes for styling.
variant"primary" | "secondary""primary"Visual variant of the input. primary is the default style with shadow. secondary is a lower emphasis variant without shadow, suitable for use in surfaces.
typestring'text'Input type (text, password, email, etc.).
valuestring-Current value (controlled).
defaultValuestring-Default value (uncontrolled).
placeholderstring-Placeholder text.
disabledboolean-Whether the input is disabled.
readOnlyboolean-Whether the input is read-only.

InputGroup.TextArea Props

InputGroup.TextArea inherits all props from React Aria's TextArea component.

PropTypeDefaultDescription
classNamestring-CSS classes for styling.
variant"primary" | "secondary""primary"Visual variant of the textarea. primary is the default style with shadow. secondary is a lower emphasis variant without shadow, suitable for use in surfaces.
valuestring-Current value (controlled).
defaultValuestring-Default value (uncontrolled).
placeholderstring-Placeholder text.
rowsnumber-Number of visible text lines.
disabledboolean-Whether the textarea is disabled.
readOnlyboolean-Whether the textarea is read-only.

InputGroup.Prefix Props

PropTypeDefaultDescription
childrenReact.ReactNode-Content to display in the prefix (icons, text, etc.).
classNamestring-CSS classes for styling.

InputGroup.Suffix Props

PropTypeDefaultDescription
childrenReact.ReactNode-Content to display in the suffix (icons, buttons, badges, etc.).
classNamestring-CSS classes for styling.

See upstream InputGroup showcases. Product showcases are not part of the local component runtime.