Can the conversation scroll beneath the composer?
Agents
ChatLayout
A scrolling conversation with message turns and a floating composer dock.
Composition
Conversation and composer
- <ChatLayout>
- <ChatThread>
- <ChatTurn>
- <ChatMessage />
- <Activity>
- <ActivityTrigger>
- <ActivityTitle />
- <ActivityContent>
- reasoning content
- <ActivityTrigger>
- <ChatComposer />
- <ChatTurn>
- <ChatThread>
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
This agent installs from the chat-layout registry. Install the bundle with the command above, or inspect the source below.
npx shadcn@latest add https://control-ui.dev/r/chat-layout.jsonUsage
import type { ReactNode } from "react";import { ChatLayout, ChatThread, ChatTurn } from "@/components/control-ui/chat-layout";export function Example({ children }: { children: ReactNode }) { return ( <ChatLayout> <ChatThread> <ChatTurn from="assistant">{children}</ChatTurn> </ChatThread> </ChatLayout> );}Raw code
This agent’s owned source and private support files
import type { ComponentProps, CSSProperties, ReactNode } from "react";import type { ChatLayoutKnobStyle } from "@/components/control-ui/knob-contracts/chat-layout-knobs";import { cn } from "@/components/control-ui/lib/cn";import { SkinAdornment } from "@/components/control-ui/skin-provider";export type ChatLayoutChrome = "panel" | "embedded";export type ChatLayoutProps = Omit<ComponentProps<"section">, "style"> & { chrome?: ChatLayoutChrome; style?: CSSProperties & ChatLayoutKnobStyle;};export function ChatLayout({ children, chrome = "panel", className, ...props }: ChatLayoutProps) { return ( <section data-control-ui="chat-layout" data-control-family="chat-layout" data-slot="root" data-chrome={chrome} data-surface={chrome === "panel" ? "panel" : undefined} className={cn("relative mx-auto flex min-h-[640px] w-full max-w-3xl flex-col overflow-hidden", className)} {...props} > {chrome === "panel" ? <SkinAdornment scope="chat-layout" part="titlebar" context={{}} /> : null} {children} </section> );}export type ChatThreadProps = ComponentProps<"div"> & { composer?: ReactNode;};export function ChatThread({ children, composer, className, ...props }: ChatThreadProps) { return ( <div data-control-ui="chat-thread" data-control-family="chat-layout" data-chat-layout-kind="thread" data-slot="root" className={cn("min-h-0 min-w-0 flex-1 overflow-y-auto", className)} {...props} > <div className="relative flex min-h-full min-w-0 flex-col"> <div data-control-ui="chat-thread" data-control-family="chat-layout" data-slot="thread-content" className="flex min-w-0 flex-1 flex-col" > {children} </div> {composer ? ( <div data-control-ui="chat-thread" data-control-family="chat-layout" data-slot="dock" className="sticky bottom-0 z-10 shrink-0"> {composer} </div> ) : null} </div> </div> );}export type ChatTurnProps = ComponentProps<"section"> & { from: "user" | "assistant";};export function ChatTurn({ from, children, className, ...props }: ChatTurnProps) { return ( <section data-control-ui="chat-turn" data-control-family="chat-layout" data-slot="turn" data-from={from} className={cn("flex w-full flex-col", from === "user" && "items-end", className)} {...props} > {children} </section> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-chat-layout-* · 5 knobsHow the cascade resolves--cui-chat-layout-radius<length-percentage>var(--radius-scene)--cui-chat-layout-background<color>var(--background)--cui-chat-layout-border-color<color>var(--border)--cui-chat-layout-border-width<length>1px--cui-chat-layout-shadow*var(--shadow-md)