Skip to content

Commit 47670de

Browse files
diegomuraclaude
andauthored
feat(site): add Tailwind docs page and playground example (#3541)
Adds a Tailwind docs page with a live playground example, and lets playground code import `@react-pdf/tailwind` as an optional lazy module in `@react-pdf/ui`, alongside math and mermaid. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5c621d9 commit 47670de

9 files changed

Lines changed: 236 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@react-pdf/ui': patch
3+
---
4+
5+
Allow playground code to import `@react-pdf/tailwind`, loaded lazily like the other optional packages

apps/site/content/docs/v4/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"styling",
2525
"floats",
2626
"fonts",
27+
"tailwind",
2728
"---SVG---",
2829
"svg/svg",
2930
"svg/line",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
title: 'Tailwind'
3+
---
4+
5+
The `@react-pdf/tailwind` package converts a compatible subset of the Tailwind CSS class syntax into style objects that react-pdf understands.
6+
7+
## Installation
8+
9+
```bash
10+
npm install @react-pdf/tailwind
11+
```
12+
13+
## Usage
14+
15+
```jsx
16+
import { Document, Page, Text, View } from '@react-pdf/renderer';
17+
import { createTw } from '@react-pdf/tailwind';
18+
19+
// Apply your own styles on top of Tailwind defaults
20+
const tw = createTw({
21+
fontFamily: {
22+
sans: ['Papyrus'],
23+
},
24+
colors: {
25+
custom: '#bada55',
26+
},
27+
});
28+
29+
const MyDocument = () => (
30+
<Document>
31+
<Page size="A4" style={tw('p-12 font-sans')}>
32+
<View style={tw('p-20 bg-gray-100')}>
33+
<Text style={tw('text-custom text-3xl')}>Section #1</Text>
34+
</View>
35+
<View style={tw('mt-12 px-8 rotate-2')}>
36+
<Text style={tw('text-amber-600 text-2xl')}>Section #2</Text>
37+
</View>
38+
</Page>
39+
</Document>
40+
);
41+
```
42+
43+
<Example name="tailwind" />
44+
45+
The returned `tw` function takes a space-separated class string and returns a react-pdf `Style` object. Unknown classes are skipped with a console warning, emitted once per distinct class.
46+
47+
## createTw
48+
49+
`createTw(config, options)` builds the `tw` function. `config` is a theme object merged into Tailwind's `defaultTheme`, following the Tailwind v4 theme shape — see [Tailwind's default theme](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/compat/default-theme.ts) for reference.
50+
51+
```js
52+
const tw = createTw(
53+
{
54+
fontFamily: {
55+
sans: ['Papyrus'],
56+
},
57+
spacing: {
58+
verybig: '999rem',
59+
},
60+
colors: {
61+
custom: '#bada55',
62+
},
63+
},
64+
{
65+
// Base font size in points. Defaults to 12.
66+
ptPerRem: 12,
67+
},
68+
);
69+
```
70+
71+
Scales merge one level deep, so overriding a single key keeps the rest of the default scale — `spacing: { 4: '2rem' }` changes `p-4` while leaving `p-8` alone, and `colors: { gray: { 500: '#fff' } }` leaves the other grays intact. Replace a whole scale by overriding it with a non-object value.
72+
73+
`fontFamily` is the exception: it comes from your config alone, neither merging with Tailwind's defaults nor falling back to them. react-pdf can only draw [fonts you have registered](/docs/v4/fonts), and Tailwind's stacks name web families like `-apple-system`, so resolving `font-sans` against them would throw at render time. Register a font, map it in the config, and `font-<key>` works; without a config, `font-sans` / `font-serif` / `font-mono` warn as unsupported while `font-bold` and friends still resolve.
74+
75+
## Color opacity
76+
77+
`bg-red-500/50` and friends work anywhere a color does — `bg-`, `text-`, `border-`, `decoration-` — including black, white, custom and arbitrary colors:
78+
79+
```js
80+
tw('bg-red-500/50'); // { backgroundColor: '#ef444480' }
81+
tw('text-black/25'); // { color: '#00000040' }
82+
tw('bg-[#bada55]/60'); // { backgroundColor: '#bada5599' }
83+
```
84+
85+
A bare suffix is a percentage; a bracketed one is `0``1` unless it carries a `%`, so `/[0.55]` and `/[55%]` agree. `transparent`, `currentColor` and `inherit` name no channel to modulate and reject the suffix.
86+
87+
## Variants
88+
89+
Breakpoint and orientation variants become react-pdf media queries, which resolve against the **page box** rather than a viewport:
90+
91+
```js
92+
tw('p-2 lg:p-4 landscape:p-6');
93+
// {
94+
// padding: 6,
95+
// '@media min-width: 768': { padding: 12 },
96+
// '@media orientation: landscape': { padding: 18 },
97+
// }
98+
```
99+
100+
| Variant | Becomes |
101+
| ------------------------------ | ----------------------- |
102+
| `sm:` `md:` `lg:` `xl:` `2xl:` | `@media min-width: N` |
103+
| `max-sm:``max-2xl:` | `@media max-width: N` |
104+
| `min-[600px]:` `max-[40rem]:` | the width you give it |
105+
| `portrait:` `landscape:` | `@media orientation: …` |
106+
| stacked, e.g. `lg:portrait:` | both, joined with `and` |
107+
108+
Tailwind v4 states its breakpoints in rem, so at the default `1rem = 12pt` they land at page scale: `sm` is 480pt, `md` 576pt, `lg` 768pt, `xl` 960pt. An A4 page is 595pt wide upright and 842pt on its side, so `md` matches portrait and `lg` matches landscape. Set `screens` in the config to choose your own.
109+
110+
State variants — `hover:`, `focus:`, `dark:`, `group-*`, `peer-*` — describe something a printed page never enters, and are reported as unsupported rather than applied. Applying them would bake the hover style into the output.
111+
112+
## Notes
113+
114+
- Supports the CSS properties that make sense in a PDF context and are supported by react-pdf — see [valid CSS properties](/docs/v4/styling#valid-css-properties).
115+
- Uses `pt` as the internal unit ([valid units](/docs/v4/styling#valid-units)), with `1rem = 12pt` by default. Change it with `ptPerRem`.
116+
- react-pdf uses [Yoga](https://yogalayout.dev/) for layout, so some defaults differ from the web — `flex-direction` defaults to `column`, for example. Add `flex-row` where you need it.
117+
- Line heights are emitted unitless, since react-pdf only supports unitless `lineHeight`.
118+
- `aspect-auto` and `line-clamp-none` warn as unsupported. react-pdf has no style value meaning "no aspect ratio" or "no clamp" — leaving the utility off is the reset.
119+
- Intrinsic sizing (`w-fit`, `h-min`, `max-w-max`, …), `max-w-none` / `max-h-none`, and lengths in units react-pdf can't parse (`max-w-prose` is `65ch`) warn as unsupported. Yoga has no equivalent, and passing the value through would throw while laying out the document.
120+
- `float-*` and `clear-*` map to react-pdf's [float support](/docs/v4/floats), which is newer and has rough edges: setting `lineHeight` on floated content breaks text wrap, and parents don't grow to contain their floats.

apps/site/lib/examples/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import shape_outside from './shape-outside';
3939
import styles from './styles';
4040
import svg from './svg';
4141
import svgtext from './svgtext';
42+
import tailwind from './tailwind';
4243
import text from './text';
4344
import textinput from './textinput';
4445

@@ -84,6 +85,7 @@ export const examples: Record<string, string> = {
8485
styles: styles,
8586
svg: svg,
8687
svgtext: svgtext,
88+
tailwind: tailwind,
8789
text: text,
8890
textinput: textinput,
8991
};
@@ -98,7 +100,7 @@ const RULES: [string, RegExp][] = [
98100
['Text & fonts', /text|font|hyphenation|emoji/],
99101
['Layout & pagination', /page|break|orphans|widows|fixed|float|shape|wrap/],
100102
['Images', /image/],
101-
['Advanced', /math|resume|knobs/],
103+
['Advanced', /math|resume|knobs|tailwind/],
102104
];
103105

104106
const FALLBACK = 'Essentials';

apps/site/lib/examples/tailwind.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const tailwind = `import { createTw } from '@react-pdf/tailwind';
2+
3+
// Tailwind's default font stacks name web families, so map the ones you use to
4+
// fonts react-pdf can draw
5+
const tw = createTw({
6+
fontFamily: {
7+
sans: ['Helvetica'],
8+
},
9+
colors: {
10+
brand: '#4f46e5',
11+
},
12+
});
13+
14+
const Row = ({ item, qty, price, muted }) => (
15+
<View style={tw(\`flex-row py-2 border-b \${muted ? 'border-gray-100' : 'border-gray-200'}\`)}>
16+
<Text style={tw('flex-1 text-[10px] text-gray-800')}>{item}</Text>
17+
<Text style={tw('w-16 text-[10px] text-right text-gray-500')}>{qty}</Text>
18+
<Text style={tw('w-20 text-[10px] text-right text-gray-800')}>{price}</Text>
19+
</View>
20+
);
21+
22+
const doc = (
23+
<Document>
24+
<Page size="A4" style={tw('bg-white font-sans p-10 landscape:p-16')}>
25+
<View style={tw('flex-row justify-between items-start pb-6 border-b-2 border-brand')}>
26+
<View>
27+
<Text style={tw('text-2xl font-bold text-gray-900')}>Invoice</Text>
28+
<Text style={tw('mt-1 text-[10px] text-gray-400')}>#2026-0042</Text>
29+
</View>
30+
<View style={tw('items-end')}>
31+
<Text style={tw('text-[10px] text-gray-500')}>Issued 28 Aug 2026</Text>
32+
<Text style={tw('mt-1 text-[10px] text-gray-500')}>Due 27 Sep 2026</Text>
33+
</View>
34+
</View>
35+
36+
<View style={tw('flex-row gap-4 mt-6')}>
37+
<View style={tw('flex-1 rounded-lg bg-brand/5 p-4')}>
38+
<Text style={tw('text-[8px] uppercase tracking-wide text-brand')}>Billed to</Text>
39+
<Text style={tw('mt-2 text-xs text-gray-900')}>Acme Corporation</Text>
40+
<Text style={tw('mt-1 text-[10px] leading-normal text-gray-500')}>
41+
120 Fifth Avenue{'\\n'}New York, NY 10011
42+
</Text>
43+
</View>
44+
<View style={tw('flex-1 rounded-lg bg-gray-50 p-4')}>
45+
<Text style={tw('text-[8px] uppercase tracking-wide text-gray-400')}>From</Text>
46+
<Text style={tw('mt-2 text-xs text-gray-900')}>react-pdf studio</Text>
47+
<Text style={tw('mt-1 text-[10px] leading-normal text-gray-500')}>
48+
Rendered with @react-pdf/tailwind
49+
</Text>
50+
</View>
51+
</View>
52+
53+
<View style={tw('mt-8')}>
54+
<View style={tw('flex-row border-b border-gray-300 pb-2')}>
55+
<Text style={tw('flex-1 text-[8px] uppercase tracking-wide text-gray-400')}>Description</Text>
56+
<Text style={tw('w-16 text-[8px] uppercase tracking-wide text-right text-gray-400')}>Qty</Text>
57+
<Text style={tw('w-20 text-[8px] uppercase tracking-wide text-right text-gray-400')}>Amount</Text>
58+
</View>
59+
<Row item="Design system audit" qty="1" price="$2,400.00" />
60+
<Row item="Component library" qty="12" price="$4,800.00" muted />
61+
<Row item="PDF templates" qty="6" price="$1,800.00" />
62+
<Row item="Support retainer" qty="3" price="$900.00" muted />
63+
</View>
64+
65+
<View style={tw('mt-6 flex-row justify-end')}>
66+
<View style={tw('w-52')}>
67+
<View style={tw('flex-row justify-between')}>
68+
<Text style={tw('text-[10px] text-gray-500')}>Subtotal</Text>
69+
<Text style={tw('text-[10px] text-gray-800')}>$9,900.00</Text>
70+
</View>
71+
<View style={tw('mt-1 flex-row justify-between')}>
72+
<Text style={tw('text-[10px] text-gray-500')}>Tax (8.875%)</Text>
73+
<Text style={tw('text-[10px] text-gray-800')}>$878.63</Text>
74+
</View>
75+
<View style={tw('mt-3 flex-row justify-between rounded-md bg-brand p-3')}>
76+
<Text style={tw('text-xs font-bold text-white')}>Total</Text>
77+
<Text style={tw('text-xs font-bold text-white')}>$10,778.63</Text>
78+
</View>
79+
</View>
80+
</View>
81+
82+
<Text style={tw('mt-auto text-center text-[8px] text-gray-400')}>
83+
Styled entirely with Tailwind classes, no StyleSheet in sight
84+
</Text>
85+
</Page>
86+
</Document>
87+
);
88+
89+
ReactPDF.render(doc);`;
90+
91+
export default tailwind;

apps/site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@react-pdf/math": "^5.0.1",
2121
"@react-pdf/mermaid": "^5.0.1",
2222
"@react-pdf/renderer": "^4.8.0",
23+
"@react-pdf/tailwind": "^0.1.0",
2324
"@react-pdf/ui": "^0.1.0",
2425
"@uiw/react-codemirror": "^4.25.11",
2526
"codemirror": "^6.0.2",

apps/site/yarn.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,14 @@
11691169
bidi-js "^1.0.2"
11701170
unicode-properties "^1.4.1"
11711171

1172+
"@react-pdf/tailwind@^0.1.0":
1173+
version "0.1.0"
1174+
resolved "https://registry.yarnpkg.com/@react-pdf/tailwind/-/tailwind-0.1.0.tgz#51a7aa4439b46826fd92912bbc7c1a457c2d734e"
1175+
integrity sha512-GUDesmIu+h4ZVQ21C1aXLjCFhpAzh/oFZy8OUUyddwlJnfD8oDoBaeALbWITLRk3vaxLxHP+cxdi2j21PEKDKw==
1176+
dependencies:
1177+
"@react-pdf/types" "^2.13.1"
1178+
tailwindcss "^4.1.12"
1179+
11721180
"@react-pdf/types@^2.13.1":
11731181
version "2.13.1"
11741182
resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.13.1.tgz#d753d54ce53824ca137582544d2ec0b6c2f61961"
@@ -3606,7 +3614,7 @@ svg-arc-to-cubic-bezier@^3.0.0, svg-arc-to-cubic-bezier@^3.2.0:
36063614
resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6"
36073615
integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==
36083616

3609-
tailwindcss@4.3.3, tailwindcss@^4.0.0:
3617+
tailwindcss@4.3.3, tailwindcss@^4.0.0, tailwindcss@^4.1.12:
36103618
version "4.3.3"
36113619
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.3.tgz#c006861611c213c1877893ab5b23daa16be2bb55"
36123620
integrity sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==

packages/ui/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"@react-pdf/renderer": ">=4.8.0",
3131
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
3232
"@react-pdf/math": ">=5.0.0",
33-
"@react-pdf/mermaid": ">=5.0.0"
33+
"@react-pdf/mermaid": ">=5.0.0",
34+
"@react-pdf/tailwind": ">=0.1.0"
3435
},
3536
"devDependencies": {
3637
"@react-pdf/renderer": "^4.8.1"
@@ -41,6 +42,9 @@
4142
},
4243
"@react-pdf/mermaid": {
4344
"optional": true
45+
},
46+
"@react-pdf/tailwind": {
47+
"optional": true
4448
}
4549
}
4650
}

packages/ui/src/render/render.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const joinFiles = (files: PlaygroundFile[]) =>
2323
const lazyModules: Record<string, () => Promise<unknown>> = {
2424
'@react-pdf/math': () => import('@react-pdf/math'),
2525
'@react-pdf/mermaid': () => import('@react-pdf/mermaid'),
26+
'@react-pdf/tailwind': () => import('@react-pdf/tailwind'),
2627
};
2728

2829
const loaded: Record<string, unknown> = {};

0 commit comments

Comments
 (0)