Chat Minimap
A rail of lines that maps a conversation, one line per turn.
"use client";
import { cn } from "cn";
import { useState } from "react";
import {
ChatMinimap,
ChatMinimapItem,
ChatMinimapNavButton,
} from "../components/chat-minimap";
import {
CHAT_MINIMAP_DEMO_TURNS,
CHAT_MINIMAP_VISIBLE_TURNS,
} from "../constants/chat-minimap";
import type { ChatMinimapExampleProps } from "../types/chat-minimap";
export default function ChatMinimapExample({
initialStart = 2,
side = "right",
turns = CHAT_MINIMAP_DEMO_TURNS,
}: ChatMinimapExampleProps) {
const [start, setStart] = useState(initialStart);
const lastStart = turns.length - CHAT_MINIMAP_VISIBLE_TURNS;
const clampStart = (next: number) => Math.min(Math.max(next, 0), lastStart);
return (
<div
className={cn(
"flex w-full px-6 py-10",
side === "left" ? "justify-end" : "justify-start"
)}
>
<ChatMinimap side={side}>
<ChatMinimapNavButton
direction="previous"
disabled={start === 0}
onClick={() => setStart((current) => clampStart(current - 1))}
/>
{turns.map((turn, index) => (
<ChatMinimapItem
active={
index >= start && index < start + CHAT_MINIMAP_VISIBLE_TURNS
}
description={turn.description}
key={turn.id}
onClick={() => setStart(clampStart(index - 1))}
title={turn.title}
/>
))}
<ChatMinimapNavButton
direction="next"
disabled={start === lastStart}
onClick={() => setStart((current) => clampStart(current + 1))}
/>
</ChatMinimap>
</div>
);
}
$ bunx --bun shadcn@latest add @notra/chat-minimapEach line is a shadcn Button holding an SVG. The lines share one length at rest. The hovered line grows the most and its neighbours grow less the further away they are. Hovering a line opens a shadcn HoverCard with a Card inside. The up and down buttons stay hidden until they are hovered or focused, show all the time on touch screens, and use a shadcn Tooltip.
<ChatMinimap side="right">
<ChatMinimapNavButton direction="previous" onClick={previous} />
{turns.map((turn, index) => (
<ChatMinimapItem
active={index === activeIndex}
description={turn.preview}
key={turn.id}
onClick={() => scrollToTurn(index)}
title={turn.title}
/>
))}
<ChatMinimapNavButton direction="next" onClick={next} />
</ChatMinimap>
| Component | Prop | Default | Description |
|---|---|---|---|
ChatMinimap |
side |
"right" |
Side the preview card opens on. "left" also aligns the lines to the right. |
ChatMinimapItem |
title |
Card title and the line’s accessible name. | |
ChatMinimapItem |
description |
Card text, clamped to two lines. | |
ChatMinimapItem |
active |
false |
Highlights the line and sets data-active. |
ChatMinimapNavButton |
direction |
"previous" or "next". |
|
ChatMinimapNavButton |
label |
"Previous turn" or "Next turn" |
Tooltip text and aria-label. |
Left side
Set side="left" to put the rail on the right edge of a thread. The lines line up on the right and the card opens to the left.
import ChatMinimapExample from "./chat-minimap";
export default function ChatMinimapLeftExample() {
return <ChatMinimapExample side="left" />;
}
Long threads
The rail stays compact on long conversations. This one has 50 turns.
import { CHAT_MINIMAP_LONG_TURNS } from "../constants/chat-minimap";
import ChatMinimapExample from "./chat-minimap";
export default function ChatMinimapLongExample() {
return (
<ChatMinimapExample initialStart={20} turns={CHAT_MINIMAP_LONG_TURNS} />
);
}
Scroll spy
Lines light up for every turn in view as the thread scrolls. useChatMinimapScrollSpy watches the turns with an IntersectionObserver and returns the visible indexes:
const { rootRef, scrollToTurn, turnRef, visible } = useChatMinimapScrollSpy(turns.length);
<div className="overflow-y-auto" ref={rootRef}>
{turns.map((turn, index) => (
<section key={turn.id} ref={turnRef(index)}>…</section>
))}
</div>
<ChatMinimapItem active={visible.has(index)} onClick={() => scrollToTurn(index)} … />