f-ui
Components

Chat Measure Band

Keeps transcript and composer on one centered measure beside a full-bleed MessageScrollerViewport — mirrors scrollbar-gutter so columns do not drift apart.

Plus Registry

This component ships from registry.plus.json, not the public registry.json. Set up @f-ui-plus on the Installation page, then install @f-ui-plus/chat-measure-band.

Chat Measure Band is the host recipe that keeps a centered max-w-3xl reading column aligned between the transcript and the composer on a full-bleed Message Scroller page.

Why It Exists

Product chat wants two things at once:

  1. Full-bleed scrollport — the vertical scrollbar sits at the main column’s right edge (not beside a narrow message column).
  2. Shared reading measure — messages and the composer stay on the same centered max-w-3xl column.

Hosts often get (1) right: Viewport is full width and uses scrollbar-gutter: stable. Then they only wrap composer in the “same” max-w-3xl div and ship. That looks correct until you notice:

  • The transcript column sits slightly left of the composer.
  • The assistant avatar hangs past the composer’s left/right edge (easy to misread as a Message / avatar bug).

Cause: Viewport’s stable gutter narrows the scroll content box on the inline-end side. A composer band outside the Viewport that does not mirror that gutter keeps a wider content box. Two max-w-3xl wrappers then center on different widths, so the columns no longer share an edge. MDN scrollbar-gutter Example 3 requires the adjacent non-scrolling band to use scrollbar-gutter: stable as well.

Also: even when both sides set stable, unequal scrollbar thicknesses (classic vs Viewport’s thin scrollbar) still reserve different gutter widths and shift the measure by a couple of pixels. Chrome mode ships the same thin scrollbar twin as Viewport.

Chat Measure Band welds the recipe so hosts stop hand-rolling it wrong:

  • mode="content" — measure only inside Content (Viewport already owns the gutter).
  • mode="chrome" — full-width shell that mirrors gutter + thin twin, then the same measure for the composer.

Avatar column is a separate decision

Measure alignment (this component) is not the same as Message row avatar layout:

Product choiceWhat to mountProse vs composer
Avatars onMessageAvatar on every rowProse is inset by the avatar column; composer stays full measure width
Avatars offOmit the slot (no spacer)Assistant prose / actions share the measure’s left padding with the composer

Pick one product rule — do not mix per-row. /showcases/chat toggles Avatar on / Avatar off against the same Chat Measure Band column.

When To Use

  • Product chat pages where the scrollbar must sit at the pane edge and messages must line up with the composer.
  • Any adjacent non-scrolling chrome under the same column as a Viewport that uses scrollbar-gutter: stable.
  • Do not wrap MessageScrollerViewport in max-w-* (that floats the scrollbar beside the narrow column — the original full-bleed bug).
  • Do not use mode="chrome" inside Content — Viewport already reserved the gutter (double gutter shifts the column again).
  • Do not put only a bare max-w-3xl around the composer beside a guttered Viewport — that recreates the misalignment this component exists to prevent.

Modes

ModePlacementBehavior
content (default)Inside MessageScrollerContentMeasure only (max-w-3xl, flex flex-col gap-4, px-4) — no second gutter
chromeSibling of Viewport (composer)Full-width shell with overflow-y-auto + scrollbarGutter: "stable" + thin scrollbar twin matching Viewport, then the same measure

Chrome shell thin scrollbar width must match Viewport’s thin scrollbar (not only stable) — unequal reserved widths shift the measure.

Installing

Configure @f-ui-plus and FUI_PLUS_REGISTRY_TOKEN as in Installation — Plus Registry.

FUI_PLUS_REGISTRY_TOKEN=xxx pnpm dlx shadcn@latest add @f-ui-plus/chat-measure-band
FUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/chat-measure-band
FUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/chat-measure-band
FUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/chat-measure-band

Install URL: https://ui.isaacfei.com/api/plus/r/chat-measure-band.json.

Usage

import { ChatMeasureBand } from "@/components/f-ui/chat-measure-band/chat-measure-band";

<MessageScrollerViewport>
  <MessageScrollerContent>
    <ChatMeasureBand className="py-4">
      {/* messages */}
    </ChatMeasureBand>
  </MessageScrollerContent>
</MessageScrollerViewport>

<ChatMeasureBand mode="chrome" className="pb-4 pt-3">
  <PromptComposer>{/* … */}</PromptComposer>
</ChatMeasureBand>

Examples

Content + Chrome

Demonstrates the failure-mode fix: fake Viewport gutter + content measure, with composer chrome mirroring gutter and thin scrollbar so the two columns share edges.

Assistant message (content measure)
User message
Composer chrome (mirrored gutter)
import { ChatMeasureBand } from "@/components/f-ui/chat-measure-band/chat-measure-band";

import "@/components/f-ui/message-scroller/message-scroller-scrollbars.css";

export function ChatMeasureBandDemo() {
  return (
    <div className="flex h-72 w-full flex-col overflow-hidden rounded-lg border bg-background">
      <div
        className="fui-message-scroller-viewport min-h-0 flex-1 overflow-y-auto"
        style={{ scrollbarGutter: "stable" }}
      >
        <ChatMeasureBand className="py-4">
          <div className="rounded-md border bg-muted/40 px-3 py-2 text-sm">
            Assistant message (content measure)
          </div>
          <div className="ml-auto max-w-[85%] rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground">
            User message
          </div>
        </ChatMeasureBand>
      </div>
      <ChatMeasureBand mode="chrome" className="border-t bg-background pb-3 pt-2">
        <div className="rounded-xl border px-3 py-2 text-sm text-muted-foreground">
          Composer chrome (mirrored gutter)
        </div>
      </ChatMeasureBand>
    </div>
  );
}

Composition

MessageScrollerViewport          ← overflow-y + scrollbarGutter:stable + thin scrollbar
└── MessageScrollerContent
    └── ChatMeasureBand          ← mode="content" (default): max-w-3xl measure
ChatMeasureBand mode="chrome"    ← shell mirrors gutter + thin twin; inner measure
└── PromptComposer | host chrome

Override padding / gap via className (measure). Override the chrome shell via shellClassName (e.g. overflow-hidden). Keep max-w-3xl unless the product deliberately changes measure width.

API Reference

Props

PropTypeDefaultDescription
mode"content" | "chrome""content"content: measure only. chrome: full-width gutter shell, then the same measure.
classNamestringMerged onto the measure (max-w-3xl, flex flex-col gap-4, px-4).
shellClassNamestringMerged onto the chrome shell only; ignored when mode="content".
childrenReactNodeMeasure contents (messages or composer).

Defaults: measure uses max-w-3xl, flex flex-col gap-4, px-4. Chrome shell adds overflow-y-auto, scrollbarGutter: "stable", and the thin scrollbar twin class (parity with MessageScrollerViewport).

On this page