---
title: Quick start
description: Install the package, set it up, and assemble your first chat.
---

**@intentface/chat** ships the behavior of a chat interface and none of its
appearance. This page takes you from an empty React 19 app to a working chat.

## Install the library

```bash
npm install @intentface/chat
# pnpm add @intentface/chat
# yarn add @intentface/chat
# bun add @intentface/chat
```

`react` and `react-dom` (v19+) are the only peer dependencies. Nothing else
ships with it beyond two small runtime deps — `@floating-ui/dom` for anchored
positioning and `nanoid` for attachment ids. No styling, no editor framework, no
animation library.

Each primitive is a separate entry point:

```tsx
import { Composer } from "@intentface/chat/composer";
import { Message } from "@intentface/chat/message";
import { Thread } from "@intentface/chat/thread";
import { groupTurns } from "@intentface/chat/message-utils";
```

## Set up

### Portals

The composer's command popover, its panel, and the attachment preview all render
through portals into `document.body`, so they escape any overflow or transform on
your layout. To keep them above the rest of the page regardless of your own
stacking, give your app root its own stacking context.

In your root layout:

```tsx
<body>
  <div className="root">{children}</div>
</body>
```

And in your global stylesheet:

```css
.root {
  isolation: isolate;
}
```

Without this, a `z-index` anywhere in your layout can paint over the command
popover.

## Assemble a component

Three primitives make a chat: `Thread` owns the scroll area and reserves space
for its docked composer, `Message` renders each turn, and `Composer` takes input.
Every part renders semantic DOM with `data-*` state attributes and no classes of
its own — you pass `className` to each one, so the look is yours from the first
render.

