suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Input Group

A text field with icons, text, buttons or keyboard hints attached inside its border.

Input Group is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

import { MagnifyingGlassIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"

export default function InputGroupDemo() {
  return (
    <div className="w-96">
      <InputGroup>
        <InputGroupInput placeholder="Search..." />
        <InputGroupAddon>
          <MagnifyingGlassIcon />
        </InputGroupAddon>
        <InputGroupAddon align="inline-end">12 results</InputGroupAddon>
      </InputGroup>
    </div>
  )
}

InputGroup is the container that draws the field's background, border and focus ring. Inside it, place one control, either InputGroupInput or InputGroupTextarea, and one or more InputGroupAddon elements. An addon can hold icons, InputGroupText, InputGroupButton or a Kbd, and is placed before, after, above or below the control with its align prop.

Use an input group when supporting content belongs inside the field, such as a search icon, a currency symbol, a clear button or a send button. For a plain field, use Input; for a field with a filterable list of options, use Combobox, which is built on this component.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/input-group

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add input-group

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add class-variance-authority
  1. Add the pieces it builds on: Button, Input, Textarea.

  2. Copy the source into components/ui/input-group.tsx:

components/ui/input-group.tsx
"use client"

import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils"

function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="input-group"
      role="group"
      className={cn(
        "group/input-group relative flex h-11 w-full min-w-0 items-center rounded-lg border border-transparent bg-secondary transition-[color,box-shadow,background-color] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 in-data-[slot=combobox-content]:outline-none! has-data-[align=block-end]:rounded-xl has-data-[align=block-start]:rounded-xl has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[[data-slot][aria-invalid=true]]:bg-destructive/5 dark:has-[[data-slot][aria-invalid=true]]:bg-destructive/10 has-[textarea]:rounded-xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5 data-[disabled=true]:opacity-50 has-[[data-slot=input-group-control]:disabled]:opacity-50",
  1. Update the import paths to match your project.

Usage

import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea } from "@/components/ui/input-group"
<div className="w-96">
  <InputGroup>
    <InputGroupInput placeholder="Search..." />
    <InputGroupAddon>
      <MagnifyingGlassIcon />
    </InputGroupAddon>
    <InputGroupAddon align="inline-end">12 results</InputGroupAddon>
  </InputGroup>
</div>

Addons are positioned with CSS order, so you can place them before or after the control in your markup. inline-start and inline-end sit beside the control; block-start and block-end sit above or below it and make the group grow to fit. Clicking an addon outside its buttons focuses the group's input.

InputGroupButton defaults to type="button", so it does not submit a surrounding form unless you set type="submit". Set aria-invalid on the control to show the error style on the whole group. To dim a disabled group's addons, disable the control and set data-disabled="true" on InputGroup.

Examples

Search

A search field with a clear button that appears only when there is text.

import { MagnifyingGlassIcon, XCircleIcon } from "@phosphor-icons/react"
import * as React from "react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group"

function SearchField() {
  const [query, setQuery] = React.useState("MacBook")
  return (
    <InputGroup>
      <InputGroupInput
        aria-label="Search"
        placeholder="Search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <InputGroupAddon>
        <MagnifyingGlassIcon />
      </InputGroupAddon>
      {query && (
        <InputGroupAddon align="inline-end">
          <InputGroupButton
            size="icon-xs"
            aria-label="Clear search"
            className="text-muted-foreground"
            onClick={() => setQuery("")}
          >
            <XCircleIcon className="fill-muted-foreground text-background" />
          </InputGroupButton>
        </InputGroupAddon>
      )}
    </InputGroup>
  )
}

export default function InputGroupSearch() {
  return (
    <div className="w-96">
      <SearchField />
    </div>
  )
}

With text

Text addons for a currency symbol and code, and for a URL prefix and suffix.

import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/components/ui/input-group"

export default function InputGroupWithText() {
  return (
    <div className="w-96">
      <div className="flex flex-col gap-4">
        <InputGroup>
          <InputGroupAddon>
            <InputGroupText>$</InputGroupText>
          </InputGroupAddon>
          <InputGroupInput placeholder="0.00" />
          <InputGroupAddon align="inline-end">
            <InputGroupText>USD</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
        <InputGroup>
          <InputGroupAddon>
            <InputGroupText>https://</InputGroupText>
          </InputGroupAddon>
          <InputGroupInput placeholder="example.com" className="ps-0.5" />
          <InputGroupAddon align="inline-end">
            <InputGroupText>.com</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
      </div>
    </div>
  )
}

With buttons

Buttons inside the field for copying a link, subscribing, and showing information.

import { CheckIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group"

export default function InputGroupWithButtons() {
  return (
    <div className="w-96">
      <div className="flex flex-col gap-4">
        <InputGroup>
          <InputGroupInput
            defaultValue="https://example.com/invite/x7k2"
            aria-label="Invite link"
            readOnly
          />
          <InputGroupAddon align="inline-end">
            <InputGroupButton size="icon-xs" aria-label="Copy">
              <CopyIcon />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
        <InputGroup>
          <InputGroupInput placeholder="Enter your email" />
          <InputGroupAddon align="inline-end">
            <InputGroupButton variant="secondary">Subscribe</InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
        <InputGroup>
          <InputGroupInput placeholder="Username" defaultValue="janedoe" />
          <InputGroupAddon align="inline-end">
            <InputGroupButton size="icon-xs" aria-label="Info">
              <InfoIcon />
            </InputGroupButton>
            <CheckIcon className="text-green-600" />
          </InputGroupAddon>
        </InputGroup>
      </div>
    </div>
  )
}

Button sizes

The xs, icon-xs and icon-sm button sizes in one addon.

import { CopyIcon, PaperclipIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group"

export default function InputGroupButtonSizes() {
  return (
    <div className="w-96">
      <InputGroup>
        <InputGroupInput placeholder="Type a command..." />
        <InputGroupAddon align="inline-end">
          <InputGroupButton size="xs">Extra small</InputGroupButton>
          <InputGroupButton size="icon-xs" aria-label="Copy">
            <CopyIcon />
          </InputGroupButton>
          <InputGroupButton size="icon-sm" aria-label="Attach">
            <PaperclipIcon />
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

With kbd

A search field with a keyboard shortcut hint at the end.

import { MagnifyingGlassIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import { Kbd } from "@/components/ui/kbd"

export default function InputGroupWithKbd() {
  return (
    <div className="w-96">
      <InputGroup>
        <InputGroupInput placeholder="Search documentation..." />
        <InputGroupAddon>
          <MagnifyingGlassIcon />
        </InputGroupAddon>
        <InputGroupAddon align="inline-end">
          <Kbd mod>K</Kbd>
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

Block addons

Addons placed above and below the input with block-start and block-end.

import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@/components/ui/input-group"

export default function InputGroupBlockAddons() {
  return (
    <div className="w-96">
      <div className="flex flex-col gap-4">
        <InputGroup>
          <InputGroupInput placeholder="Enter a title" />
          <InputGroupAddon align="block-start">
            <InputGroupText>Title</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
        <InputGroup>
          <InputGroupInput placeholder="Enter an amount" />
          <InputGroupAddon align="block-end">
            <InputGroupText>Minimum payment is $5.00</InputGroupText>
          </InputGroupAddon>
        </InputGroup>
      </div>
    </div>
  )
}

With textarea

A chat-style textarea with attach and send buttons and usage text in a bottom addon.

import { ArrowUpIcon, PaperclipIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupText,
  InputGroupTextarea,
} from "@/components/ui/input-group"

export default function InputGroupWithTextarea() {
  return (
    <div className="w-96">
      <InputGroup>
        <InputGroupTextarea placeholder="Ask, search, or chat..." />
        <InputGroupAddon align="block-end">
          <InputGroupButton size="icon-xs" aria-label="Attach file">
            <PaperclipIcon />
          </InputGroupButton>
          <InputGroupText className="ms-auto">52% used</InputGroupText>
          <InputGroupButton
            variant="default"
            size="icon-xs"
            className="rounded-full"
            aria-label="Send"
          >
            <ArrowUpIcon />
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

Invalid

An invalid input that shows the error style on the whole group.

import { InfoIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"

export default function InputGroupInvalid() {
  return (
    <div className="w-96">
      <InputGroup>
        <InputGroupInput defaultValue="jane@" aria-label="Email" aria-invalid />
        <InputGroupAddon align="inline-end">
          <InfoIcon className="text-destructive" />
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

Disabled

A disabled group with a dimmed background and addon.

import { MagnifyingGlassIcon } from "@phosphor-icons/react"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"

export default function InputGroupDisabled() {
  return (
    <div className="w-96">
      <InputGroup data-disabled="true">
        <InputGroupInput placeholder="Search..." disabled />
        <InputGroupAddon>
          <MagnifyingGlassIcon />
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

API Reference

Anatomy6 parts
<InputGroup>
<InputGroupInput />
<InputGroupAddon>
<InputGroupText />
<InputGroupButton />
</InputGroupAddon>
<InputGroupTextarea />
</InputGroup>
01

InputGroup

The container that draws the field and holds the control and addons.

Also accepts the props of React.ComponentProps<"div">.

02

InputGroupAddon

Holds icons, text or buttons beside, above or below the control.

PropTypeDefaultDescription
align"inline-start" | "inline-end" | "block-start" | "block-end""inline-start"Places the addon before (inline-start) or after (inline-end) the control, or above (block-start) or below (block-end) it.

Also accepts the props of React.ComponentProps<"div">.

03

InputGroupButton

A compact button sized to fit inside the field.

PropTypeDefaultDescription
size"xs" | "sm" | "icon-xs" | "icon-sm""xs"xs for a small text button, or icon-xs and icon-sm for square icon buttons. sm keeps the base button size.
type"button" | "submit" | "reset""button"The native button type. Defaults to button so the button does not submit forms.

Also accepts the props of React.ComponentProps<typeof Button>.

04

InputGroupText

Static text or an icon with text, such as a unit or prefix.

Also accepts the props of React.ComponentProps<"span">.

05

InputGroupInput

The single-line text control of the group.

Also accepts the props of React.ComponentProps<"input">.

06

InputGroupTextarea

The multi-line text control of the group.

Also accepts the props of React.ComponentProps<"textarea">.

Accessibility

  • InputGroup and InputGroupAddon have the group role.
  • Give the control an accessible name with a label or aria-label; placeholder text and addon text do not name it.
  • Give icon-only buttons an accessible name with aria-label.
  • Buttons in addons are reachable with Tab after the control.
  • Set aria-invalid on the control to communicate a validation error.

Notes

  • Styling hooks: data-slot values input-group, input-group-addon and input-group-control (on the input or textarea). Addons set data-align, and buttons set data-size.
  • The group's rounded corners grow when it contains a textarea or a block addon.
  • Uses the secondary, muted-foreground and destructive colors, and opacity-50 when disabled.
iOS
Input Group on iOS in light modeInput Group on iOS in dark mode
import { CopyIcon, MagnifyingGlassIcon, XIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Icon } from "@/components/ui/icon"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from "@/components/ui/input-group"
import { Spinner } from "@/components/ui/spinner"

function SearchDemo() {
  const [value, setValue] = React.useState("")
  return (
    <InputGroup>
      <InputGroupInput
        placeholder="Search"
        value={value}
        onValueChange={setValue}
      />
      <InputGroupAddon>
        <Icon as={MagnifyingGlassIcon} />
      </InputGroupAddon>
      {value ? (
        <InputGroupAddon align="inline-end">
          <InputGroupButton
            size="icon-xs"
            accessibilityLabel="Clear"
            onPress={() => setValue("")}
          >
            <Icon as={XIcon} />
          </InputGroupButton>
        </InputGroupAddon>
      ) : null}
    </InputGroup>
  )
}

export function InputGroupExample() {
  return (
    <View className="gap-4">
      <SearchDemo />
      <InputGroup>
        <InputGroupAddon>
          <InputGroupText>https://</InputGroupText>
        </InputGroupAddon>
        <InputGroupInput placeholder="example.com" autoCapitalize="none" />
        <InputGroupAddon align="inline-end">
          <InputGroupText>.com</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
      <InputGroup>
        <InputGroupInput defaultValue="sk_live_51H8" />
        <InputGroupAddon align="inline-end">
          <InputGroupButton size="icon-xs" accessibilityLabel="Copy">
            <Icon as={CopyIcon} />
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
      <InputGroup>
        <InputGroupInput placeholder="Searching..." />
        <InputGroupAddon align="inline-end">
          <Spinner />
        </InputGroupAddon>
      </InputGroup>
      <InputGroup invalid>
        <InputGroupInput defaultValue="not-an-email" />
        <InputGroupAddon align="inline-end">
          <InputGroupText>Invalid</InputGroupText>
        </InputGroupAddon>
      </InputGroup>
      <InputGroup disabled>
        <InputGroupInput placeholder="Disabled" />
        <InputGroupAddon>
          <Icon as={MagnifyingGlassIcon} />
        </InputGroupAddon>
      </InputGroup>
    </View>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct InputGroupIcon: View {
  let image: Image

  var body: some View {
    image.frame(width: 16, height: 16)
  }
}

struct InputGroupSearchDemo: View {
  @State private var value = ""

  var body: some View {
    InputGroup {
      InputGroupInput("Search", text: $value)
      InputGroupAddon {
        InputGroupIcon(image: Ph.magnifyingGlass.regular)
      }
      if !value.isEmpty {
        InputGroupAddon(align: .inlineEnd) {
          InputGroupButton(size: .iconXs) {
            value = ""
          } label: {
            InputGroupIcon(image: Ph.x.regular)
          }
          .accessibilityLabel(Text("Clear"))
        }
      }
    }
  }
}

struct InputGroupDefaultDemo: View {
  @State private var domain = ""
  @State private var key = "sk_live_51H8"
  @State private var searching = ""
  @State private var email = "not-an-email"
  @State private var disabled = ""

  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      InputGroupSearchDemo()
      InputGroup {
        InputGroupAddon {
          InputGroupText("https://")
        }
        InputGroupInput("example.com", text: $domain)
          .textInputAutocapitalization(.never)
        InputGroupAddon(align: .inlineEnd) {
          InputGroupText(".com")
        }
      }
      InputGroup {
        InputGroupInput("", text: $key)
        InputGroupAddon(align: .inlineEnd) {
          InputGroupButton(size: .iconXs) {} label: {
            InputGroupIcon(image: Ph.copy.regular)
          }
          .accessibilityLabel(Text("Copy"))
        }
      }
      InputGroup {
        InputGroupInput("Searching...", text: $searching)
        InputGroupAddon(align: .inlineEnd) {
          Spinner()
        }
      }
      InputGroup {
        InputGroupInput("", text: $email)
        InputGroupAddon(align: .inlineEnd) {
          InputGroupText("Invalid")
        }
      }
      .invalid()
      InputGroup {
        InputGroupInput("Disabled", text: $disabled)
        InputGroupAddon {
          InputGroupIcon(image: Ph.magnifyingGlass.regular)
        }
      }
      .disabled(true)
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Copy
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.adamglin.phosphoricons.regular.X
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.InputGroup
import com.esuiss.ui.components.InputGroupAddon
import com.esuiss.ui.components.InputGroupAddonAlign
import com.esuiss.ui.components.InputGroupButton
import com.esuiss.ui.components.InputGroupButtonSize
import com.esuiss.ui.components.InputGroupInput
import com.esuiss.ui.components.InputGroupText
import com.esuiss.ui.components.Spinner

@Composable
fun InputGroupSearch() {
    var value by rememberSaveable { mutableStateOf("") }
    InputGroup {
        InputGroupInput(value, { value = it }, placeholder = "Search")
        InputGroupAddon {
            Icon(PhosphorIcons.Regular.MagnifyingGlass, contentDescription = null)
        }
        if (value.isNotEmpty()) {
            InputGroupAddon(align = InputGroupAddonAlign.InlineEnd) {
                InputGroupButton(onClick = { value = "" }, size = InputGroupButtonSize.IconXs) {
                    Icon(PhosphorIcons.Regular.X, contentDescription = "Clear")
                }
            }
        }
    }
}

@Composable
fun InputGroupDefault() {
    var domain by rememberSaveable { mutableStateOf("") }
    var key by rememberSaveable { mutableStateOf("sk_live_51H8") }
    var searching by rememberSaveable { mutableStateOf("") }
    var email by rememberSaveable { mutableStateOf("not-an-email") }
    var disabled by rememberSaveable { mutableStateOf("") }
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        InputGroupSearch()
        InputGroup {
            InputGroupAddon { InputGroupText("https://") }
            InputGroupInput(
                domain,
                { domain = it },
                placeholder = "example.com",
                keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None),
            )
            InputGroupAddon(align = InputGroupAddonAlign.InlineEnd) { InputGroupText(".com") }
        }
        InputGroup {
            InputGroupInput(key, { key = it })
            InputGroupAddon(align = InputGroupAddonAlign.InlineEnd) {
                InputGroupButton(onClick = {}, size = InputGroupButtonSize.IconXs) {
                    Icon(PhosphorIcons.Regular.Copy, contentDescription = "Copy")
                }
            }
        }
        InputGroup {
            InputGroupInput(searching, { searching = it }, placeholder = "Searching...")
            InputGroupAddon(align = InputGroupAddonAlign.InlineEnd) { Spinner() }
        }
        InputGroup(invalid = true) {
            InputGroupInput(email, { email = it })
            InputGroupAddon(align = InputGroupAddonAlign.InlineEnd) { InputGroupText("Invalid") }
        }
        InputGroup(enabled = false) {
            InputGroupInput(disabled, { disabled = it }, placeholder = "Disabled")
            InputGroupAddon {
                Icon(PhosphorIcons.Regular.MagnifyingGlass, contentDescription = null)
            }
        }
    }
}

InputGroup draws one bg-secondary field and places an InputGroupInput or InputGroupTextarea inside it with addons around. Addons can sit on the start or end of the same line (inline-start, inline-end), or above or below the input (block-start, block-end), which turns the group into a column.

The group shows the focus border while its input is focused, and reads the disabled and invalid state from a surrounding Field. Pressing an addon focuses the input.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/input-group

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add input-group
$ pnpm dlx @esuiss/ui@latest add input-group --platform swift
$ pnpm dlx @esuiss/ui@latest add input-group --platform kotlin

The CLI also adds what it builds on: Button, Icon, Input, Text, Textarea.

Manual

  1. Install the dependencies:
npx expo install class-variance-authority cn
  1. Add the pieces it builds on: Button, Icon, Input, Text, Textarea, and the primitives components/primitives/field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/input-group.tsx:

uniwind/components/ui/input-group.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import { Pressable, type TextInput, View, type ViewProps } from "react-native"
import { useFieldRootContext } from "@/components/primitives/field"
import { resolveClassName } from "@/components/primitives/use-render"
import { Button } from "@/components/ui/button"
import { IconClassContext } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { Text, TextClassContext } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"

type InputGroupAlign =
  | "inline-start"
  | "inline-end"
  | "block-start"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install class-variance-authority cn
  1. Add the pieces it builds on: Button, Icon, Input, Text, Textarea, and the primitives components/primitives/field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/input-group.tsx:

nativewind/components/ui/input-group.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import { Pressable, type TextInput, View, type ViewProps } from "react-native"
import { useFieldRootContext } from "@/components/primitives/field"
import { resolveClassName } from "@/components/primitives/use-render"
import { Button } from "@/components/ui/button"
import { IconClassContext } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { Text, TextClassContext } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"

type InputGroupAlign =
  | "inline-start"
  | "inline-end"
  | "block-start"
  1. Update the import paths to match your project.
  1. Add the pieces it builds on: Button, Icon, Input, Text, Textarea, and the primitives components/primitives/field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/input-group.tsx:

stylesheet/components/ui/input-group.tsx
import * as React from "react"
import {
  Pressable,
  StyleSheet,
  type TextInput,
  View,
  type ViewProps,
} from "react-native"
import { useFieldRootContext } from "@/components/primitives/field"
import { Button, type ButtonSize } from "@/components/ui/button"
import { IconSizeContext } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { Text, TextStyleContext } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"
import { fontSize, radius, useTheme } from "@/components/ui/theme"
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/InputGroup.swift
import SwiftUI

public enum InputGroupAddonAlign: CaseIterable, Sendable, Hashable {
  case inlineStart, inlineEnd, blockStart, blockEnd

  var isBlock: Bool {
    self == .blockStart || self == .blockEnd
  }
}

struct InputGroupContext: Sendable {
  var focusRequest = 0
  var addons: Set<InputGroupAddonAlign> = []
  var requestFocus: @MainActor @Sendable () -> Void = {}
}
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/Checkbox.swift
import PhosphorSwift
import SwiftUI

extension Shadow {
  static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
  static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}

struct SelectionIndicator<Mark: View>: View {
  let checked: Bool
  let pressed: Bool
  let cornerRadius: CGFloat
  let mark: Mark

  @Environment(\.isEnabled) private var isEnabled
  @Environment(\.isInvalid) private var invalid
swift/Components/Field.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var isInvalid = false
  @Entry var fieldInset = false
  @Entry var fieldGroupInset = false
  @Entry var fieldSetInset = false
}

extension View {
  public func invalid(_ isInvalid: Bool = true) -> some View {
    environment(\.isInvalid, isInvalid)
  }
}

public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/InputGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.runtime.Composable
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Field.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
kotlin/components/FormSupport.kt
package com.esuiss.ui.components

import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
kotlin/components/Label.kt
package com.esuiss.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
kotlin/components/Separator.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Separator(
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea } from "@/components/ui/input-group"
<View className="gap-4">
  <SearchDemo />
  <InputGroup>
    <InputGroupAddon>
      <InputGroupText>https://</InputGroupText>
    </InputGroupAddon>
    <InputGroupInput placeholder="example.com" autoCapitalize="none" />
    <InputGroupAddon align="inline-end">
      <InputGroupText>.com</InputGroupText>
    </InputGroupAddon>
  </InputGroup>
  <InputGroup>
    <InputGroupInput defaultValue="sk_live_51H8" />
    <InputGroupAddon align="inline-end">
      <InputGroupButton size="icon-xs" accessibilityLabel="Copy">
        <Icon as={CopyIcon} />
      </InputGroupButton>
    </InputGroupAddon>
  </InputGroup>
  <InputGroup>
    <InputGroupInput placeholder="Searching..." />
    <InputGroupAddon align="inline-end">
      <Spinner />
    </InputGroupAddon>
  </InputGroup>
  <InputGroup invalid>
    <InputGroupInput defaultValue="not-an-email" />
    <InputGroupAddon align="inline-end">
      <InputGroupText>Invalid</InputGroupText>
    </InputGroupAddon>
  </InputGroup>
  <InputGroup disabled>
    <InputGroupInput placeholder="Disabled" />
    <InputGroupAddon>
      <Icon as={MagnifyingGlassIcon} />
    </InputGroupAddon>
  </InputGroup>
</View>

Add children in any order; the group sorts start addons before the input and end addons after it. Put short labels such as a URL prefix or a unit in InputGroupText, and actions in InputGroupButton, a small rounded Button that defaults to the ghost variant and xs size.

Use a block-end addon with InputGroupTextarea for a footer row, for example a character count and a send button. Set invalid or disabled on the group, or let a Field provide them.

Examples

With textarea

A textarea with a footer row holding a usage label and a button.

iOS
Input Group (with textarea) on iOS in light modeInput Group (with textarea) on iOS in dark mode
import { ArrowUpIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea } from "@/components/ui/input-group"

export function InputGroupWithTextareaExample() {
  return (
    <InputGroup>
      <InputGroupTextarea
        placeholder="Ask, search or chat..."
        numberOfLines={3}
      />
      <InputGroupAddon align="block-end" className="justify-between">
        <InputGroupText>52% used</InputGroupText>
        <InputGroupButton
          variant="default"
          size="icon-xs"
          accessibilityLabel="Send"
        >
          <Icon as={ArrowUpIcon} />
        </InputGroupButton>
      </InputGroupAddon>
    </InputGroup>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct InputGroupIcon: View {
  let image: Image

  var body: some View {
    image.frame(width: 16, height: 16)
  }
}

struct InputGroupTextareaDemo: View {
  @State private var message = ""

  var body: some View {
    InputGroup {
      InputGroupTextarea("Ask, search or chat...", text: $message, lineLimit: 3...8)
      InputGroupAddon(align: .blockEnd) {
        InputGroupText("52% used")
        Spacer(minLength: 0)
        InputGroupButton(variant: .default, size: .iconXs) {} label: {
          InputGroupIcon(image: Ph.arrowUp.regular)
        }
        .accessibilityLabel(Text("Send"))
      }
    }
  }
}
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowUp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.InputGroup
import com.esuiss.ui.components.InputGroupAddon
import com.esuiss.ui.components.InputGroupAddonAlign
import com.esuiss.ui.components.InputGroupButton
import com.esuiss.ui.components.InputGroupButtonSize
import com.esuiss.ui.components.InputGroupText
import com.esuiss.ui.components.InputGroupTextarea

@Composable
fun InputGroupWithTextarea() {
    var value by rememberSaveable { mutableStateOf("") }
    InputGroup {
        InputGroupTextarea(value, { value = it }, placeholder = "Ask, search or chat...", minLines = 3)
        InputGroupAddon(Modifier.fillMaxWidth(), align = InputGroupAddonAlign.BlockEnd) {
            InputGroupText("52% used")
            Spacer(Modifier.weight(1f))
            InputGroupButton(onClick = {}, size = InputGroupButtonSize.IconXs, variant = ButtonVariant.Default) {
                Icon(PhosphorIcons.Regular.ArrowUp, contentDescription = "Send")
            }
        }
    }
}

Block start

A label addon above the input with block-start.

iOS
Input Group (block start) on iOS in light modeInput Group (block start) on iOS in dark mode
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"
import { Text } from "@/components/ui/text"

export function InputGroupBlockStartExample() {
  return (
    <InputGroup>
      <InputGroupInput placeholder="Enter your name" />
      <InputGroupAddon align="block-start">
        <Text className="text-sm font-medium text-foreground">Full name</Text>
      </InputGroupAddon>
    </InputGroup>
  )
}
import EsuissUI
import SwiftUI

struct InputGroupBlockStartDemo: View {
  @State private var name = ""

  var body: some View {
    InputGroup {
      InputGroupInput("Enter your name", text: $name)
      InputGroupAddon(align: .blockStart) {
        Text("Full name")
          .textStyle(.sm, weight: .medium)
          .foregroundStyle(EsuissColor.foreground)
      }
    }
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.font.FontWeight
import com.esuiss.ui.components.InputGroup
import com.esuiss.ui.components.InputGroupAddon
import com.esuiss.ui.components.InputGroupAddonAlign
import com.esuiss.ui.components.InputGroupInput
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun InputGroupBlockStart() {
    var value by rememberSaveable { mutableStateOf("") }
    InputGroup {
        InputGroupInput(value, { value = it }, placeholder = "Enter your name")
        InputGroupAddon(align = InputGroupAddonAlign.BlockStart) {
            Text("Full name", style = Typography.sm.copy(fontWeight = FontWeight.Medium, color = EsuissTheme.colors.foreground))
        }
    }
}

API Reference

01

InputGroup

Field container that lays out the input and addons.

PropTypeDefaultDescription
disabledbooleanfalseDisables the group and its input.
invalidboolean-Shows the destructive border and tint.

Also accepts the props of ViewProps.

02

InputGroupAddon

Slot for text, icons or buttons beside or around the input.

PropTypeDefaultDescription
align"inline-start" | "inline-end" | "block-start" | "block-end""inline-start"inline-start, inline-end, block-start or block-end. Defaults to inline-start.

Also accepts the props of React.ComponentProps<typeof Pressable>.

03

InputGroupButton

Compact button inside an addon.

PropTypeDefaultDescription
sizekeyof typeof BUTTON_SIZE"xs"xs, sm, icon-xs or icon-sm. Defaults to xs.

Also accepts the props of React.ComponentProps<typeof Button>.

04

InputGroupText

Text inside an addon.

Also accepts the props of React.ComponentProps<typeof Text>.

05

InputGroupInput

Single-line input without its own border.

Also accepts the props of React.ComponentProps<typeof Input>.

06

InputGroupTextarea

Multi-line input without its own border.

Also accepts the props of React.ComponentProps<typeof Textarea>.

Accessibility

  • The container has the group role.
  • Addons are not separate accessibility elements; their text is not announced with the input, so give the input an accessibilityLabel that includes needed context.
  • Icon-only buttons need an accessibilityLabel.

Notes

  • Uses bg-secondary, border-ring, border-destructive tokens, with opacity-50 when disabled; the invalid tint is stronger in dark mode.
  • In Uniwind and NativeWind, a focused field also draws the ring-3 ring-ring/50 focus ring.
  • Light and dark follow the system appearance.
NextInput OTPPreviousInput Accessory Bar

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Search
    • With text
    • With buttons
    • Button sizes
    • With kbd
    • Block addons
    • With textarea
    • Invalid
    • Disabled
  4. 04API Reference
    • InputGroup
    • InputGroupAddon
    • InputGroupButton
    • InputGroupText
    • InputGroupInput
    • InputGroupTextarea
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • With textarea
    • Block start
  10. 10API Reference
    • InputGroup
    • InputGroupAddon
    • InputGroupButton
    • InputGroupText
    • InputGroupInput
    • InputGroupTextarea
  11. 11Accessibility
  12. 12Notes