Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-admonition-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"streamdown": minor
---

Add GitHub-style admonition (GFM Alerts) support with `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, and `> [!CAUTION]` syntax. Includes built-in icons, colored styles, and full customization via components, icons, and translations props.
6 changes: 6 additions & 0 deletions .changeset/extract-remark-gfm-admonition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"remark-gfm-admonition": minor
"streamdown": patch
---

Extract admonition remark plugin into standalone `remark-gfm-admonition` package with GitHub-compatible HTML output. Streamdown now consumes this package internally.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ Visit [Streamdown on GitHub](https://github.com/haydenbleasel/streamdown) or pas

---

## Admonitions

> [!NOTE]
> Highlights information that users should take into account.

> [!TIP]
> Optional information to help a user be more successful.

> [!IMPORTANT]
> Crucial information necessary for users to succeed.

> [!WARNING]
> Critical content demanding immediate user attention due to potential risks.

> [!CAUTION]
> Negative potential consequences of an action.

---

## Lists

### Unordered Lists
Expand Down
169 changes: 169 additions & 0 deletions apps/website/content/docs/admonitions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: Admonitions
description: Render GitHub-style alert callouts with built-in icons, colors, and full customization.
type: guide
summary: Display note, tip, important, warning, and caution callout boxes from standard GFM alert syntax.
prerequisites:
- /docs/getting-started
related:
- /docs/gfm
- /docs/components
- /docs/internationalization
- /docs/styling
---

Admonitions are callout boxes that highlight important information using GitHub-style alert syntax. Streamdown renders them with distinct icons, colors, and titles for each type.

## Syntax

Use the `> [!TYPE]` blockquote syntax from GitHub Flavored Markdown:

```markdown
> [!NOTE]
> Useful background information.

> [!TIP]
> Helpful advice for getting the most out of a feature.

> [!IMPORTANT]
> Key details you need to know.

> [!WARNING]
> Potential issues to watch out for.

