shadimage
vllnt-ui
componentai-chat-inputsource: stale · stale

AI Chat Input

Prompt composer for conversational interfaces with helper text, toolbar actions, and submit states.

npx shadcn add "https://ui.vllnt.com/r/ai-chat-input.json"
free

This source snapshot is older than our freshness window and couldn't be re-verified just now — it may be outdated.

Where it goes

statschat

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Files1 · source cached 10/8/2026, 4:29:08 AM

registry/default/ai-chat-input/ai-chat-input.tsx
import { cva } from "class-variance-authority";
import { LoaderCircle, SendHorizontal } from "lucide-react";

import { cn } from "@vllnt/ui";
import { Button } from "@vllnt/ui";
import { Textarea } from "@vllnt/ui";

const formShellVariants = cva(
  "rounded-2xl border border-input bg-background shadow-sm",
);

function AIChatInputFooter({
  currentLength,
  helperText,
  isSubmitDisabled,
  isSubmitting,
  maxLength,
  status,
  submitLabel,
}: {
  currentLength: number;
  helperText?: string;
  isSubmitDisabled: boolean;
  isSubmitting: boolean;
  maxLength?: number;
  status?: string;
  submitLabel: string;
}) {
  return (
    <div className="flex flex-col gap-3 border-t border-border/60 pt-3 sm:flex-row sm:items-end sm:justify-between">
      <div className="space-y-1 text-xs text-muted-foreground">
        {helperText ? <p>{helperText}</p> : null}
        <div className="flex flex-wrap items-center gap-2">
          {status ? <span>{status}</span> : null}
          {typeof maxLength === "number" ? (
            <span>
              {currentLength}/{maxLength}
            </span>
          ) : null}
        </div>
      </div>

      <Button
        className="self-start rounded-full px-4 sm:self-auto"
        disabled={isSubmitDisabled}
        type="submit"
      >
        {isSubmitting ? (
          <LoaderCircle
            className="mr-2 size-4 animate-spin motion-reduce:[animation-duration:1.5s]"
            data-motion="essential"
          />
        ) : (
          <SendHorizontal className="mr-2 size-4" />
        )}
        {submitLabel}
      </Button>
    </div>
  );
}

export type AIChatInputProps = React.ComponentPropsWithoutRef<"form"> & {
  /** Disables editing and submit actions. */
  disabled?: boolean;
  /** Optional helper text shown below the prompt field. */
  helperText?: string;
  /**
   * Accessible name for the message textarea. Defaults to `"Chat message"`.
   * Override `textareaProps.aria-label` / `aria-labelledby` for finer control.
   */
  inputLabel?: string;
  /** Whether the submit action is in progress. */
  isSubmitting?: boolean;
  /** Called whenever the textarea value changes. */
  onValueChange?: (value: string) => void;
  /** Optional status text shown beside helper copy. */
  status?: string;
  /** Label for the submit button. */
  submitLabel?: string;
  /** Props forwarded to the textarea primitive. */
  textareaProps?: React.TextareaHTMLAttributes<HTMLTextAreaElement>;
  /** Optional controls rendered above the footer row. */
  toolbar?: React.ReactNode;
  /** Controlled textarea value. */
  value?: string;
};

const AIChatInput = ({
  className,
  disabled = false,
  helperText,
  inputLabel = "Chat message",
  isSubmitting = false,
  onSubmit,
  onValueChange,
  ref,
  status,
  submitLabel = "Send",
  textareaProps,
  toolbar,
  value,
  ...props
}: AIChatInputProps & { ref?: React.Ref<HTMLFormElement> }) => {
  const currentValue = value ?? "";
  const maxLength = textareaProps?.maxLength;
  const isSubmitDisabled =
    disabled || isSubmitting || currentValue.trim().length === 0;

  return (
    <form
      className={cn(formShellVariants(), "w-full p-3", className)}
      onSubmit={onSubmit}
      ref={ref}
      {...props}
    >
      <div className="space-y-3">
        <Textarea
          aria-label={inputLabel}
          className="min-h-[120px] resize-none rounded-xl border-0 bg-transparent p-1 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
          disabled={disabled}
          onChange={(event) => {
            textareaProps?.onChange?.(event);
            onValueChange?.(event.target.value);
          }}
          placeholder="Ask a follow-up question, paste context, or describe what you need..."
          value={value}
          {...textareaProps}
        />

        {toolbar ? <div className="flex flex-wrap gap-2">{toolbar}</div> : null}

        <AIChatInputFooter
          currentLength={currentValue.length}
          helperText={helperText}
          isSubmitDisabled={isSubmitDisabled}
          isSubmitting={isSubmitting}
          maxLength={maxLength}
          status={status}
          submitLabel={submitLabel}
        />
      </div>
    </form>
  );
};

AIChatInput.displayName = "AIChatInput";

export { AIChatInput };

More from vllnt-ui

Browse all components in this library →