Page actions

Layout Modes

@threadplane/chat ships four ready-made layout compositions. Pick the one that best fits how chat lives inside your app.

ModeComponentBest for
Embedded<chat>Chat is the primary purpose of the page
Popup<chat-popup>Non-intrusive assistant overlay on any page
Sidebar<chat-sidebar>Copilot panel alongside existing app content
Sidenav<chat-sidenav>A thread and project navigation rail next to a full-page chat

Embedded — <chat>

The embedded composition fills its parent container using height: 100% and a flex column layout. Reach for it when chat is the main feature of a route.

When to pick this mode:

  • Chat is the whole page (e.g., a dedicated /chat route)
  • You control the container's dimensions and want the chat to fill them
  • You do not need to show app content alongside the chat simultaneously
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { injectAgent, provideAgent } from '@threadplane/langgraph';
import { ChatComponent } from '@threadplane/chat';
 
@Component({
  selector: 'app-chat-page',
  standalone: true,
  imports: [ChatComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    provideAgent({
      assistantId: 'chat',
      threadId: signal(null),
    }),
  ],
  template: `
    <div style="height: 100vh;">
      <chat [agent]="chatAgent" />
    </div>
  `,
})
export class ChatPageComponent {
  protected readonly chatAgent = injectAgent();
}
Tip: Height is required

<chat> uses height: 100%. Give its parent an explicit height or the component will collapse to zero.

The popup composition renders a circular launcher button fixed to the bottom-right corner of the viewport. Clicking it opens a floating chat window. The window can be dismissed without losing the conversation.

When to pick this mode:

  • Chat is a secondary feature on pages that already have a purpose
  • You want minimal disruption to existing layouts
  • Support chat, help bots, or contextual assistants
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { injectAgent, provideAgent } from '@threadplane/langgraph';
import { ChatPopupComponent } from '@threadplane/chat';
 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChatPopupComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    provideAgent({
      assistantId: 'support_agent',
      threadId: signal(null),
    }),
  ],
  template: `
    <!-- your existing app content -->
    <router-outlet />
 
    <!-- chat launcher + window, position: fixed internally -->
    <chat-popup [agent]="chatAgent" />
  `,
})
export class AppComponent {
  protected readonly chatAgent = injectAgent();
}

Programmatic control is available via template reference or two-way binding:

<!-- Open from a custom button -->
<button (click)="popup.openWindow()">Ask AI</button>
<chat-popup [agent]="chatAgent" #popup />
 
<!-- Two-way binding -->
<chat-popup [agent]="chatAgent" [(open)]="chatIsOpen" />

The sidebar composition renders a slide-in panel anchored to the right edge. It supports two modes: overlay (default) and push-content. In push-content mode, your projected app content is shifted left rather than covered.

When to pick this mode:

  • Copilot experience where the user references the app while chatting
  • You want both the chat and the app content visible at the same time
  • Dashboards, editors, or data-heavy pages where context matters
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { injectAgent, provideAgent } from '@threadplane/langgraph';
import { ChatSidebarComponent } from '@threadplane/chat';
 
@Component({
  selector: 'app-shell',
  standalone: true,
  imports: [ChatSidebarComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    provideAgent({
      assistantId: 'copilot_agent',
      threadId: signal(null),
    }),
  ],
  template: `
    <chat-sidebar [agent]="chatAgent" [pushContent]="true">
      <!-- app content is projected here and shifts when sidebar opens -->
      <main>
        <router-outlet />
      </main>
    </chat-sidebar>
  `,
})
export class AppShellComponent {
  protected readonly chatAgent = injectAgent();
}

Toggle the sidebar from a nav button:

<nav>
  <button (click)="sidebar.toggle()">
    Chat
  </button>
</nav>
 
<chat-sidebar [agent]="chatAgent" #sidebar>
  <main><router-outlet /></main>
</chat-sidebar>

Sidenav — <chat-sidenav>

The sidenav composition is the conversation navigation rail: a thread list, an optional project list, a search action, and a new-chat action. It is the one composition here that does not wrap <chat>. All of its content slots are named and target regions inside the rail, so a <chat> placed between the tags is silently dropped. Render the chat as a sibling and lay the two out yourself.

When to pick this mode:

  • The product is a full-page chat with many saved conversations
  • You want ChatGPT-style thread and project navigation beside the conversation
  • You already have a thread store to feed the list
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
import { ChatComponent, ChatSidenavComponent } from '@threadplane/chat';
import type { Thread } from '@threadplane/chat';
 
@Component({
  selector: 'app-chat-shell',
  standalone: true,
  imports: [ChatSidenavComponent, ChatComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styles: [`
    :host { display: flex; height: 100dvh; }
    .chat-pane { flex: 1; min-width: 0; }
  `],
  template: `
    <chat-sidenav
      [agent]="chatAgent"
      [threads]="threads()"
      [activeThreadId]="activeThreadId()"
      (newChat)="activeThreadId.set(null)"
      (threadSelected)="activeThreadId.set($event)"
    />
    <main class="chat-pane">
      <chat [agent]="chatAgent" />
    </main>
  `,
})
export class ChatShellComponent {
  protected readonly chatAgent = injectAgent();
  protected readonly threads = signal<Thread[]>([]);
  protected readonly activeThreadId = signal<string | null>(null);
}

[mode] switches between 'expanded', 'collapsed', and 'drawer', and the component emits (modeChange) when the user presses Cmd/Ctrl+B. Cmd/Ctrl+K emits (searchOpened) so you can open your own command palette.

Tip: The devtools launcher is on by default

When @threadplane/chat/debug is part of your build and [agent] is bound, <chat-sidenav> renders a Devtools launcher in its footer. [debug] already defaults to true; pass [debug]="false" to suppress it.

Choosing a Mode

QuestionAnswer
Is chat the whole page?Use <chat>
Should chat be invisible until needed?Use <chat-popup>
Should chat and app content coexist?Use <chat-sidebar>
Should the sidebar push content aside?Use <chat-sidebar [pushContent]="true">
Do users need to browse many saved conversations?Add <chat-sidenav> beside <chat>

All four compositions share the same --tplane-chat-* token system, so whichever mode you land on, it will match your brand. Next, head to Theming to customize colors, shapes, and typography across all modes at once.

Looking for something specific?