Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/input-group.jsonInstalls to @/components/ui/input-group.
Manual installation
Install the dependencies:
pnpm add class-variance-authority@^0.7.1Add the components it builds on: button, input, textarea.
Copy the source into your project:
'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-10 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none hover:border-primary/90 has-[[data-slot=input-group-control]:read-only]:border-dashed has-[[data-slot=input-group-control]:read-only]:bg-muted/50 has-[[data-slot=input-group-control]:read-only]:hover:border-input in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:disabled]:bg-input/50 has-[[data-slot=input-group-control]:disabled]:opacity-50 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 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 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 dark:bg-input/30 dark:has-[[data-slot=input-group-control]:disabled]:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
className,
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
'inline-start': 'order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]',
'inline-end': 'order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]',
'block-start': 'order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2',
'block-end': 'order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2',
},
},
defaultVariants: {
align: 'inline-start',
},
},
)
function InputGroupAddon({
className,
align = 'inline-start',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return
}
e.currentTarget.parentElement?.querySelector('input')?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: '',
'icon-xs': 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
},
},
defaultVariants: {
size: 'xs',
},
})
function InputGroupButton({
className,
type = 'button',
variant = 'ghost',
size = 'xs',
...props
}: Omit<React.ComponentProps<typeof Button>, 'size' | 'type'> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: 'button' | 'submit' | 'reset'
}) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
return (
<Input
data-slot="input-group-control"
className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 read-only:bg-transparent disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className,
)}
{...props}
/>
)
}
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 read-only:bg-transparent disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className,
)}
{...props}
/>
)
}
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea }An input or textarea with addons that share a single border and focus ring. Addons hold icons, text or small buttons and can sit inline at either end of the control or as full-width blocks above and below it.
Features
- One border and focus ring shared by the control and its addons; hover, focus,
disabledandaria-invalidstates apply to the whole group - Addons at either end of the control (
inline-start,inline-end) or as full-width blocks above and below it (block-start,block-end) - Addons hold icons, text, buttons, a
Kbdshortcut or aSpinner - Works with a single-line input (
InputGroupInput) or a textarea (InputGroupTextarea) - Clicking an addon focuses the control, unless the click landed on a button
- Small button sizes made for addons (
xs,sm,icon-xs,icon-sm)
Usage
import { useState } from 'react'
import { Search, X } from 'lucide-react'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from '@/components/ui/input-group'
export function InputGroupDemo() {
const [value, setValue] = useState('')
return (
<InputGroup className="max-w-sm">
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupInput
placeholder="Search cases..."
value={value}
onChange={(event) => setValue(event.target.value)}
/>
{value && (
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" aria-label="Clear" onClick={() => setValue('')}>
<X />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
)
}Examples
Default
A search field for the case register with a result count at the end.
With text
InputGroupText for currencies and units.
With button
A clear button that only appears while the field has a value.
With textarea
A block-end addon below a textarea holds an attach button, a character count and a send button.
Block addon
A block-start addon above a read-only link, with a copy button.
Button sizes
With keyboard shortcut
Loading
A Spinner in an inline-end addon while a value is being checked.
Disabled
Invalid
aria-invalid on the control turns the whole group's border and ring destructive.
Anatomy
import {
InputGroup,
InputGroupInput,
InputGroupTextarea,
InputGroupAddon,
InputGroupButton,
InputGroupText,
} from "@/components/ui/input-group"
<InputGroup>
<InputGroupAddon> {/* align="inline-start" (default) */}
<Search />
</InputGroupAddon>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs"><X /></InputGroupButton>
</InputGroupAddon>
</InputGroup>InputGroupInput and InputGroupTextarea render the regular Input and Textarea components with their own border
and ring removed.
Props
InputGroup— the wrapper (divwithrole="group").InputGroupInput/InputGroupTextarea— the control. Accept all nativeinput/textareaprops.InputGroupAddon— holds icons,InputGroupText,InputGroupButtonorKbd.align:inline-start(default),inline-end,block-start,block-end.InputGroupButton— aButton(defaults tovariant="ghost",type="button").size:xs(default),sm,icon-xs,icon-sm. Accepts all otherButtonvariants.InputGroupText— muted inline text or icon + text.
Related Components
- Input
- Textarea
- Button Group
- Kbd