---
name: input-group
title: Input Group
url: https://ui.frontic.com/components/ui/input-group.md
tier: ui
install: npx @frontic/ui add input-group
files:
  - components/ui/input-group/InputGroup.vue
  - components/ui/input-group/InputGroupAddon.vue
  - components/ui/input-group/InputGroupButton.vue
  - components/ui/input-group/InputGroupInput.vue
  - components/ui/input-group/InputGroupText.vue
  - components/ui/input-group/InputGroupTextarea.vue
  - components/ui/input-group/index.ts
  - components/ui/input-group/keys.ts
registryDependencies:
  - button
  - input
  - textarea
dependencies:
  - class-variance-authority
  - reka-ui
---

# Input Group

An input or textarea with addons beside, above or below it, such as a search icon, a unit or a button that submits.

## Install

```sh
npx @frontic/ui add input-group
```

Built from: [Button](https://ui.frontic.com/components/ui/button.md), [Input](https://ui.frontic.com/components/ui/input.md), [Textarea](https://ui.frontic.com/components/ui/textarea.md)

Used by: [Autocomplete](https://ui.frontic.com/components/ui/autocomplete.md), [Filter](https://ui.frontic.com/components/ui/filter.md), [Payment Card](https://ui.frontic.com/components/ui/payment-card.md), [Promo Code](https://ui.frontic.com/components/sections/promo-code.md), [Checkout Page](https://ui.frontic.com/components/templates/checkout-page.md), [Filter Sheet](https://ui.frontic.com/components/templates/filter-sheet.md), [Filter Sidebar](https://ui.frontic.com/components/templates/filter-sidebar.md)

npm packages: `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { SearchIcon, XIcon } from '@lucide/vue'

import { ref, useTemplateRef } from 'vue'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group'

const query = ref('hardshell')
const input = useTemplateRef<{ $el: HTMLInputElement }>('input')

function clear() {
  query.value = ''
  input.value?.$el.focus()
}
</script>

<template>
  <InputGroup class="max-w-sm">
    <InputGroupInput ref="input" v-model="query" placeholder="Search products" aria-label="Search products" />
    <InputGroupAddon>
      <SearchIcon data-icon />
    </InputGroupAddon>
    <InputGroupAddon v-if="query" align="inline-end">
      <InputGroupButton size="icon-xs" aria-label="Clear search" @click="clear">
        <XIcon data-icon />
      </InputGroupButton>
    </InputGroupAddon>
  </InputGroup>
</template>
```

## Anatomy

```vue
<InputGroup>
  <InputGroupInput />
  <InputGroupAddon>
    <InputGroupButton />
  </InputGroupAddon>
</InputGroup>
```

## API reference

### InputGroup

Groups an input with addons such as icons, buttons or text. Renders a `<div>`.

`data-slot="input-group"` · `InputGroup.vue`

Styling: `input-group`, which extends [`control`](https://ui.frontic.com/DESIGN.md#control). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `InputGroupSize`: `'md' \| 'sm'` |  | The height of the group; the input inside it follows. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-size` | `'md' \| 'sm'` | Carries the `size` prop. |

### InputGroupAddon

Places an icon, text or button beside the input of its group. Renders a `<div>`.

`data-slot="input-group-addon"` · `InputGroupAddon.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `align` | `InputGroupAddonAlign`: `'inline-start' \| 'inline-end' \| 'block-start' \| 'block-end'` | `'inline-start'` | Where the addon sits: before or after the input on its line, or above or below it. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-align` | `'inline-start' \| 'inline-end' \| 'block-start' \| 'block-end'` | Carries the `align` prop. |

### InputGroupButton

Lets the user act on the input of its group, such as submit it.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'solid' \| 'outline' \| 'soft' \| 'ghost' \| 'link'` | `'ghost'` | The button style: solid, outline, soft, ghost or link. |
| `color` | `'neutral' \| 'primary' \| 'destructive' \| 'buy' \| 'checkout' \| 'inverse'` | `'neutral'` | The button colour role, e.g. neutral or brand. |
| `size` | `'xs' \| 'sm' \| 'icon-xs' \| 'icon-sm'` | `'xs'` | The button's size inside the field; the `icon-*` sizes make a square, icon-only button. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-color` | `'neutral' \| 'primary' \| 'destructive' \| 'buy' \| 'checkout' \| 'inverse'` | Carries the `color` prop. |
| `data-size` | `'xs' \| 'sm' \| 'icon-xs' \| 'icon-sm'` | Carries the `size` prop. |
| `data-variant` | `'solid' \| 'outline' \| 'soft' \| 'ghost' \| 'link'` | Carries the `variant` prop. |

### InputGroupInput

Lets the user enter a single line of text inside the group.

`data-slot="input-group-control"` · `InputGroupInput.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### InputGroupText

Shows fixed text, such as a unit or prefix, inside the group. Renders a `<span>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### InputGroupTextarea

Lets the user enter several lines of text inside the group.

`data-slot="input-group-control"` · `InputGroupTextarea.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

## Exports

```ts
import { type InputGroupSize, type InputGroupAddonAlign, type InputGroupButtonProps } from '@/components/ui/input-group'
```

- `InputGroupSize`: `'md' \| 'sm'`
- `InputGroupAddonAlign`: `'inline-start' \| 'inline-end' \| 'block-start' \| 'block-end'`
- `InputGroupButtonProps`: below

```ts
interface InputGroupButtonProps {
  /** The button style: solid, outline, soft, ghost or link. */
  variant?: InstanceType<typeof Button>['$props']['variant']
  /** The button colour role, e.g. neutral or brand. */
  color?: InstanceType<typeof Button>['$props']['color']
  /** The button's size inside the field; the `icon-*` sizes make a square, icon-only button. */
  size?: 'xs' | 'sm' | 'icon-xs' | 'icon-sm'
  class?: HTMLAttributes['class']
}
```

## Styling

| Part | Recipe |
| --- | --- |
| `textarea` | [`input-group-textarea`](https://ui.frontic.com/DESIGN.md#input-group-textarea) |
| `input` | [`input-group-input`](https://ui.frontic.com/DESIGN.md#input-group-input) |
| `default` | [`input-group`](https://ui.frontic.com/DESIGN.md#input-group) |
| `addon` | [`input-group-addon`](https://ui.frontic.com/DESIGN.md#input-group-addon) |
| `text` | [`input-group-text`](https://ui.frontic.com/DESIGN.md#input-group-text) |
| `button` | [`input-group-button`](https://ui.frontic.com/DESIGN.md#input-group-button) |
| `disabled` | [`disabled`](https://ui.frontic.com/DESIGN.md#disabled) |

States with a look of their own: focus and invalid.

## Source

`components/ui/input-group/InputGroup.vue`

```vue
<script lang="ts">
/** Groups an input with addons such as icons, buttons or text. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { InputGroupSize } from '.'
import { cva } from 'class-variance-authority'
import { provide, toRef } from 'vue'
import { fronticInputGroupSize } from './keys'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
      /** The height of the group; the input inside it follows. */
      size?: InputGroupSize
    }
  >(),
  { as: 'div' },
)

const group = cva(
  'fui-input-group group/input-group relative outline-none transition-colors 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',
  {
    variants: {
      size: { md: 'fui-input-group-md', sm: 'fui-input-group-sm' },
    },
    defaultVariants: { size: 'md' },
  },
)

provide(fronticInputGroupSize, toRef(props, 'size'))
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="input-group" role="group" :data-size="props.size ?? 'md'" :class="[group({ size: props.size }), props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/input-group/InputGroupAddon.vue`

```vue
<script lang="ts">
/** Places an icon, text or button beside the input of its group. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { InputGroupAddonAlign } from '.'
import { cva } from 'class-variance-authority'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      /** Where the addon sits: before or after the input on its line, or above or below it. */
      align?: InputGroupAddonAlign
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'div', align: 'inline-start' },
)

const addon = cva('fui-input-group-addon cursor-text select-none [&_[data-slot=kbd]]:rounded-none [&_[data-slot=kbd]]:px-1.5 group-data-disabled/input-group:fui-disabled', {
  variants: {
    align: {
      'inline-start': 'fui-input-group-addon-inline-start',
      'inline-end': 'fui-input-group-addon-inline-end',
      'block-start': 'fui-input-group-addon-block-start',
      'block-end': 'fui-input-group-addon-block-end',
    },
  },
  defaultVariants: { align: 'inline-start' },
})
const ALIGN_LAYOUT: Record<InputGroupAddonAlign, string> = {
  'inline-start': 'order-first has-[>button]:-ms-1 has-[>kbd]:-ms-1',
  'inline-end': 'order-last has-[>button]:-me-1 has-[>kbd]:-me-1',
  'block-start': 'order-first w-full justify-start group-has-[>input]/input-group:pt-3.5',
  'block-end': 'order-last w-full justify-start group-has-[>input]/input-group:pb-3.5',
}

function handleInputGroupAddonClick(e: MouseEvent) {
  const currentTarget = e.currentTarget as HTMLElement | null
  const target = e.target as HTMLElement | null
  if (target && target.closest('button')) {
    return
  }
  if (currentTarget && currentTarget?.parentElement) {
    currentTarget.parentElement?.querySelector('input')?.focus()
  }
}
</script>

<template>
  <Primitive
    :as="props.as"
    :as-child="props.asChild"
    role="group"
    data-slot="input-group-addon"
    :data-align="props.align"
    :class="[addon({ align: props.align }), ALIGN_LAYOUT[props.align], props.class]"
    @click="handleInputGroupAddonClick"
  >
    <slot />
  </Primitive>
</template>
```

`components/ui/input-group/InputGroupButton.vue`

```vue
<script lang="ts">
/** Lets the user act on the input of its group, such as submit it. */
export default {}
</script>

<script setup lang="ts">
import type { InputGroupButtonProps } from '.'
import { cva } from 'class-variance-authority'
import { Button } from '@library/components/ui/button'

const props = withDefaults(defineProps<InputGroupButtonProps>(), {
  size: 'xs',
  variant: 'ghost',
  color: 'neutral',
})

const button = cva('fui-input-group-button', {
  variants: {
    size: {
      xs: 'fui-input-group-button-xs',
      sm: 'fui-input-group-button-sm',
      'icon-xs': 'fui-input-group-button-icon-xs',
      'icon-sm': 'fui-input-group-button-icon-sm',
    },
  },
  defaultVariants: { size: 'xs' },
})
</script>

<template>
  <Button type="button" :size="null" :data-size="props.size" :variant="props.variant" :color="props.color" :class="[button({ size: props.size }), props.class]">
    <slot />
  </Button>
</template>
```

`components/ui/input-group/InputGroupInput.vue`

```vue
<script lang="ts">
/** Lets the user enter a single line of text inside the group. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { Input } from '@library/components/ui/input'
import { fronticInputGroupSize } from './keys'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()

const size = inject(fronticInputGroupSize, undefined)
</script>

<template>
  <Input
    data-slot="input-group-control"
    :size="size"
    :class="[
      'fui-input-group-input border-0 focus-visible:border-b-0 bg-transparent group-has-[>[data-align=inline-start]]/input-group:ps-2 group-has-[>[data-align=inline-end]]/input-group:pe-2',
      props.class,
    ]"
  />
</template>
```

`components/ui/input-group/InputGroupText.vue`

```vue
<script lang="ts">
/** Shows fixed text, such as a unit or prefix, inside the group. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'span' },
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" :class="['fui-input-group-text **:data-icon:pointer-events-none', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/input-group/InputGroupTextarea.vue`

```vue
<script lang="ts">
/** Lets the user enter several lines of text inside the group. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Textarea } from '@library/components/ui/textarea'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()
</script>

<template>
  <Textarea data-slot="input-group-control" :class="['fui-input-group-textarea resize-none border-0 focus-visible:border-b-0 bg-transparent', props.class]" />
</template>
```

`components/ui/input-group/index.ts`

```ts
import type { HTMLAttributes } from 'vue'
import type { Button } from '@library/components/ui/button'

export { default as InputGroup } from './InputGroup.vue'
export { default as InputGroupAddon } from './InputGroupAddon.vue'
export { default as InputGroupButton } from './InputGroupButton.vue'
export { default as InputGroupInput } from './InputGroupInput.vue'
export { default as InputGroupText } from './InputGroupText.vue'
export { default as InputGroupTextarea } from './InputGroupTextarea.vue'

/** The height of the group and its input: md is 40px, sm is 36px. */
export type InputGroupSize = 'md' | 'sm'

export type InputGroupAddonAlign = 'inline-start' | 'inline-end' | 'block-start' | 'block-end'

export interface InputGroupButtonProps {
  /** The button style: solid, outline, soft, ghost or link. */
  variant?: InstanceType<typeof Button>['$props']['variant']
  /** The button colour role, e.g. neutral or brand. */
  color?: InstanceType<typeof Button>['$props']['color']
  /** The button's size inside the field; the `icon-*` sizes make a square, icon-only button. */
  size?: 'xs' | 'sm' | 'icon-xs' | 'icon-sm'
  class?: HTMLAttributes['class']
}
```

`components/ui/input-group/keys.ts`

```ts
import type { InjectionKey, Ref } from 'vue'
import type { InputGroupSize } from '.'

/** The group's height, which the input inside it takes too. */
export const fronticInputGroupSize: InjectionKey<Ref<InputGroupSize | undefined>> = Symbol('fronticInputGroupSize')
```
