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
18 changes: 18 additions & 0 deletions .changeset/fix-tailwind-prefix-scanning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"streamdown": patch
---

fix(tailwind): export STREAMDOWN_CLASSES and getSourceInline() for Tailwind v4 prefix support

When using Tailwind v4's `prefix()` option, the Tailwind scanner cannot match
unprefixed class names in streamdown's dist files to the prefixed utilities it
generates (e.g. it looks for `tw:flex` but dist files only contain `flex`).

This patch adds a new `streamdown/tailwind` entry point that exports:
- `STREAMDOWN_CLASSES` – a readonly array of every Tailwind utility class used
by streamdown and its official plugins
- `getSourceInline(prefix?)` – returns a ready-to-paste Tailwind v4
`@source inline(...)` directive with all classes, optionally prefixed

Users can now generate the correct `@source inline()` directive for their
prefix and add it to their CSS file.
22 changes: 9 additions & 13 deletions packages/remend/__tests__/broken-markdown-variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ describe("multiple incomplete links", () => {

it("should handle one complete and one incomplete link", () => {
const result = remend("[first](url1) and [second");
expect(result).toBe("[first](url1) and [second](streamdown:incomplete-link)");
expect(result).toBe(
"[first](url1) and [second](streamdown:incomplete-link)"
);
});

it("should handle nested incomplete brackets", () => {
Expand Down Expand Up @@ -250,9 +252,7 @@ describe("KaTeX with complex content", () => {
});

it("should close inline katex when enabled", () => {
expect(remend("$x^2 + y^2", { inlineKatex: true })).toBe(
"$x^2 + y^2$"
);
expect(remend("$x^2 + y^2", { inlineKatex: true })).toBe("$x^2 + y^2$");
});

it("should not treat currency as katex without inlineKatex", () => {
Expand Down Expand Up @@ -493,9 +493,7 @@ describe("disabled handlers via options", () => {
});

it("should close italic even when bold is disabled", () => {
expect(remend("**bold *italic", { bold: false })).toBe(
"**bold *italic*"
);
expect(remend("**bold *italic", { bold: false })).toBe("**bold *italic*");
});

it("should not close anything when all are disabled", () => {
Expand All @@ -512,9 +510,7 @@ describe("disabled handlers via options", () => {
it("should not close bold when asterisk in content blocks pattern (italic disabled)", () => {
// Bold pattern can't match when * appears in content; italic is disabled
// So nothing closes
expect(remend("**bold *italic", { italic: false })).toBe(
"**bold *italic"
);
expect(remend("**bold *italic", { italic: false })).toBe("**bold *italic");
});

it("should close strikethrough but not bold when bold is disabled", () => {
Expand Down Expand Up @@ -598,8 +594,8 @@ describe("real-world AI streaming patterns", () => {
});

it("should handle link with incomplete formatting after it", () => {
expect(
remend("[click here](https://example.com) for **more")
).toBe("[click here](https://example.com) for **more**");
expect(remend("[click here](https://example.com) for **more")).toBe(
"[click here](https://example.com) for **more**"
);
});
});
41 changes: 41 additions & 0 deletions packages/streamdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,47 @@ If you are already using shadcn/ui, these variables are set up automatically. If

You can also use the shadcn/ui [theme generator](https://ui.shadcn.com/themes) to create a custom palette and copy the generated CSS variables directly into your project.

### Tailwind v4 prefix

If you use Tailwind v4's `prefix()` option (e.g. `prefix(tw)`), Tailwind's
scanner cannot match unprefixed class names from Streamdown's dist files to the
prefixed utilities it generates. Use Tailwind's `@source inline()` directive
to supply the correct prefixed class list instead.

Streamdown exports a `getSourceInline` helper that generates the directive for
you. Run it once with a small build script and paste (or write) the output into
your CSS file:

```js
// scripts/gen-streamdown-source.js
import { getSourceInline } from "streamdown/tailwind";
// Also import helpers from installed plugins, e.g.:
// import { getSourceInline as codeSource } from "@streamdown/code/tailwind";

process.stdout.write(getSourceInline("tw") + "
");
```

```sh
node scripts/gen-streamdown-source.js >> app/globals.css
```

Or copy the generated line directly into your CSS:

```css
@import "tailwindcss" prefix(tw);

/* Replace 'tw' with your actual prefix: */
@source inline("tw:{absolute,animate-spin,appearance-none,...}");
```

You can also import the raw class array and build your own tooling:

```ts
import { STREAMDOWN_CLASSES } from "streamdown/tailwind";
// string[] of every Tailwind class used by streamdown
```

## Usage

Here's how you can use Streamdown in your React application with the AI SDK:
Expand Down
218 changes: 218 additions & 0 deletions packages/streamdown/lib/tailwind-classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* Complete list of Tailwind CSS utility classes used by streamdown and its
* official plugins (@streamdown/code, @streamdown/math, @streamdown/mermaid,
* @streamdown/cjk).
*
* Use this list to configure Tailwind v4's `@source inline()` directive when
* you need a custom prefix. See the README for details.
*/
export const STREAMDOWN_CLASSES: readonly string[] = [
Copy link
Copy Markdown
Contributor

@vercel vercel bot Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STREAMDOWN_CLASSES array is missing 6 Tailwind CSS utility classes ([&_thead]:sticky, [&_thead]:top-0, [&_thead]:z-10, overflow-auto, pt-0, underline) that are actively used in the source code, causing those styles to be absent when using Tailwind v4 prefix scanning.

Fix on Vercel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added 20 missing utility classes that are actively used in the source but were absent from the array:

-mt-10, [&>p]:inline, [&_svg]:h-auto, [&_svg]:w-auto, [counter-increment:line_0], [counter-reset:line], [li_&]:pl-6, before:content-[counter(line)], bg-[var(--sdm-bg,inherit], bg-[var(--sdm-tbg)], border, dark:bg-[var(--shiki-dark-bg,var(--sdm-bg,inherit)], dark:bg-[var(--shiki-dark-bg,var(--sdm-tbg))], dark:text-[var(--shiki-dark,var(--sdm-c,inherit))], list-decimal, list-disc, list-inside, rounded, sticky, text-[var(--sdm-c,inherit)]

"-mt-10",
"[&>p]:inline",
"[&_svg]:h-auto",
"[&_svg]:w-auto",
"[counter-increment:line_0]",
"[counter-reset:line]",
"[li_&]:pl-6",
"absolute",
"animate-spin",
"appearance-none",
"backdrop-blur-sm",
"before:[counter-increment:line]",
"before:content-[counter(line)]",
"before:font-mono",
"before:inline-block",
"before:mr-4",
"before:select-none",
"before:text-[13px]",
"before:text-muted-foreground/50",
"before:text-right",
"before:w-6",
"bg-[var(--sdm-bg,inherit]",
"bg-[var(--sdm-tbg)]",
"bg-background",
"bg-background/50",
"bg-background/80",
"bg-background/90",
"bg-background/95",
"bg-black/10",
"bg-muted",
"bg-muted/40",
"bg-muted/80",
"bg-primary",
"bg-red-100",
"bg-red-50",
"bg-sidebar",
"bg-sidebar/80",
"block",
"border",
"border-b",
"border-b-2",
"border-border",
"border-collapse",
"border-current",
"border-l-4",
"border-muted-foreground/30",
"border-red-200",
"border-sidebar",
"border-t",
"bottom-2",
"bottom-4",
"break-all",
"cursor-pointer",
"dark:bg-[var(--shiki-dark-bg,var(--sdm-bg,inherit)]",
"dark:bg-[var(--shiki-dark-bg,var(--sdm-tbg))]",
"dark:text-[var(--shiki-dark,var(--sdm-c,inherit))]",
"disabled:cursor-not-allowed",
"disabled:opacity-50",
"divide-border",
"divide-y",
"duration-150",
"duration-200",
"ease-out",
"fixed",
"flex",
"flex-1",
"flex-col",
"font-medium",
"font-mono",
"font-semibold",
"gap-1",
"gap-2",
"gap-4",
"group",
"group-hover:block",
"group-hover:opacity-100",
"h-4",
"h-8",
"h-[46px]",
"h-auto",
"h-full",
"hidden",
"hover:bg-background",
"hover:bg-muted",
"hover:bg-muted/40",
"hover:bg-primary/90",
"hover:text-foreground",
"inline-block",
"inset-0",
"italic",
"items-center",
"justify-between",
"justify-center",
"justify-end",
"left-2",
"left-4",
"list-decimal",
"list-disc",
"list-inside",
"lowercase",
"max-h-32",
"max-w-full",
"max-w-md",
"mb-2",
"min-h-28",
"min-h-[200px]",
"min-w-[120px]",
"ml-1",
"mt-1",
"mt-2",
"mt-6",
"mx-4",
"my-4",
"my-6",
"opacity-0",
"origin-center",
"overflow-hidden",
"overflow-x-auto",
"overflow-y-auto",
"overscroll-y-auto",
"p-1",
"p-1.5",
"p-2",
"p-3",
"p-4",
"p-6",
"pl-4",
"pointer-events-auto",
"pointer-events-none",
"px-1.5",
"px-3",
"px-4",
"py-0.5",
"py-1",
"py-2",
"relative",
"right-0",
"right-2",
"right-4",
"rounded",
"rounded-full",
"rounded-lg",
"rounded-md",
"rounded-xl",
"shadow-lg",
"shadow-sm",
"shrink-0",
"size-4",
"size-full",
"space-x-2",
"space-y-2",
"space-y-4",
"sticky",
"supports-[backdrop-filter]:backdrop-blur",
"supports-[backdrop-filter]:backdrop-blur-sm",
"supports-[backdrop-filter]:bg-background/70",
"supports-[backdrop-filter]:bg-sidebar/70",
"text-2xl",
"text-3xl",
"text-[var(--sdm-c,inherit)]",
"text-base",
"text-left",
"text-lg",
"text-muted-foreground",
"text-primary",
"text-primary-foreground",
"text-red-600",
"text-red-700",
"text-red-800",
"text-sm",
"text-xl",
"text-xs",
"top-2",
"top-4",
"top-full",
"transition-all",
"transition-colors",
"transition-transform",
"w-4",
"w-8",
"w-full",
"whitespace-normal",
"whitespace-nowrap",
"wrap-anywhere",
"z-10",
"z-50",
] as const;

/**
* Generates a Tailwind v4 `@source inline()` directive containing all
* streamdown classes, optionally prefixed.
*
* @param prefix - Tailwind v4 prefix string (e.g. `"tw"` for `prefix(tw)`).
* Omit or pass `undefined` when you are not using a prefix.
* @returns A ready-to-paste CSS `@source inline(...)` rule string.
*
* @example
* // In a postcss.config.js build script, write this string to a CSS file:
* import { getSourceInline } from 'streamdown/tailwind';
* console.log(getSourceInline('tw'));
* // → @source inline("tw:{absolute,animate-spin,...}");
*/
export function getSourceInline(prefix?: string): string {
const names = [...STREAMDOWN_CLASSES];
const body = prefix
? `${prefix}:{${names.join(",")}}`
: `{${names.join(",")}}`;
return `@source inline("${body}");`;
}
6 changes: 5 additions & 1 deletion packages/streamdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./styles.css": "./styles.css"
"./styles.css": "./styles.css",
"./tailwind": {
"types": "./dist/tailwind-classes.d.ts",
"import": "./dist/tailwind-classes.js"
}
},
"files": [
"dist",
Expand Down
2 changes: 1 addition & 1 deletion packages/streamdown/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from "tsup";

export default defineConfig({
dts: true,
entry: ["index.tsx"],
entry: ["index.tsx", "lib/tailwind-classes.ts"],
format: ["esm"],
minify: true,
outDir: "dist",
Expand Down
Loading