Skip to content
ChatJS
Esc
navigateopen⌘Jpreview
On this page

Chat Layout

Composable resizable main and secondary panel layout

Split-panel chat interface with a collapsible secondary panel for artifacts, previews, or tools.

Overview

A responsive two-panel layout where the main chat occupies the primary space and a secondary panel (for artifacts, code previews, etc.) slides in when needed. On mobile, the secondary panel replaces the main panel. Composable shadcn-style primitives.

How it works

The layout uses react-resizable-panels wrapped in composable components:

Component Purpose
ChatLayout Root container with sidebar-aware max-width
ChatLayoutMain Primary panel, hides on mobile when secondary is visible
ChatLayoutHandle Drag handle between panels
ChatLayoutSecondary Conditional panel, renders only when visible

Visibility is controlled via the isSecondaryPanelVisible prop on ChatLayout. This value is shared to child components through React context, so they automatically show/hide based on the state.

Code

Components

"use client";

import type { ComponentProps } from "react";
import { createContext, useContext } from "react";
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable";
import { useSidebar } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";

type ChatLayoutContextValue = {
  isSecondaryPanelVisible: boolean;
};

const ChatLayoutContext = createContext<ChatLayoutContextValue | null>(null);

function useChatLayoutContext() {
  const context = useContext(ChatLayoutContext);
  if (!context) {
    throw new Error("ChatLayout components must be used within <ChatLayout />");
  }
  return context;
}

type ChatLayoutProps = Omit<
  ComponentProps<typeof ResizablePanelGroup>,
  "direction"
> & {
  isSecondaryPanelVisible?: boolean;
};

export const ChatLayout = ({
  className,
  children,
  isSecondaryPanelVisible = false,
  ...props
}: ChatLayoutProps) => {
  const { state: sidebarState } = useSidebar();

  return (
    <ChatLayoutContext.Provider value={{ isSecondaryPanelVisible }}>
      <ResizablePanelGroup
        className={cn(
          "@container flex h-dvh w-full min-w-0 max-w-screen flex-col bg-background md:max-w-[calc(100vw-var(--sidebar-width))]",
          sidebarState === "collapsed" && "md:max-w-screen",
          className
        )}
        direction="horizontal"
        {...props}
      >
        {children}
      </ResizablePanelGroup>
    </ChatLayoutContext.Provider>
  );
};

type ChatLayoutMainProps = ComponentProps<typeof ResizablePanel>;

export const ChatLayoutMain = ({
  className,
  defaultSize = 65,
  minSize = 40,
  ...props
}: ChatLayoutMainProps) => {
  const { isSecondaryPanelVisible } = useChatLayoutContext();

  return (
    <ResizablePanel
      className={cn(isSecondaryPanelVisible && "hidden md:block", className)}
      defaultSize={defaultSize}
      minSize={minSize}
      {...props}
    />
  );
};

type ChatLayoutSecondaryProps = ComponentProps<typeof ResizablePanel>;

export const ChatLayoutSecondary = ({
  defaultSize = 35,
  minSize = 25,
  ...props
}: ChatLayoutSecondaryProps) => {
  const { isSecondaryPanelVisible } = useChatLayoutContext();

  if (!isSecondaryPanelVisible) {
    return null;
  }

  return (
    <ResizablePanel defaultSize={defaultSize} minSize={minSize} {...props} />
  );
};

type ChatLayoutHandleProps = ComponentProps<typeof ResizableHandle>;

export const ChatLayoutHandle = ({
  className,
  withHandle = true,
  ...props
}: ChatLayoutHandleProps) => {
  const { isSecondaryPanelVisible } = useChatLayoutContext();

  if (!isSecondaryPanelVisible) {
    return null;
  }

  return (
    <ResizableHandle
      className={cn("hidden md:flex", className)}
      withHandle={withHandle}
      {...props}
    />
  );
};

Usage

"use client";

import {
  ChatLayout,
  ChatLayoutHandle,
  ChatLayoutMain,
  ChatLayoutSecondary,
} from "@/components/chat/chat-layout";
import { MainChatPanel } from "@/components/chat/main-chat-panel";
import { SecondaryChatPanel } from "@/components/chat/secondary-chat-panel";
import { useArtifactSelector } from "@/hooks/use-artifact";

export function Chat({ id, isReadonly, projectId }) {
  const isSecondaryPanelVisible = useArtifactSelector(
    (state) => state.isVisible
  );

  return (
    <ChatLayout isSecondaryPanelVisible={isSecondaryPanelVisible}>
      <ChatLayoutMain>
        <MainChatPanel
          chatId={id}
          className="flex h-full min-w-0 flex-1 flex-col"
          isReadonly={isReadonly}
          projectId={projectId}
        />
      </ChatLayoutMain>

      <ChatLayoutHandle />

      <ChatLayoutSecondary>
        <SecondaryChatPanel
          className="flex h-full min-w-0 flex-1 flex-col"
          isReadonly={isReadonly}
        />
      </ChatLayoutSecondary>
    </ChatLayout>
  );
}

Customization

Override default sizes per-instance:

<ChatLayoutMain defaultSize={70} minSize={50}>
  {/* ... */}
</ChatLayoutMain>

<ChatLayoutSecondary defaultSize={30} minSize={20}>
  {/* ... */}
</ChatLayoutSecondary>

Was this page helpful?