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:
- Full-bleed scrollport — the vertical scrollbar sits at the main column’s right edge (not beside a narrow message column).
- Shared reading measure — messages and the composer stay on the same centered
max-w-3xlcolumn.
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 choice | What to mount | Prose vs composer |
|---|---|---|
| Avatars on | MessageAvatar on every row | Prose is inset by the avatar column; composer stays full measure width |
| Avatars off | Omit 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
MessageScrollerViewportinmax-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-3xlaround the composer beside a guttered Viewport — that recreates the misalignment this component exists to prevent.
Modes
| Mode | Placement | Behavior |
|---|---|---|
content (default) | Inside MessageScrollerContent | Measure only (max-w-3xl, flex flex-col gap-4, px-4) — no second gutter |
chrome | Sibling 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-bandFUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/chat-measure-bandFUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/chat-measure-bandFUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/chat-measure-bandInstall 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.
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 chromeOverride 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
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "content" | "chrome" | "content" | content: measure only. chrome: full-width gutter shell, then the same measure. |
className | string | — | Merged onto the measure (max-w-3xl, flex flex-col gap-4, px-4). |
shellClassName | string | — | Merged onto the chrome shell only; ignored when mode="content". |
children | ReactNode | — | Measure 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).