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 casessecondary- 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. Usesmin-h-9for flexible height anditems-centerby default, switching toitems-startwhen 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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | (values: GroupRenderProps) => React.ReactNode | - | Child components (Input, TextArea, Prefix, Suffix) or render function. |
className | string | (values: GroupRenderProps) => string | - | CSS classes for styling, supports render props. |
style | React.CSSProperties | (values: GroupRenderProps) => React.CSSProperties | - | Inline styles, supports render props. |
fullWidth | boolean | false | Whether the input group should take full width of its container |
id | string | - | The element's unique identifier. |
Variant Props
| Prop | Type | Default | Description |
|---|---|---|---|
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
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessibility label when no visible label is present. |
aria-labelledby | string | - | ID of elements that label this group. |
aria-describedby | string | - | ID of elements that describe this group. |
aria-details | string | - | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | 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. |
type | string | 'text' | Input type (text, password, email, etc.). |
value | string | - | Current value (controlled). |
defaultValue | string | - | Default value (uncontrolled). |
placeholder | string | - | Placeholder text. |
disabled | boolean | - | Whether the input is disabled. |
readOnly | boolean | - | Whether the input is read-only. |
InputGroup.TextArea Props
InputGroup.TextArea inherits all props from React Aria's TextArea component.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | 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. |
value | string | - | Current value (controlled). |
defaultValue | string | - | Default value (uncontrolled). |
placeholder | string | - | Placeholder text. |
rows | number | - | Number of visible text lines. |
disabled | boolean | - | Whether the textarea is disabled. |
readOnly | boolean | - | Whether the textarea is read-only. |
InputGroup.Prefix Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | Content to display in the prefix (icons, text, etc.). |
className | string | - | CSS classes for styling. |
InputGroup.Suffix Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | Content to display in the suffix (icons, buttons, badges, etc.). |
className | string | - | CSS classes for styling. |
Related Showcases
See upstream InputGroup showcases. Product showcases are not part of the local component runtime.