Conversation

free

Conversation keeps a growing entry stream anchored to its latest item until a reader deliberately scrolls away.

Preview
Dark

Entry 1

Project brief saved.

Entry 2

Jordan added a new research link.

Entry 3

Review notes are ready for the next pass.

Entry 4

Two open tasks were assigned to the design team.

Entry 5

The staging deployment completed successfully.

Entry 6

The activity feed now includes file changes.

Entry 7

A new comment was added to the timeline.

Entry 8

The weekly report is available for review.

import { useState } from 'react';
import Conversation from '@/components/composites/Conversation';
import Button from '@/components/ui/Button';

const initialEntries = [
  { id: '1', text: 'Project brief saved.' },
  { id: '2', text: 'Jordan added a new research link.' },
  { id: '3', text: 'Review notes are ready for the next pass.' },
  { id: '4', text: 'Two open tasks were assigned to the design team.' },
  { id: '5', text: 'The staging deployment completed successfully.' },
  { id: '6', text: 'The activity feed now includes file changes.' },
  { id: '7', text: 'A new comment was added to the timeline.' },
  { id: '8', text: 'The weekly report is available for review.' },
];

export default function UsageDemo() {
  const [entries, setEntries] = useState(initialEntries);

  return (
    <div className="space-y-2">
      <Button
        size="sm"
        onClick={() => {
          setEntries((current) => [
            ...current,
            {
              id: String(current.length + 1),
              text: `New entry ${current.length + 1} was added.`,
            },
          ]);
        }}
      >
        Add entry
      </Button>
      <div className="h-72 overflow-hidden rounded-card border border-border bg-card">
        <Conversation className="h-full">
          {entries.map((entry, index) => (
            <div
              key={entry.id}
              className="border-b border-border pb-3 last:border-b-0"
            >
              <p className="text-xs text-muted-foreground">
                Entry {index + 1}
              </p>
              <p className="mt-1 text-sm text-foreground">{entry.text}</p>
            </div>
          ))}
          <Conversation.ScrollButton />
        </Conversation>
      </div>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Conversation

Examples

Without Scroll Button

The scroll button is optional when a feed does not need a return-to-latest control.

Preview
Dark

Configuration updated.

A member joined the workspace.

The latest sync completed.

The status changed to ready.

import Conversation from '@/components/composites/Conversation';

const entries = [
  'Configuration updated.',
  'A member joined the workspace.',
  'The latest sync completed.',
  'The status changed to ready.',
];

export default function WithoutScrollButtonDemo() {
  return (
    <div className="h-64 overflow-hidden rounded-card border border-border bg-card">
      <Conversation className="h-full">
        {entries.map((entry) => (
          <div
            key={entry}
            className="border-b border-border pb-3 last:border-b-0"
          >
            <p className="text-sm text-foreground">{entry}</p>
          </div>
        ))}
      </Conversation>
    </div>
  );
}

API

Conversation

PropDescriptionTypeDefault
childrenEntries to render, plus an optional direct Conversation.ScrollButton child.ReactNode—
classNameClasses merged with the full-height conversation root.string—
...divPropsStandard <div> attributes forwarded to the conversation root.ComponentProps<'div'>—

Conversation.ScrollButton

PropDescriptionTypeDefault
aria-labelAccessible name for the return-to-latest control.string'Scroll to bottom'
classNameClasses merged with the positioned circular button.string—
disabledPrevents the control from scrolling.booleanfalse
...buttonPropsStandard <button> attributes forwarded to the control.ComponentProps<'button'>—