```tsx title="quick-start/demos/hero.tsx"
"use client";

import { Composer, type ComposerSubmitData } from "@intentface/chat/composer";
import { Message } from "@intentface/chat/message";
import { Thread } from "@intentface/chat/thread";
import { type ComponentProps, useState } from "react";

type ChatMessage = { id: string; role: "user" | "assistant"; text: string };

const INITIAL: ChatMessage[] = [
  { id: "1", role: "user", text: "Can you summarise this thread?" },
  {
    id: "2",
    role: "assistant",
    text: "Sure — it covers the composer's segment model, how chips serialise, and why the editor owns its own DOM.",
  },
];

// Three primitives assembled: Thread owns the scroll area and reserves space
// for its docked composer, Message renders each turn, Composer takes input.
export const Hero = () => {
  const [messages, setMessages] = useState<ChatMessage[]>(INITIAL);

  const handleSubmit = (data: ComposerSubmitData) => {
    if (data.kind !== "message" || !data.text.trim()) return;
    setMessages((current) => [
      ...current,
      { id: `${current.length}`, role: "user", text: data.text },
    ]);
  };

  return (
    <div className="h-[360px] w-full max-w-xl overflow-hidden rounded-xl border border-[#f0f0f0] bg-white dark:border-[#262626] dark:bg-[#111111]">
      <Thread.Root className="relative flex h-full w-full overflow-hidden [--thread-overlay-top-height:1rem]">
        <Thread.Viewport className="h-full w-full overflow-x-hidden overflow-y-auto outline-none [overflow-anchor:auto]">
          <div className="relative flex min-h-full w-full flex-col items-center pt-(--thread-overlay-top-height) pb-(--thread-overlay-bottom-height)">
            <Thread.Content className="mx-auto flex min-h-full w-full flex-col gap-4 px-4 [&>*:last-child]:min-h-(--thread-turn-min-height,0px)">
              {messages.map((message, index) => (
                <Message.Root
                  key={message.id}
                  role={message.role}
                  isLast={index === messages.length - 1}
                  className="group flex w-full flex-col gap-1 data-[role=user]:items-end"
                >
                  <Message.Text className="text-sm leading-[1.7] text-[#1a1a1a] group-data-[role=user]:min-h-9 group-data-[role=user]:max-w-[80%] group-data-[role=user]:rounded-2xl group-data-[role=user]:border group-data-[role=user]:border-[#f0f0f0] group-data-[role=user]:bg-white group-data-[role=user]:px-3 group-data-[role=user]:py-1.5 group-data-[role=user]:shadow-xs dark:text-[#fcfcfc] dark:group-data-[role=user]:border-[#262626] dark:group-data-[role=user]:bg-[#181818]">
                    {message.text}
                  </Message.Text>
                </Message.Root>
              ))}
            </Thread.Content>
          </div>
        </Thread.Viewport>
        <Thread.Composer className="absolute inset-x-0 bottom-0 z-2 w-full">
          <div className="flex w-full flex-col items-center px-4 pb-4">
            <Composer.Root onSubmit={handleSubmit} className="flex w-full flex-col">
              <Composer.Container className="cursor-text rounded-2xl border border-[#f0f0f0] bg-white shadow-xs transition-colors focus-within:border-[#ececec] dark:border-[#262626] dark:bg-[#181818] dark:focus-within:border-[#2d2d2d]">
                {/* The editable element is engine-owned and out of JSX reach, so
                    it is styled through the data-composer-editor variants. */}
                <Composer.Textarea className="max-h-32 min-h-8 overflow-y-auto px-4 pt-3 text-sm **:data-composer-editor:w-full **:data-composer-editor:max-w-none **:data-composer-editor:leading-[1.7] [&_[data-composer-editor]:focus]:outline-none">
                  <Composer.Placeholder
                    placeholder="Send a message…"
                    className="leading-[1.7] text-[#949494] dark:text-[#6f6f6f]"
                  />
                </Composer.Textarea>
                <Composer.Actions className="flex justify-end gap-2 p-2">
                  <Composer.Submit className="flex size-8 items-center justify-center rounded-full bg-[#1a1a1a] text-white transition-opacity disabled:opacity-40 dark:bg-[#fcfcfc] dark:text-[#111111]">
                    <SendIcon />
                  </Composer.Submit>
                </Composer.Actions>
              </Composer.Container>
            </Composer.Root>
          </div>
        </Thread.Composer>
      </Thread.Root>
    </div>
  );
};

const SendIcon = (props: ComponentProps<"svg">) => (
  <svg
    width="16"
    height="16"
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.5"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="M8 13V3m0 0L3.5 7.5M8 3l4.5 4.5" />
  </svg>
);
```

`Composer.Submit` disables itself while the field is empty, and `Thread`
publishes its docked-composer reserve as `--thread-overlay-bottom-height` so the
scroll area never hides behind it. See [Composer](/primitives/composer),
[Thread](/primitives/thread), and [Message](/primitives/message) for
the full part lists.

## Pre-styled components

There is no pre-styled `@intentface/chat` package, and no CSS to install. The
demos on each component page are the styled reference: they use stock Tailwind,
depend on nothing but this package, and are meant to be copied and edited.

This site's own chat is built from components that live in the app, not the
package. They use design tokens, Motion, and local icon files, and they are not
published — read them as a reference implementation if you like, but they are not
a starting point.

## Working with LLMs

Append `.md` to any docs URL to get that page as markdown — for example
[`/primitives/composer.md`](/primitives/composer.md). Every demo's
source is inlined into it as a code block, so an agent reading the page gets the
code rather than a component tag it can't resolve. The **View as Markdown** link
in the page header does the same thing.

[`/llms.txt`](/llms.txt) indexes every page with its description and markdown
URL. Feed it to an assistant to let it navigate the docs without crawling HTML.

## Next steps

- [Styling](/handbook/styling) — the state model, data attributes, and render props
- [Composer](/primitives/composer) — commands, chips, attachments, and the ask-user flow
- [Thread](/primitives/thread) — the scroll container and auto-follow behavior