> [!CAUTION]
> Negative consequences of an action.
```

## Supported types

Streamdown supports all five GFM alert types:

| Type | Purpose |
|------|---------|
| `NOTE` | Background context or supplementary information |
| `TIP` | Helpful suggestions and best practices |
| `IMPORTANT` | Critical details the reader must not miss |
| `WARNING` | Potential problems or things to watch for |
| `CAUTION` | Actions that could cause data loss or breaking changes |

Each type renders with a unique icon and color scheme that matches GitHub's visual styling.

## Multi-line content

Admonitions support rich Markdown content inside them, including bold, code, links, and lists:

```markdown
> [!TIP]
> You can use **bold text**, `inline code`, and [links](https://example.com) inside admonitions.
>
> - Lists work too
> - Including nested content
```

## Customizing the component

Override the default admonition rendering by passing a custom component:

```tsx title="app/page.tsx"
const MyAdmonition = ({ children, "data-admonition-type": type, ...props }) => (
<div className={`my-admonition my-admonition-${type}`} {...props}>
{children}
</div>
);

<Streamdown components={{ admonition: MyAdmonition }}>
{markdown}
</Streamdown>
```

The component receives `data-admonition-type` as a prop with the lowercase type string (`"note"`, `"tip"`, `"important"`, `"warning"`, or `"caution"`).

<Callout type="warn">
Custom components **fully replace** the default implementation, including the built-in icon, title bar, and styles. See the [Components](/docs/components) documentation for details on component overrides.
</Callout>

## Customizing icons

Replace individual admonition icons through the `icons` prop:

```tsx title="app/page.tsx"
import { InfoIcon } from "lucide-react";

<Streamdown icons={{ AdmonitionNoteIcon: InfoIcon }}>
{markdown}
</Streamdown>
```

Available icon keys:

- `AdmonitionNoteIcon`
- `AdmonitionTipIcon`
- `AdmonitionImportantIcon`
- `AdmonitionWarningIcon`
- `AdmonitionCautionIcon`

## Customizing title text (i18n)

Override the title text displayed in admonition headers using the `translations` prop:

```tsx title="app/page.tsx"
<Streamdown
translations={{
admonitionNote: "Nota",
admonitionTip: "Consejo",
admonitionImportant: "Importante",
admonitionWarning: "Advertencia",
admonitionCaution: "Precaucion",
}}
>
{markdown}
</Streamdown>
```

See the [Internationalization](/docs/internationalization) documentation for the full list of translation keys.

## Styling with CSS

Target admonitions using `data-streamdown` and `data-admonition-type` attribute selectors:

```css title="styles/streamdown.css"
/* Style all admonitions */
[data-streamdown="admonition"] {
border-radius: 0.5rem;
}

/* Style a specific type */
[data-streamdown="admonition"][data-admonition-type="note"] {
background-color: #dbeafe;
border-left: 4px solid #3b82f6;
}

/* Style the title bar */
[data-streamdown="admonition-title"] {
font-weight: 600;
}

/* Style the content area */
[data-streamdown="admonition-content"] {
padding: 0.75rem 1rem;
}
```

See the [Styling](/docs/styling) documentation for more on CSS customization.

## Disabling admonitions

To disable admonition rendering, provide custom `remarkPlugins` that exclude the admonition plugin:

```tsx title="app/page.tsx"
import { Streamdown, defaultRemarkPlugins } from "streamdown";

const { admonition, ...remainingPlugins } = defaultRemarkPlugins;

<Streamdown remarkPlugins={Object.values(remainingPlugins)}>
{markdown}
</Streamdown>
```

With admonitions disabled, `> [!NOTE]` blocks render as standard blockquotes.
1 change: 1 addition & 0 deletions apps/website/content/docs/components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ You can override any of the following standard HTML components:
- **Links**: `a`
- **Code**: `code`, `pre`
- **Quotes**: `blockquote`
- **Admonitions**: `admonition`
- **Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`
- **Media**: `img`
- **Other**: `hr`, `sup`, `sub`, `section`
Expand Down
4 changes: 4 additions & 0 deletions apps/website/content/docs/gfm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,7 @@ import { Streamdown, defaultRemarkPlugins } from "streamdown";
{markdown}
</Streamdown>
```

## Admonitions (GFM Alerts)

Streamdown also supports GitHub-style alert callouts using the `> [!TYPE]` syntax. See the [Admonitions](/docs/admonitions) page for details.
34 changes: 33 additions & 1 deletion apps/website/content/docs/internationalization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ Pass a partial translations object to replace specific labels:

## Available translation keys

### Admonition

<TypeTable
type={{
admonitionCaution: {
description: "Title text for caution admonitions",
type: "string",
default: '"Caution"',
},
admonitionImportant: {
description: "Title text for important admonitions",
type: "string",
default: '"Important"',
},
admonitionNote: {
description: "Title text for note admonitions",
type: "string",
default: '"Note"',
},
admonitionTip: {
description: "Title text for tip admonitions",
type: "string",
default: '"Tip"',
},
admonitionWarning: {
description: "Title text for warning admonitions",
type: "string",
default: '"Warning"',
},
}}
/>

### Code block

<TypeTable
Expand Down Expand Up @@ -248,6 +280,6 @@ import type { StreamdownTranslations } from "streamdown";
import { defaultTranslations, useTranslations } from "streamdown";
```

- `StreamdownTranslations` — interface with all 37 translation keys
- `StreamdownTranslations` — interface with all 42 translation keys
- `defaultTranslations` — the built-in English defaults
- `useTranslations()` — React hook returning the active translations object
108 changes: 108 additions & 0 deletions packages/remark-gfm-admonition/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import rehypeStringify from "rehype-stringify";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
import { describe, expect, it } from "vitest";
import remarkGfmAdmonition from "../index";

function processMarkdown(md: string): string {
return unified()
.use(remarkParse)
.use(remarkGfmAdmonition)
.use(remarkRehype)
.use(rehypeStringify)
.processSync(md)
.toString();
}

describe("remarkGfmAdmonition", () => {
it("should transform [!NOTE] blockquote to github-compatible HTML", () => {
const result = processMarkdown("> [!NOTE]\n> This is a note.");
expect(result).toContain('class="markdown-alert markdown-alert-note"');
expect(result).toContain('class="markdown-alert-title"');
expect(result).toContain("Note");
expect(result).toContain("This is a note.");
});

it("should support all 5 admonition types", () => {
const types = [
{ input: "NOTE", title: "Note", cls: "markdown-alert-note" },
{ input: "TIP", title: "Tip", cls: "markdown-alert-tip" },
{
input: "IMPORTANT",
title: "Important",
cls: "markdown-alert-important",
},
{ input: "WARNING", title: "Warning", cls: "markdown-alert-warning" },
{ input: "CAUTION", title: "Caution", cls: "markdown-alert-caution" },
];

for (const { input, title, cls } of types) {
const result = processMarkdown(`> [!${input}]\n> Content`);
expect(result).toContain(cls);
expect(result).toContain(title);
}
});

it("should preserve normal blockquotes", () => {
const result = processMarkdown("> This is a normal quote.");
expect(result).not.toContain("markdown-alert");
expect(result).toContain("<blockquote>");
});

it("should be case insensitive", () => {
for (const variant of ["[!note]", "[!Note]", "[!NOTE]"]) {
const result = processMarkdown(`> ${variant}\n> Content`);
expect(result).toContain("markdown-alert-note");
}
});

it("should leave invalid types as normal blockquotes", () => {
const result = processMarkdown("> [!INVALID]\n> Content");
expect(result).not.toContain("markdown-alert");
expect(result).toContain("<blockquote>");
});

it("should handle empty body", () => {
const result = processMarkdown("> [!NOTE]");
expect(result).toContain("markdown-alert-note");
expect(result).toContain("Note");
});

it("should preserve inline text after marker", () => {
const result = processMarkdown("> [!NOTE] Some inline text");
expect(result).toContain("markdown-alert-note");
expect(result).toContain("Some inline text");
});

it("should handle multi-line content", () => {
const result = processMarkdown("> [!NOTE]\n> Line 1\n> Line 2");
expect(result).toContain("Line 1");
expect(result).toContain("Line 2");
});

it("should use div wrapper, not blockquote", () => {
const result = processMarkdown("> [!NOTE]\n> Content");
expect(result).not.toContain("<blockquote>");
expect(result).toContain("<div");
});

it("should render title as first child paragraph", () => {
const result = processMarkdown("> [!WARNING]\n> Be careful!");
const titleIndex = result.indexOf("markdown-alert-title");
const contentIndex = result.indexOf("Be careful!");
expect(titleIndex).toBeLessThan(contentIndex);
});

it("should not treat marker on non-first line as admonition", () => {
const result = processMarkdown("> Some text\n> [!NOTE]");
expect(result).not.toContain("markdown-alert");
expect(result).toContain("<blockquote>");
});

it("should transform nested blockquote with marker", () => {
const result = processMarkdown("> > [!NOTE]\n> > Nested note");
expect(result).toContain("markdown-alert-note");
expect(result).toContain("Nested note");
});
});
Loading
Loading