52% used
import { IconArrowUp, IconCheck, IconInfoCircle, IconPlus, IconSearch } from "~/components/icons"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "~/components/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea
} from "~/components/ui/input-group"
import { Separator } from "~/components/ui/separator"
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"
export function InputGroupDemo() {
return (
<div class="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<IconSearch />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput class="!pl-1" placeholder="example.com" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger as={InputGroupButton} class="rounded-full" size="icon-xs">
<IconInfoCircle />
</TooltipTrigger>
<TooltipContent>This is content in a tooltip.</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
<InputGroupAddon align="block-end">
<InputGroupButton class="rounded-full" size="icon-xs" variant="outline">
<IconPlus />
</InputGroupButton>
<DropdownMenu placement="top-start">
<DropdownMenuTrigger as={InputGroupButton} variant="ghost">
Auto
</DropdownMenuTrigger>
<DropdownMenuContent class="[--radius:0.95rem]">
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText class="ml-auto">52% used</InputGroupText>
<Separator class="!h-4" orientation="vertical" />
<InputGroupButton class="rounded-full" disabled size="icon-xs" variant="default">
<IconArrowUp />
<span class="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="@shadcn" />
<InputGroupAddon align="inline-end">
<div class="flex size-4 items-center justify-center rounded-full bg-primary text-primary-foreground">
<IconCheck class="size-3" />
</div>
</InputGroupAddon>
</InputGroup>
</div>
)
}
Installation
CLI
Manual
Install the following dependencies:
Copy and paste the following code into your project.
import { type Component, type ComponentProps, mergeProps, splitProps } from "solid-js"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"import { Button } from "~/components/ui/button"import { Input } from "~/components/ui/input"import { Textarea } from "~/components/ui/textarea"
const InputGroup: Component<ComponentProps<"div">> = (props) => { const [local, others] = splitProps(props, ["class"]) return ( <div class={cn( "group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30", "h-9 min-w-0 has-[>textarea]:h-auto", // Variants based on alignment. "has-[>[data-align=inline-start]]:[&>input]:pl-2", "has-[>[data-align=inline-end]]:[&>input]:pr-2", "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3", "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", // Focus state. "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50", // Error state. "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", local.class )} data-slot="input-group" role="group" {...others} /> )}
const inputGroupAddonVariants = cva( "flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 font-medium text-muted-foreground text-sm 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-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", "inline-end": "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", "block-start": "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3", "block-end": "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3" } }, defaultVariants: { align: "inline-start" } })
type InputGroupAddonProps = ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>
const InputGroupAddon: Component<InputGroupAddonProps> = (rawProps) => { const props = mergeProps({ align: "inline-start" } as const, rawProps) const [local, others] = splitProps(props, ["class", "align"]) return ( <div class={cn(inputGroupAddonVariants({ align: local.align }), local.class)} data-align={local.align} data-slot="input-group-addon" onClick={(e) => { if ((e.target as HTMLElement).closest("button")) { return } e.currentTarget.parentElement?.querySelector("input")?.focus() }} role="group" {...others} /> )}
const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", { variants: { size: { xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5", "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", "icon-sm": "size-8 p-0 has-[>svg]:p-0" } }, defaultVariants: { size: "xs" }})
type InputGroupButtonProps = Omit<ComponentProps<typeof Button>, "size"> & VariantProps<typeof inputGroupButtonVariants>
const InputGroupButton: Component<InputGroupButtonProps> = (rawProps) => { const props = mergeProps({ type: "button", variant: "ghost", size: "xs" } as const, rawProps) const [local, others] = splitProps(props, ["class", "type", "variant", "size"]) return ( <Button class={cn(inputGroupButtonVariants({ size: local.size }), local.class)} data-size={local.size} type={local.type} variant={local.variant} {...others} /> )}
const InputGroupText: Component<ComponentProps<"span">> = (props) => { const [local, others] = splitProps(props, ["class"]) return ( <span class={cn( "flex items-center gap-2 text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none", local.class )} {...others} /> )}
const InputGroupInput: Component<ComponentProps<"input">> = (props) => { const [local, others] = splitProps(props, ["class"]) return ( <Input class={cn( "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", local.class )} data-slot="input-group-control" {...others} /> )}
const InputGroupTextarea: Component<ComponentProps<"textarea">> = (props) => { const [local, others] = splitProps(props, ["class"]) return ( <Textarea class={cn( "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", local.class )} data-slot="input-group-control" {...others} /> )}
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea}Usage
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea,} from "~/components/ui/input-group";<InputGroup> <InputGroupInput placeholder="Search..." /> <InputGroupAddon> <SearchIcon /> </InputGroupAddon> <InputGroupAddon align="inline-end"> <InputGroupButton>Search</InputGroupButton> </InputGroupAddon></InputGroup>