Forms

Input Group.

An input or textarea with icons, text prefixes like hub://, and inline buttons attached inside one border.

128 sites
hub://

Installation#

With the CLI

$npx lanterncn add input-group

This also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/input-group. See Installation if your project is not set up yet.

Manually

  1. Install the dependencies

    npm install class-variance-authority
  2. Copy the source into your project

    components/ui/input-group.tsx
    "use client";
    
    import * as React from "react";
    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";
    
    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-md border border-input bg-background/40 transition-[border-color,box-shadow] outline-none",
            "hover:border-muted-foreground/60 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-primary has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/25",
            "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20",
            "has-[[data-slot=input-group-control]:disabled]:opacity-50",
            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 [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-sm",
      {
        variants: {
          align: {
            "inline-start": "order-first pl-3 has-[>button]:-ml-1.5 has-[>kbd]:-ml-1",
            "inline-end": "order-last pr-3 has-[>button]:-mr-2 has-[>kbd]:-mr-1",
            "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" },
      },
    );
    
    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<HTMLElement>("input, textarea")?.focus();
          }}
          {...props}
        />
      );
    }
    
    const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm active:translate-y-0", {
      variants: {
        size: {
          xs: "h-6 gap-1 rounded-sm px-2 text-xs has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
          sm: "h-8 gap-1.5 rounded-sm px-2.5 text-xs has-[>svg]:px-2.5",
          "icon-xs": "size-6 rounded-sm p-0 has-[>svg]:p-0",
          "icon-sm": "size-8 rounded-sm 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"> & VariantProps<typeof inputGroupButtonVariants>) {
      return (
        <Button
          type={type}
          data-size={size}
          variant={variant}
          className={cn(inputGroupButtonVariants({ size }), "focus-visible:ring-offset-0", className)}
          {...props}
        />
      );
    }
    
    function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
      return (
        <span
          className={cn(
            "flex items-center gap-2 font-mono text-[13px] 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(
            "h-[38px] flex-1 rounded-none border-0 bg-transparent shadow-none hover:border-0 focus-visible:ring-0",
            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-3 shadow-none focus-visible:ring-0",
            className,
          )}
          {...props}
        />
      );
    }
    
    export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea };
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils and uses button, input, textarea.

Usage#

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupText,
  InputGroupInput,
  InputGroupTextarea,
} from "@/components/ui/input-group";
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>hub://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="my-base" />
</InputGroup>

Addons take align="inline-start", "inline-end", "block-start" or "block-end". Clicking an addon focuses the input.

Examples#

Prefix and suffix

hub://
.lua

No spaces allowed.

With buttons

Markdown supported