nexus-ui
uiprompt-inputsource: ok · hit
Prompt Input
Composable chat input with auto-resizing textarea and action slots
npx shadcn add "https://nexus-ui.dev/r/prompt-input.json"free
Where it goes
chat
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-slot
Registry dependencies
textareascroll-areatooltipkbd
Files1 · source cached 10/9/2026, 11:51:40 PM
components/nexus-ui/prompt-input.tsx
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { mergeRefs } from "@/lib/merge-refs";
import { cn } from "@/lib/utils";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea, ScrollViewport } from "@/components/ui/scroll-area";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Kbd } from "@/components/ui/kbd";
type PromptInputContextValue = {
setTextareaNode: (node: HTMLTextAreaElement | null) => void;
onSubmit?: (value: string) => void;
};
const PromptInputContext = React.createContext<PromptInputContextValue | null>(
null,
);
type PromptInputProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
"onSubmit"
> & {
/**
* Called when Enter is pressed in the textarea (without Shift). Use with
* value/onChange on PromptInputTextarea for controlled mode. Shift+Enter
* inserts a new line.
*/
onSubmit?: (value: string) => void;
};
function PromptInput({
className,
role: _role,
"aria-label": _ariaLabel,
onClick,
onSubmit,
...props
}: PromptInputProps) {
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null);
const handleClick = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (
!target.closest(
'button, a, input, textarea, [role="button"], [role="tab"]',
)
) {
textareaRef.current?.focus();
}
onClick?.(e);
},
[onClick],
);
const contextValue = React.useMemo<PromptInputContextValue>(
() => ({
setTextareaNode: (node) => {
textareaRef.current = node;
},
onSubmit,
}),
[onSubmit],
);
return (
<TooltipProvider delayDuration={100}>
<PromptInputContext.Provider value={contextValue}>
<div
role="group"
aria-label="Chat input"
className={cn(
"relative flex h-auto w-full cursor-text flex-col gap-0 overflow-hidden rounded-3xl border border-border bg-card dark:border-border/50 dark:bg-input/30",
className,
)}
onClick={handleClick}
{...props}
/>
</PromptInputContext.Provider>
</TooltipProvider>
);
}
type PromptInputTextareaProps = React.ComponentProps<typeof Textarea>;
const PromptInputTextarea = React.forwardRef<
HTMLTextAreaElement,
PromptInputTextareaProps
>(function PromptInputTextarea(
{ className, "aria-label": _ariaLabel, onKeyDown, ...props },
ref,
) {
const context = React.useContext(PromptInputContext);
const setTextareaNode = context?.setTextareaNode;
const onSubmit = context?.onSubmit;
const handleTextareaRef = React.useCallback(
(node: HTMLTextAreaElement | null) => {
mergeRefs<HTMLTextAreaElement>(setTextareaNode, ref)(node);
},
[setTextareaNode, ref],
);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey && onSubmit) {
e.preventDefault();
onSubmit(e.currentTarget.value);
}
onKeyDown?.(e);
},
[onSubmit, onKeyDown],
);
return (
<ScrollArea className="max-h-40">
<ScrollViewport>
<Textarea
ref={handleTextareaRef}
aria-label="Message input"
placeholder="How can I help you today?"
className={cn(
"min-h-14 w-full resize-none border-0 bg-transparent px-4 py-4 text-sm leading-6 font-normal text-primary shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent",
className,
)}
onKeyDown={handleKeyDown}
{...props}
/>
</ScrollViewport>
</ScrollArea>
);
});
type PromptInputActionsProps = React.HTMLAttributes<HTMLDivElement>;
function PromptInputActions({
className,
role: _role,
"aria-label": _ariaLabel,
...props
}: PromptInputActionsProps) {
return (
<div
role="group"
aria-label="Input actions"
className={cn(
"flex w-full shrink-0 items-center justify-between px-2 py-2",
className,
)}
{...props}
/>
);
}
type PromptInputActionGroupProps = React.HTMLAttributes<HTMLDivElement>;
function PromptInputActionGroup({
className,
...props
}: PromptInputActionGroupProps) {
return <div className={cn("flex gap-2", className)} {...props} />;
}
type PromptInputActionProps = React.HTMLAttributes<HTMLDivElement> & {
asChild?: boolean;
tooltip?:
| string
| {
content?: string;
side?: "top" | "right" | "bottom" | "left";
shortcut?: string;
};
};
function PromptInputAction({
asChild = false,
tooltip,
...props
}: PromptInputActionProps) {
const Comp = asChild ? Slot : "div";
const { content, side, shortcut } =
typeof tooltip === "string" ? { content: tooltip } : (tooltip ?? {});
if (!content) {
return <Comp {...props} />;
}
return (
<Tooltip>
<TooltipTrigger asChild>
<Comp {...props} />
</TooltipTrigger>
<TooltipContent className="rounded-full" side={side}>
{content}
{shortcut ? <Kbd className="rounded-md!">{shortcut}</Kbd> : null}
</TooltipContent>
</Tooltip>
);
}
export {
PromptInput,
PromptInputTextarea,
PromptInputActions,
PromptInputActionGroup,
PromptInputAction,
};