Input Group
An input field with attached addons like icons, buttons, or text labels.
52% used
---import { ArrowUpIcon, PlusIcon, SearchIcon } from '@lucide/astro';
import InputGroup from '@bejamas/ui/components/InputGroup.astro';import Separator from '@bejamas/ui/components/Separator.astro';---
<div className="grid w-full max-w-sm gap-6"> <InputGroup> <InputGroup part="input" placeholder="Search..." /> <InputGroup part="addon"> <SearchIcon /> </InputGroup> <InputGroup part="addon" align="inline-end">12 results</InputGroup> </InputGroup>
<InputGroup> <InputGroup part="input" placeholder="example.com" class="!pl-1" /> <InputGroup part="addon"> <InputGroup part="text">https://</InputGroup> </InputGroup> <InputGroup part="addon" align="inline-end"> <InputGroup part="button" size="xs"> Check </InputGroup> </InputGroup> </InputGroup>
<InputGroup> <InputGroup part="textarea" placeholder="Ask, Search or Chat..." /> <InputGroup part="addon" align="block-end"> <InputGroup part="button" size="icon-xs" class="rounded-full"> <PlusIcon /> </InputGroup> <InputGroup part="button" variant="ghost">Auto</InputGroup> <InputGroup part="text" class="ml-auto">52% used</InputGroup> <Separator orientation="vertical" class="!h-4" /> <InputGroup part="button" size="icon-xs" variant="default" class="rounded-full"> <ArrowUpIcon /> <span class="sr-only">Send</span> </InputGroup> </InputGroup> </InputGroup></div>Installation
Section titled “Installation” bunx bejamas add input-group npx bejamas add input-group pnpm dlx bejamas add input-group yarn dlx bejamas add input-group---/** * @component InputGroup * @title Input Group * @description An input field with attached addons like icons, buttons, or text labels. * * @preview * * <div className="grid w-full max-w-sm gap-6"> * <InputGroup> * <InputGroup part="input" placeholder="Search..." /> * <InputGroup part="addon"> * <SearchIcon /> * </InputGroup> * <InputGroup part="addon" align="inline-end">12 results</InputGroup> * </InputGroup> * * <InputGroup> * <InputGroup part="input" placeholder="example.com" class="!pl-1" /> * <InputGroup part="addon"> * <InputGroup part="text">https://</InputGroup> * </InputGroup> * <InputGroup part="addon" align="inline-end"> * <InputGroup part="button" size="xs"> * Check * </InputGroup> * </InputGroup> * </InputGroup> * * <InputGroup> * <InputGroup part="textarea" placeholder="Ask, Search or Chat..." /> * <InputGroup part="addon" align="block-end"> * <InputGroup part="button" size="icon-xs" class="rounded-full"> * <PlusIcon /> * </InputGroup> * <InputGroup part="button" variant="ghost">Auto</InputGroup> * <InputGroup part="text" class="ml-auto">52% used</InputGroup> * <Separator orientation="vertical" class="!h-4" /> * <InputGroup part="button" size="icon-xs" variant="default" class="rounded-full"> * <ArrowUpIcon /> * <span class="sr-only">Send</span> * </InputGroup> * </InputGroup> * </InputGroup> * </div> */import type { HTMLAttributes, HTMLTag } from "astro/types";import { cva } from "class-variance-authority";import { cn } from "@bejamas/ui/lib/utils";import Button from "@bejamas/ui/components/Button.astro";import Input from "@bejamas/ui/components/Input.astro";import Textarea from "@bejamas/ui/components/Textarea.astro";
type InputGroupPart = | "root" | "addon" | "button" | "text" | "input" | "textarea";
type Align = "inline-start" | "inline-end" | "block-start" | "block-end";type GroupButtonSize = "xs" | "sm" | "icon-xs" | "icon-sm";
type RootProps = { part?: "root"; as?: HTMLTag; class?: string;} & HTMLAttributes<"div">;
type AddonProps = { part: "addon"; as?: HTMLTag; align?: Align; class?: string;} & HTMLAttributes<"div">;
type ButtonProps = { part: "button"; as?: HTMLTag; size?: GroupButtonSize; variant?: Parameters<typeof Button>[0]["variant"]; type?: "button" | "submit" | "reset"; class?: string;} & HTMLAttributes<"button">;
type TextProps = { part: "text"; as?: HTMLTag; class?: string;} & HTMLAttributes<"span">;
type InputProps = { part: "input"; as?: HTMLTag; class?: string;} & HTMLAttributes<"input">;
type TextareaProps = { part: "textarea"; as?: HTMLTag; class?: string;} & HTMLAttributes<"textarea">;
type Props = | RootProps | AddonProps | ButtonProps | TextProps | InputProps | TextareaProps;
const { part: rawPart, as: rawTag, class: className = "", align = "inline-start", size = "xs", variant = "ghost", type = "button", ...rest} = Astro.props as any as Props & { align?: Align; size?: GroupButtonSize;};
const part: InputGroupPart = (rawPart ?? "root") as InputGroupPart;
const rootClasses = "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none h-9 min-w-0 has-[>textarea]:h-auto 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 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40";
const addonVariants = cva( "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50", { 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 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5", "block-end": "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5", }, }, defaultVariants: { align: "inline-start", }, },);
const buttonSizeVariants = cva("text-sm shadow-none flex gap-2 items-center", { variants: { size: { xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2", sm: "h-8 px-2.5 gap-1.5 rounded-md 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", },});---
{ part === "root" && (() => { const Tag = (rawTag as HTMLTag) ?? "div"; return ( <Tag {...rest} role="group" data-slot="input-group" class={cn(rootClasses, className)} > <slot /> </Tag> ); })()}
{ part === "addon" && (() => { const Tag = (rawTag as HTMLTag) ?? "div"; return ( <Tag {...rest} role="group" data-slot="input-group-addon" data-align={align as Align} class={cn(addonVariants({ align: align as Align }), className)} > <slot /> </Tag> ); })()}
{ part === "button" && (() => { const Tag = (rawTag as HTMLTag) ?? undefined; if (Tag) { return ( <Tag {...rest} data-slot="input-group-button" class={cn( buttonSizeVariants({ size: size as GroupButtonSize }), className, )} > <slot /> </Tag> ); } return ( <Button type={type as any} variant={(variant as any) ?? "ghost"} data-size={size as GroupButtonSize} data-slot="input-group-button" class={cn( buttonSizeVariants({ size: size as GroupButtonSize }), className, )} {...(rest as any)} > <slot /> </Button> ); })()}
{ part === "text" && (() => { const Tag = (rawTag as HTMLTag) ?? "span"; return ( <Tag {...rest} data-slot="input-group-text" class={cn( "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", className, )} > <slot /> </Tag> ); })()}
{ part === "input" && (() => { const Tag = (rawTag as HTMLTag) ?? undefined; if (Tag) { return ( <Tag {...rest} data-slot="input-group-control" class={cn( "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", className, )} /> ); } return ( <Input data-slot="input-group-control" class={cn( "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", className, )} {...(rest as any)} /> ); })()}
{ part === "textarea" && (() => { const Tag = (rawTag as HTMLTag) ?? undefined; if (Tag) { return ( <Tag {...rest} data-slot="input-group-control" class={cn( "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", className, )} /> ); } return ( <Textarea data-slot="input-group-control" class={cn( "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", className, )} {...(rest as any)} /> ); })()}
<script type="module"> // Focus the nearest input when clicking on addons (unless clicking a button) document.addEventListener("click", (e) => { const addon = e.target.closest("[data-slot='input-group-addon']"); if (!addon) return; if (e.target.closest("button")) return; const root = addon.closest("[data-slot='input-group']"); if (!root) return; const input = root.querySelector("[data-slot='input-group-control']"); input?.focus(); }); // Disabled styling passthrough when any control is disabled document.querySelectorAll("[data-slot='input-group']").forEach((group) => { const updateDisabled = () => { const ctrl = group.querySelector("[data-slot='input-group-control']"); group.dataset.disabled = ctrl?.hasAttribute("disabled") ? "true" : "false"; }; updateDisabled(); group.addEventListener("change", updateDisabled, { capture: true }); }); // Expose for CSS ':has()' emulation via dataset if needed by consumers</script>