@ashwin_droid/notion-stream
v1.2.0
Published
React component that converts streaming markdown into editable Notion-like blocks with real-time rendering
Maintainers
Readme
@ashwin_droid/notion-stream
A React component that converts streaming markdown into editable Notion-like blocks with real-time rendering. Perfect for AI chat interfaces, collaborative editors, and any application that needs to display and edit markdown content in a beautiful, block-based format.
Features
- Streaming Support: Renders markdown in real-time as it streams in (perfect for LLM responses)
- Notion-like Editing: Click-to-edit blocks with familiar keyboard shortcuts
- Rich Block Types: Paragraphs, headings (H1-H6), lists, blockquotes, code blocks, tables, images, math (LaTeX), Mermaid diagrams, and dividers
- Inline Formatting: Bold, italic, strikethrough, inline code, links, and inline math
- Slash Commands: Type
/to quickly insert any block type - Markdown Shortcuts: Type
#,-,1.,>,```,---etc. to convert blocks - Dark Mode: Built-in light/dark theme support with system preference detection
- Diff Review: Built-in utilities for comparing and reviewing markdown changes
- Fully Typed: Complete TypeScript support with exported types
- Customizable: Hooks for custom block rendering and slash commands
Installation
npm install @ashwin_droid/notion-stream
# or
yarn add @ashwin_droid/notion-stream
# or
pnpm add @ashwin_droid/notion-streamQuick Start
import { NotionStream } from '@ashwin_droid/notion-stream'
import '@ashwin_droid/notion-stream/styles.css'
function App() {
const [markdown, setMarkdown] = useState('# Hello World\n\nStart typing...')
return (
<NotionStream
markdown={markdown}
onMarkdownChange={setMarkdown}
/>
)
}Usage with Streaming (AI Chat)
import { NotionStream } from '@ashwin_droid/notion-stream'
import '@ashwin_droid/notion-stream/styles.css'
function ChatMessage({ stream }: { stream: AsyncIterable<string> }) {
const [markdown, setMarkdown] = useState('')
const [isStreaming, setIsStreaming] = useState(true)
useEffect(() => {
async function consume() {
for await (const chunk of stream) {
setMarkdown(prev => prev + chunk)
}
setIsStreaming(false)
}
consume()
}, [stream])
return (
<NotionStream
markdown={markdown}
isStreaming={isStreaming}
onMarkdownChange={setMarkdown}
/>
)
}Props
NotionStreamProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| markdown | string | required | The markdown content to render |
| isStreaming | boolean | false | Whether content is currently streaming in |
| onMarkdownChange | (markdown: string) => void | - | Callback when content is edited |
| className | string | '' | Additional CSS classes |
| theme | 'light' \| 'dark' \| 'system' | auto-detect | Theme mode |
| readonly | boolean | false | Disable editing (viewer mode) |
| hooks | BlockHooks | - | Custom block hooks (see below) |
Block Types
NotionStream supports the following block types:
| Block | Markdown Syntax | Description |
|-------|----------------|-------------|
| Paragraph | Plain text | Default text block |
| Heading 1-6 | # to ###### | Section headings |
| Bulleted List | - or * | Unordered list items |
| Numbered List | 1. | Ordered list items |
| Blockquote | > | Quote blocks |
| Code Block | ``` | Syntax-highlighted code |
| Math Block | $$ | LaTeX equations (KaTeX) |
| Mermaid | ```mermaid | Diagrams and flowcharts |
| Table | \| ... \| | Markdown tables |
| Image |  | Image blocks |
| Divider | --- | Horizontal rule |
Inline Formatting
| Style | Markdown | Result |
|-------|----------|--------|
| Bold | **text** | text |
| Italic | *text* or _text_ | text |
| Strikethrough | ~~text~~ | ~~text~~ |
| Inline Code | `code` | code |
| Link | [text](url) | text |
| Inline Math | $x^2$ | Rendered LaTeX |
Keyboard Shortcuts
Block Operations
Enter- Create new block below (in paragraphs/lists)Backspaceat start - Delete empty block or merge with previousTab/Shift+Tab- Indent/outdent list items/on empty block - Open slash command menu
Markdown Shortcuts (type at start of empty block)
#- Heading 1##- Heading 2###- Heading 3-or*- Bulleted list1.- Numbered list>- Blockquote```- Code block---- Divider
Ref API
Access the imperative API using a ref:
import { useRef } from 'react'
import { NotionStream, NotionStreamRef } from '@ashwin_droid/notion-stream'
function Editor() {
const ref = useRef<NotionStreamRef>(null)
const addCodeBlock = () => {
ref.current?.insertBlockAfter(null, {
type: 'code',
language: 'typescript',
content: '// Your code here'
})
}
return (
<>
<button onClick={addCodeBlock}>Add Code Block</button>
<NotionStream ref={ref} markdown="" />
</>
)
}NotionStreamRef Methods
| Method | Signature | Description |
|--------|-----------|-------------|
| updateBlock | (blockId: string, value: string \| Span[]) => void | Update block content |
| getBlock | (blockId: string) => Block \| null | Get block by ID |
| getAllBlocks | () => Block[] | Get all blocks |
| getMarkdown | () => string | Get current markdown |
| insertBlockAfter | (blockId: string \| null, block: BlockInsert) => string | Insert new block |
| deleteBlock | (blockId: string) => void | Delete a block |
| setBlockType | (blockId: string, type: BlockType, options?) => void | Change block type |
| indentListItem | (blockId: string) => void | Indent list item |
| outdentListItem | (blockId: string) => void | Outdent list item |
Custom Block Rendering
Use hooks to customize block rendering:
import { NotionStream, BlockHooks, Block } from '@ashwin_droid/notion-stream'
const hooks: BlockHooks = {
renderBlock: (block: Block, defaultRender: () => React.ReactNode) => {
// Custom rendering for specific blocks
if (block.type === 'code' && block.language === 'custom') {
return <MyCustomCodeBlock code={block.content} />
}
// Fall back to default rendering
return defaultRender()
},
onBlockChange: (blockId: string, newValue: string | Span[], block: Block) => {
console.log('Block changed:', blockId, newValue)
}
}
function Editor() {
return <NotionStream markdown="" hooks={hooks} />
}Custom Slash Commands
Add your own slash commands:
const hooks: BlockHooks = {
getSlashCommands: ({ block }) => [
{
id: 'callout',
title: 'Callout',
description: 'Highlighted callout box',
group: 'Custom',
aliases: ['info', 'warning'],
icon: <InfoIcon />,
execute: (api, { blockId }) => {
api.setBlockType(blockId, 'blockquote')
api.updateBlock(blockId, [{ text: 'Callout content', style: 'plain' }])
}
}
]
}Diff Review Utilities
Compare and review markdown changes:
import { useDiffReview } from '@ashwin_droid/notion-stream'
function DiffReviewer({ original, modified }: { original: string; modified: string }) {
const {
items, // Array of diff items
decisions, // Current accept/reject decisions
accept, // Accept a change
reject, // Reject a change
resultMarkdown, // Final markdown after applying decisions
stats // { added, removed, modified, accepted, rejected, pending }
} = useDiffReview({
baseMarkdown: original,
nextMarkdown: modified,
defaultDecision: 'unset'
})
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.type === 'added' && <span className="text-green-500">+ {item.markdown}</span>}
{item.type === 'removed' && <span className="text-red-500">- {item.markdown}</span>}
{item.type !== 'unchanged' && (
<>
<button onClick={() => accept(item.id)}>Accept</button>
<button onClick={() => reject(item.id)}>Reject</button>
</>
)}
</div>
))}
</div>
)
}Diff Utilities
| Function | Description |
|----------|-------------|
| computeDiff(a, b) | Word-level diff between strings |
| computeCodeDiff(a, b) | Line-level diff for code with inline highlights |
| computeMarkdownBlockDiff(a, b) | Block-level markdown diff |
| computeMarkdownDiffReviewItems(a, b) | Structured diff for review UI |
| applyMarkdownDiffReview(items, decisions) | Apply review decisions to get final markdown |
Low-Level Hooks
For advanced use cases, individual hooks are exported:
import {
useStreamParser, // Parse markdown into blocks
useBlockState, // Manage block state
useMarkdownShortcuts, // Handle markdown shortcuts
useDiffReview // Diff review utilities
} from '@ashwin_droid/notion-stream'Span Utilities
Work with inline formatting:
import {
parseMarkdownToSpans, // Parse inline markdown to spans
spansToMarkdown, // Convert spans back to markdown
spansToHtml, // Convert spans to HTML
renderSpans, // Render spans as React nodes
applyStyleToRange, // Apply style to text range
toggleStyle // Toggle inline style
} from '@ashwin_droid/notion-stream'Block Components
Individual block components are exported for custom layouts:
import {
ParagraphBlock,
HeadingBlock,
CodeBlock,
ImageBlock,
MathBlock,
MermaidBlock,
ListBlock,
BlockquoteBlock,
DividerBlock
} from '@ashwin_droid/notion-stream'Theming
Auto Theme Detection
By default, NotionStream detects the theme from:
.darkclass on<html>or<body>elements- System preference via
prefers-color-scheme
Explicit Theme
<NotionStream markdown="" theme="dark" />
<NotionStream markdown="" theme="light" />
<NotionStream markdown="" theme="system" />CSS Variables
Customize colors using CSS variables:
.notion-stream {
--notion-bg: #ffffff;
--notion-text: #1f2937;
--notion-text-secondary: #6b7280;
--notion-border: #e5e7eb;
/* ... see styles.css for all variables */
}
.notion-stream.dark {
--notion-bg: #1f2937;
--notion-text: #f3f4f6;
/* ... */
}TypeScript Types
All types are exported:
import type {
Block,
BlockType,
InlineBlock,
Span,
InlineStyle,
ListType,
BlockInsert,
SlashCommand,
NotionStreamProps,
NotionStreamRef,
BlockHooks,
BlockProps,
Theme,
TableAlign,
TableBlock,
DiffSegment,
CodeDiff,
MarkdownDiffReviewItem,
DiffReviewDecision
} from '@ashwin_droid/notion-stream'Peer Dependencies
This package requires:
react>= 18.0.0react-dom>= 18.0.0streamdown>= 2.0.0@streamdown/code>= 1.0.0@streamdown/math>= 1.0.0 (includes katex)@streamdown/mermaid>= 1.0.0tailwindcss>= 3.0.0@radix-ui/react-scroll-area>= 1.0.0
npm install react react-dom streamdown @streamdown/code @streamdown/math @streamdown/mermaid tailwindcss @radix-ui/react-scroll-areaNote: You must also import the KaTeX CSS for math rendering:
import 'katex/dist/katex.min.css'Dependencies
NotionStream uses:
- nanoid - Unique ID generation
Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Acknowledgments
Inspired by Notion's block-based editing experience.
