Components
Prompt Input
A chat composer with tools, a model picker and a send button that turns into stop.
Installation#
Install the dependencies
npm install cn lucide-react class-variance-authority radix-uiCopy the source into your project
components/ui/prompt-input.tsx"use client"import * as React from "react"import { cn } from "cn"import { ArrowUpIcon, SquareIcon } from "lucide-react"import { Button } from "@/components/ui/button"function PromptInput({ className, ...props }: React.ComponentProps<"form">) { return ( <form data-slot="prompt-input" className={cn( "flex w-full flex-col gap-1 rounded-2xl border border-input bg-background p-2 shadow-xs transition-[border-color,box-shadow] duration-150 ease-out focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-[textarea:disabled]:opacity-60 dark:bg-input/30", className )} {...props} /> )}/** Grows with its content. Enter submits the form, Shift+Enter adds a line. */function PromptInputTextarea({ className, onKeyDown, ...props}: React.ComponentProps<"textarea">) { function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) { onKeyDown?.(event) if (event.defaultPrevented) return if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault() event.currentTarget.form?.requestSubmit() } } return ( <textarea data-slot="prompt-input-textarea" rows={1} onKeyDown={handleKeyDown} className={cn( "field-sizing-content max-h-48 min-h-10 w-full resize-none bg-transparent px-2 py-1.5 text-base outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm", className )} {...props} /> )}function PromptInputToolbar({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="prompt-input-toolbar" className={cn("flex items-center justify-between gap-2", className)} {...props} /> )}function PromptInputTools({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="prompt-input-tools" className={cn("flex min-w-0 items-center gap-1", className)} {...props} /> )}/** Send button. While `streaming`, it turns into a stop button. */function PromptInputSubmit({ streaming = false, onStop, className, ...props}: React.ComponentProps<typeof Button> & { streaming?: boolean; onStop?: () => void }) { if (streaming) { return ( <Button data-slot="prompt-input-submit" type="button" size="icon-sm" aria-label="Stop" onClick={onStop} className={cn("rounded-full", className)} {...props} > <SquareIcon className="size-3 fill-current" /> </Button> ) } return ( <Button data-slot="prompt-input-submit" type="submit" size="icon-sm" aria-label="Send" className={cn("rounded-full", className)} {...props} > <ArrowUpIcon /> </Button> )}export { PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputTools, PromptInputSubmit }components/ui/button.tsximport * as React from "react"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "cn"import { Slot } from "radix-ui"const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium tracking-[-0.01em] whitespace-nowrap transition-[transform,opacity,background-color,box-shadow,color] duration-150 ease-out outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-primary bg-clip-border text-primary-foreground surface-primary hover:brightness-[1.06] active:brightness-95", outline: "bg-clip-border text-foreground surface-raised hover:[--raised:color-mix(in_oklab,var(--background),var(--foreground)_4%)] aria-expanded:[--raised:color-mix(in_oklab,var(--background),var(--foreground)_4%)] dark:hover:[--raised:color-mix(in_oklab,var(--muted),white_6%)]", secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 gap-2 px-4 pb-0.5 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5", xs: "h-7 gap-1 rounded-md px-2.5 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", sm: "h-8 gap-1.5 rounded-md px-3 text-[13px] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-3.5", lg: "h-11 gap-2 px-5 pb-0.5 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4", icon: "size-10", "icon-xs": "size-7 rounded-md in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", "icon-sm": "size-8 rounded-md in-data-[slot=button-group]:rounded-lg", "icon-lg": "size-11", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant = "default", size = "default", asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot.Root : "button" return ( <Comp data-slot="button" data-variant={variant} data-size={size} className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }Make sure the foundation is in place
Components read their colors, radius and surfaces from the tokens in
app/globals.css. See Theming.
Usage#
import { PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputTools, PromptInputSubmit,} from "@/components/ui/prompt-input"