Sabil UI

Search documentation

Search for a command to run...

Components

Prompt Input

A chat composer with tools, a model picker and a send button that turns into stop.

Installation#

  1. Install the dependencies

    npm install cn lucide-react class-variance-authority radix-ui
  2. Copy 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.tsx
    import * 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 }
    
  3. 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"