React x ChakraUI: How to Build Stylish PDFs
Thursday, April 18, 2024
As a developer, you may have already encountered the need to generate PDFs programmatically. Whether it’s for invoices, reports, or any other type of document, creating PDFs is a common requirement in many applications.
As mentioned in another article written by Titouan Launay, CTO and co-founder of Fileforge:
PDF was invented in 1993 by Adobe as a cross-platform document format. The format itself focuses on being portable rather than interactive - an orthogonal approach to HTML and CSS. While the latter defines a box model, the former has an imperative approach. In a nutshell, an HTML rectangle is a set of 4 lines in PDF.
So how can we keep the strenght of the PDF format while leveraging the flexibility of modern web technologies like React and ChakraUI?
Craft your first PDF with React and ChakraUI
The open-source library react-print-pdf brings a set of components and wrappers we can use to build beautiful PDFs in minutes.
Compile to HTML
ChakraUI is a dynamic CSS framework that relies on a JavaScript runtime to generate the final CSS. This is a problem for PDF generation, as we require a static file. We will first convert to static HTML, then to PDF.
For this, we will add the { emotion: true }
option to the compile
fonction from react-print-pdf
. This will allow us to use the ChakraUI components and generate the final CSS.
You can use any ChakraUI component available, here we will use Box, Image, Flex, Badge and Text to create a simple card. Note we use the ChakraProvider to wrap our components and use the ChakraUI theme.
1 | import React from "react"; |
2 | import { MdStar } from "react-icons/md"; |
3 | import { |
4 | Box, |
5 | Image, |
6 | Flex, |
7 | Badge, |
8 | Text, |
9 | ChakraProvider, |
10 | } from "@chakra-ui/react"; |
11 | import { compile } from "@fileforge/react-print"; |
12 | |
13 | export const getHTML = () => { |
14 | return compile( |
15 | // A simple example of a Chakra UI Component that will be rendered to a PDF |
16 | <ChakraProvider> |
17 | <Box p="5" maxW="30%" maxH="30%" borderWidth="1px"> |
18 | <Image boxSize="150px" borderRadius="md" src="https://bit.ly/2k1H1t6" /> |
19 | <Flex align="baseline" mt={2}> |
20 | <Badge colorScheme="pink">Plus</Badge> |
21 | <Text |
22 | ml={2} |
23 | textTransform="uppercase" |
24 | fontSize="sm" |
25 | fontWeight="bold" |
26 | color="pink.800" |
27 | > |
28 | Verified • Cape Town |
29 | </Text> |
30 | </Flex> |
31 | <Text mt={2} fontSize="xl" fontWeight="semibold" lineHeight="short"> |
32 | Modern, Chic Penthouse with Mountain, City & Sea Views |
33 | </Text> |
34 | <Text mt={2}>$119/night</Text> |
35 | <Flex mt={2} align="center"> |
36 | <Box as={MdStar} color="orange.400" /> |
37 | <Text ml={1} fontSize="sm"> |
38 | <b>4.84</b> (190) |
39 | </Text> |
40 | </Flex> |
41 | </Box> |
42 | </ChakraProvider>, |
43 | { emotion: true } |
44 | ); |
45 | }; |
When calling getHTML
, we will get the following HTML:
1 | <!doctype html> |
2 | <html> |
3 | <head> |
4 | <style> |
5 | 75%;line-height:0;position:relative;vertical-align:baseline;}sub{bottom:-0.25em;}sup{top:-0.5em;}img{border-style:none;}:where(button, input, optgroup, select, textarea){font-family:inherit;font-size:100%;line-height:1.15;margin:0;}:where(button, input){overflow:visible;}:where(button, select){text-transform:none;}:where( |
6 | html { |
7 | line-height: 1.5; |
8 | -webkit-text-size-adjust: 100%; |
9 | font-family: system-ui, sans-serif; |
10 | -webkit-font-smoothing: antialiased; |
11 | text-rendering: optimizeLegibility; |
12 | -moz-osx-font-smoothing: grayscale; |
13 | touch-action: manipulation; |
14 | } |
15 | |
16 | body { |
17 | position: relative; |
18 | min-height: 100%; |
19 | margin: 0; |
20 | font-feature-settings: "kern"; |
21 | } |
22 | |
23 | :where(*, *::before, *::after) { |
24 | border-width: 0; |
25 | border-style: solid; |
26 | box-sizing: border-box;ne;margin-top:0.5rem;}.css-1618c9b{display:inline-block;white-space:nowrap;vertical-align:middle;-webkit-padding-start:0.25rem;padding-left:0.25rem;-webkit-padding-end:0.25rem;padding-right:0.25rem;text-transform:uppercase;font-size:0.75rem;border-radius:0.125rem;font-weight:700;background:#FED7E2;color:#702459;box-shadow:undefined;}.css-qigmjc{margin-left:0.5rem;text-transform:uppercase;font-size:0.875rem;font-weight:700;color:#702459;}.css-1x3wlpg{margin-top:0.5rem;font-size:1.25rem;font-weight:600;line-height:1.375;}.css-rltemf{margin-top:0.5rem;}.css-1myfyhp{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-top:0.5rem;}.css-1jkapds{color:#ED8936;}.css-1ipfgui{margin-left:0.25rem;font-size:0.875rem;} |
27 | </style> |
28 | <style> |
29 | /* src/generic.css */ |
30 | word-wrap: break-word; |
31 | } |
32 | |
33 | main { |
34 | display: block; |
35 | } |
36 | |
37 | hr { |
38 | border-top-width: 1px; |
39 | box-sizing: content-box; |
40 | height: 0; |
41 | overflow: visible; |
42 | } |
43 | |
44 | :where(pre, code, kbd, samp) { |
45 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; |
46 | font-size: 1em; |
47 | } |
48 | |
49 | a { |
50 | background-color: transparent; |
51 | color: inherit; |
52 | -webkit-text-decoration: inherit; |
53 | text-decoration: inherit; |
54 | } |
55 | |
56 | abbr[title] { |
57 | border-bottom: none; |
58 | -webkit-text-decoration: underline; |
59 | text-decoration: underline; |
60 | -webkit-text-decoration: underline dotted; |
61 | -webkit-text-decoration: underline dotted; |
62 | text-decoration: underline dotted; |
63 | } |
64 | |
65 | :where(b, strong) { |
66 | font-weight: bold; |
67 | } |
68 | |
69 | small { |
70 | font-size: 80%; |
71 | } |
72 | |
73 | :where(sub, sup) { |
74 | font-size: 75%; |
75 | line-height: 0; |
76 | position: relative; |
77 | vertical-align: baseline; |
78 | } |
79 | |
80 | sub { |
81 | bottom: -0.25em; |
82 | } |
83 | |
84 | sup { |
85 | top: -0.5em; |
86 | } |
87 | |
88 | img { |
89 | border-style: none; |
90 | } |
91 | |
92 | :where(button, input, optgroup, select, textarea) { |
93 | font-family: inherit; |
94 | font-size: 100%; |
95 | line-height: 1.15; |
96 | margin: 0; |
97 | } |
98 | |
99 | :where(button, input) { |
100 | overflow: visible; |
101 | } |
102 | |
103 | :where(button, select) { |
104 | text-transform: none; |
105 | } |
106 | |
107 | :where( |
108 | button::-moz-focus-inner, |
109 | [type="button"]::-moz-focus-inner, |
110 | [type="reset"]::-moz-focus-inner, |
111 | [type="submit"]::-moz-focus-inner |
112 | ) { |
113 | border-style: none; |
114 | padding: 0; |
115 | } |
116 | |
117 | fieldset { |
118 | padding: 0.35em 0.75em 0.625em; |
119 | } |
120 | |
121 | legend { |
122 | box-sizing: border-box; |
123 | color: inherit; |
124 | display: table; |
125 | max-width: 100%; |
126 | padding: 0; |
127 | white-space: normal; |
128 | } |
129 | |
130 | progress { |
131 | vertical-align: baseline; |
132 | } |
133 | |
134 | textarea { |
135 | overflow: auto; |
136 | } |
137 | |
138 | :where([type="checkbox"], [type="radio"]) { |
139 | box-sizing: border-box; |
140 | padding: 0; |
141 | } |
142 | |
143 | input[type="number"]::-webkit-inner-spin-button, |
144 | input[type="number"]::-webkit-outer-spin-button { |
145 | -webkit-appearance: none !important; |
146 | } |
147 | |
148 | input[type="number"] { |
149 | -moz-appearance: textfield; |
150 | } |
151 | |
152 | input[type="search"] { |
153 | -webkit-appearance: textfield; |
154 | outline-offset: -2px; |
155 | } |
156 | |
157 | input[type="search"]::-webkit-search-decoration { |
158 | -webkit-appearance: none !important; |
159 | } |
160 | |
161 | ::-webkit-file-upload-button { |
162 | -webkit-appearance: button; |
163 | font: inherit; |
164 | } |
165 | |
166 | details { |
167 | display: block; |
168 | } |
169 | |
170 | summary { |
171 | display: -webkit-box; |
172 | display: -webkit-list-item; |
173 | display: -ms-list-itembox; |
174 | display: list-item; |
175 | } |
176 | |
177 | template { |
178 | display: none; |
179 | } |
180 | |
181 | [hidden] { |
182 | display: none !important; |
183 | } |
184 | |
185 | :where( |
186 | blockquote, |
187 | dl, |
188 | dd, |
189 | h1, |
190 | h2, |
191 | h3, |
192 | h4, |
193 | h5, |
194 | h6, |
195 | hr, |
196 | figure, |
197 | p, |
198 | pre |
199 | ) { |
200 | margin: 0; |
201 | } |
202 | |
203 | button { |
204 | background: transparent; |
205 | padding: 0; |
206 | } |
207 | |
208 | fieldset { |
209 | margin: 0; |
210 | padding: 0; |
211 | } |
212 | |
213 | :where(ol, ul) { |
214 | margin: 0; |
215 | padding: 0; |
216 | } |
217 | |
218 | textarea { |
219 | resize: vertical; |
220 | } |
221 | |
222 | :where(button, [role="button"]) { |
223 | cursor: pointer; |
224 | } |
225 | |
226 | button::-moz-focus-inner { |
227 | border: 0 !important; |
228 | } |
229 | |
230 | table { |
231 | border-collapse: collapse; |
232 | } |
233 | |
234 | :where(h1, h2, h3, h4, h5, h6) { |
235 | font-size: inherit; |
236 | font-weight: inherit; |
237 | } |
238 | |
239 | :where(button, input, optgroup, select, textarea) { |
240 | padding: 0; |
241 | line-height: inherit; |
242 | color: inherit; |
243 | } |
244 | |
245 | :where(img, svg, video, canvas, audio, iframe, embed, object) { |
246 | display: block; |
247 | } |
248 | |
249 | :where(img, video) { |
250 | max-width: 100%; |
251 | height: auto; |
252 | } |
253 | |
254 | [data-js-focus-visible] :focus:not([data-focus-visible-added]):not( |
255 | [data-focus-visible-disabled] |
256 | ) { |
257 | outline: none; |
258 | box-shadow: none; |
259 | } |
260 | |
261 | select::-ms-expand { |
262 | display: none; |
263 | } |
264 | |
265 | body { |
266 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, |
267 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; |
268 | color: undefined; |
269 | background: undefined; |
270 | transition-property: background-color; |
271 | transition-duration: 200ms; |
272 | line-height: 1.5; |
273 | } |
274 | |
275 | *::-webkit-input-placeholder { |
276 | color: rgba(255, 255, 255, 0.24); |
277 | } |
278 | |
279 | *::-moz-placeholder { |
280 | color: rgba(255, 255, 255, 0.24); |
281 | } |
282 | |
283 | *:-ms-input-placeholder { |
284 | color: rgba(255, 255, 255, 0.24); |
285 | } |
286 | |
287 | *::placeholder { |
288 | color: rgba(255, 255, 255, 0.24); |
289 | } |
290 | |
291 | * { |
292 | border-color: rgba(255, 255, 255, 0.16); |
293 | } |
294 | |
295 | *::before { |
296 | border-color: rgba(255, 255, 255, 0.16); |
297 | } |
298 | |
299 | ::after { |
300 | border-color: undefined; |
301 | } |
302 | |
303 | .css-13az0h3 { |
304 | padding: 1.25rem; |
305 | max-width: 30%; |
306 | max-height: 30%; |
307 | border-width: 1px; |
308 | } |
309 | |
310 | .css-1h5t4dr { |
311 | width: 150px; |
312 | height: 150px; |
313 | border-radius: 0.375rem; |
314 | } |
315 | |
316 | .css-1safuhm { |
317 | display: -webkit-box; |
318 | display: -webkit-flex; |
319 | display: -ms-flexbox; |
320 | display: flex; |
321 | -webkit-align-items: baseline; |
322 | -webkit-box-align: baseline; |
323 | -ms-flex-align: baseline; |
324 | align-items: baseline; |
325 | margin-top: 0.5rem; |
326 | } |
327 | |
328 | .css-1618c9b { |
329 | display: inline-block; |
330 | white-space: nowrap; |
331 | vertical-align: middle; |
332 | -webkit-padding-start: 0.25rem; |
333 | padding-left: 0.25rem; |
334 | -webkit-padding-end: 0.25rem; |
335 | padding-right: 0.25rem; |
336 | text-transform: uppercase; |
337 | font-size: 0.75rem; |
338 | border-radius: 0.125rem; |
339 | font-weight: 700; |
340 | background: #FED7E2; |
341 | color: #702459; |
342 | box-shadow: undefined; |
343 | } |
344 | |
345 | .css-qigmjc { |
346 | margin-left: 0.5rem; |
347 | text-transform: uppercase; |
348 | font-size: 0.875rem; |
349 | font-weight: 700; |
350 | color: #702459; |
351 | } |
352 | |
353 | .css-1x3wlpg { |
354 | margin-top: 0.5rem; |
355 | font-size: 1.25rem; |
356 | font-weight: 600; |
357 | line-height: 1.375; |
358 | } |
359 | |
360 | .css-rltemf { |
361 | margin-top: 0.5rem; |
362 | } |
363 | |
364 | .css-1myfyhp { |
365 | display: -webkit-box; |
366 | display: -webkit-flex; |
367 | display: -ms-flexbox; |
368 | display: flex; |
369 | -webkit-align-items: center; |
370 | -webkit-box-align: center; |
371 | -ms-flex-align: center; |
372 | align-items: center; |
373 | margin-top: 0.5rem; |
374 | } |
375 | |
376 | .css-1jkapds { |
377 | color: #ED8936; |
378 | } |
379 | |
380 | .css-1ipfgui { |
381 | margin-left: 0.25rem; |
382 | font-size: 0.875rem; |
383 | } |
384 | </style> |
385 | <div class="css-13az0h3"> |
386 | <img src="https://bit.ly/2k1H1t6" class="chakra-image css-1h5t4dr" /> |
387 | <div class="css-1safuhm"> |
388 | <span class="chakra-badge css-1618c9b">Plus</span> |
389 | <p class="chakra-text css-qigmjc">Verified • Cape Town</p> |
390 | </div> |
391 | <p class="chakra-text css-1x3wlpg"> |
392 | Modern, Chic Penthouse with Mountain, City & Sea Views |
393 | </p> |
394 | <p class="chakra-text css-rltemf">$119/night</p> |
395 | <div class="css-1myfyhp"> |
396 | <svg |
397 | stroke="currentColor" |
398 | fill="currentColor" |
399 | stroke-width="0" |
400 | viewBox="0 0 24 24" |
401 | class="css-1jkapds" |
402 | height="1em" |
403 | width="1em" |
404 | xmlns="http://www.w3.org/2000/svg" |
405 | > |
406 | <path fill="none" d="M0 0h24v24H0z"></path> |
407 | <path fill="none" d="M0 0h24v24H0z"></path> |
408 | <path |
409 | d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" |
410 | ></path> |
411 | </svg> |
412 | <p class="chakra-text css-1ipfgui"><b>4.84</b> (190)</p> |
413 | </div> |
414 | </div> |
415 | <span></span> |
416 | <span id="__chakra_env" hidden=""></span> |
417 | </head> |
418 | </html> |
By looking at the HTML, we can see how powerfull ChakraUI is. The CSS is generated and applied to the components, making it easy to create beautiful PDFs.
Converting the HTML to PDF
There are several ways to convert this HTML to a PDF:
- Use Fileforge as a client-side or server-side API, that will support all features such as headers, footers, and page numbers.
- If on the client side, you can use
react-to-print
to use the browser’s print dialog. This is cheap option but will not support advanced features and may introduce a lot of visual bugs. - Use a server-side headless browser such as
puppeteer
to convert the HTML to PDF. This is the most reliable free option, but requires a server. If you need to use it in production, we recommend you use Gotenberg.
If you’re interested into the difference between these methods, you can read this article that I wrote on the subject.
Here is an example on how to convert the HTML to PDF using Fileforge:
1 | import { Fileforge } from "@fileforge/client"; |
2 | import { getHTML } from "./blog.tsx"; |
3 | import fs from "fs"; |
4 | |
5 | const fileforge = new Fileforge(process.env.FILEFORGE_API_KEY!); // |
6 | |
7 | (async () => { |
8 | const { file, error } = await fileforge.render({ |
9 | html: await getHTML(), |
10 | }); |
11 | |
12 | if (error) { |
13 | console.error(error); |
14 | } |
15 | |
16 | fs.writeFileSync("chakraUI_example.pdf", new Buffer(file)); |
17 | })(); |
That’s it! You now have a beautiful PDF generated from your React app. You can use most of ChakraUI features as well as the components from react-print-pdf
to create advanced layouts. Check out the documentation for more information.
Conclusion
In this article, we’ve seen how to use ChakraUI and the react-print-pdf library to create stylish PDFs with Fileforge. By leveraging the power of ChakraUI and React, you can easily create beautiful PDFs that match the look and feel of your web application.
If you’re more a Tailwind fan, you can check out this article , written by Titouan Launay, that explains how to create PDFs with Tailwind and React.
Happy coding!