React x ChakraUI: How to Build Stylish PDFs

React x ChakraUI: How to Build Stylish PDFs

Thursday, April 18, 2024

Auguste Lefevre Grunewald

Studied Computer Science and Quantitative Finance. Co-founder @ Onedoc. I'm passionate about technology, politics, history and nature. I love to share my thoughts and learn from others.

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 Onedoc:

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 {Box, Image, Flex, Badge, Text, ChakraProvider } from "@chakra-ui/react";
4
import {compile} from "@onedoc/react-print";
5
6
export const getHTML = () => {
7
return compile(
8
// A simple example of a Chakra UI Component that will be rendered to a PDF
9
<ChakraProvider>
10
<Box p="5" maxW="30%" maxH="30%" borderWidth="1px">
11
<Image boxSize='150px' borderRadius="md" src="https://bit.ly/2k1H1t6" />
12
<Flex align="baseline" mt={2}>
13
<Badge colorScheme="pink">Plus</Badge>
14
<Text
15
ml={2}
16
textTransform="uppercase"
17
fontSize="sm"
18
fontWeight="bold"
19
color="pink.800"
20
>
21
Verified &bull; Cape Town
22
</Text>
23
</Flex>
24
<Text mt={2} fontSize="xl" fontWeight="semibold" lineHeight="short">
25
Modern, Chic Penthouse with Mountain, City & Sea Views
26
</Text>
27
<Text mt={2}>$119/night</Text>
28
<Flex mt={2} align="center">
29
<Box as={MdStar} color="orange.400" />
30
<Text ml={1} fontSize="sm">
31
<b>4.84</b> (190)
32
</Text>
33
</Flex>
34
</Box>
35
</ChakraProvider>
36
, { emotion: true }
37
);
38
}

When calling getHTML, we will get the following HTML:

1
<!doctype html>
2
<html>
3
<head>
4
<style>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(
5
html {
6
line-height: 1.5;
7
-webkit-text-size-adjust: 100%;
8
font-family: system-ui, sans-serif;
9
-webkit-font-smoothing: antialiased;
10
text-rendering: optimizeLegibility;
11
-moz-osx-font-smoothing: grayscale;
12
touch-action: manipulation;
13
}
14
15
body {
16
position: relative;
17
min-height: 100%;
18
margin: 0;
19
font-feature-settings: "kern";
20
}
21
22
:where(*, *::before, *::after) {
23
border-width: 0;
24
border-style: solid;
25
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;}</style><style>/* src/generic.css */
26
word-wrap: break-word;
27
}
28
29
main {
30
display: block;
31
}
32
33
hr {
34
border-top-width: 1px;
35
box-sizing: content-box;
36
height: 0;
37
overflow: visible;
38
}
39
40
:where(pre, code, kbd, samp) {
41
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
42
font-size: 1em;
43
}
44
45
a {
46
background-color: transparent;
47
color: inherit;
48
-webkit-text-decoration: inherit;
49
text-decoration: inherit;
50
}
51
52
abbr[title] {
53
border-bottom: none;
54
-webkit-text-decoration: underline;
55
text-decoration: underline;
56
-webkit-text-decoration: underline dotted;
57
-webkit-text-decoration: underline dotted;
58
text-decoration: underline dotted;
59
}
60
61
:where(b, strong) {
62
font-weight: bold;
63
}
64
65
small {
66
font-size: 80%;
67
}
68
69
:where(sub, sup) {
70
font-size: 75%;
71
line-height: 0;
72
position: relative;
73
vertical-align: baseline;
74
}
75
76
sub {
77
bottom: -0.25em;
78
}
79
80
sup {
81
top: -0.5em;
82
}
83
84
img {
85
border-style: none;
86
}
87
88
:where(button, input, optgroup, select, textarea) {
89
font-family: inherit;
90
font-size: 100%;
91
line-height: 1.15;
92
margin: 0;
93
}
94
95
:where(button, input) {
96
overflow: visible;
97
}
98
99
:where(button, select) {
100
text-transform: none;
101
}
102
103
:where(
104
button::-moz-focus-inner,
105
[type="button"]::-moz-focus-inner,
106
[type="reset"]::-moz-focus-inner,
107
[type="submit"]::-moz-focus-inner
108
) {
109
border-style: none;
110
padding: 0;
111
}
112
113
fieldset {
114
padding: 0.35em 0.75em 0.625em;
115
}
116
117
legend {
118
box-sizing: border-box;
119
color: inherit;
120
display: table;
121
max-width: 100%;
122
padding: 0;
123
white-space: normal;
124
}
125
126
progress {
127
vertical-align: baseline;
128
}
129
130
textarea {
131
overflow: auto;
132
}
133
134
:where([type="checkbox"], [type="radio"]) {
135
box-sizing: border-box;
136
padding: 0;
137
}
138
139
input[type="number"]::-webkit-inner-spin-button,
140
input[type="number"]::-webkit-outer-spin-button {
141
-webkit-appearance: none !important;
142
}
143
144
input[type="number"] {
145
-moz-appearance: textfield;
146
}
147
148
input[type="search"] {
149
-webkit-appearance: textfield;
150
outline-offset: -2px;
151
}
152
153
input[type="search"]::-webkit-search-decoration {
154
-webkit-appearance: none !important;
155
}
156
157
::-webkit-file-upload-button {
158
-webkit-appearance: button;
159
font: inherit;
160
}
161
162
details {
163
display: block;
164
}
165
166
summary {
167
display: -webkit-box;
168
display: -webkit-list-item;
169
display: -ms-list-itembox;
170
display: list-item;
171
}
172
173
template {
174
display: none;
175
}
176
177
[hidden] {
178
display: none !important;
179
}
180
181
:where(
182
blockquote,
183
dl,
184
dd,
185
h1,
186
h2,
187
h3,
188
h4,
189
h5,
190
h6,
191
hr,
192
figure,
193
p,
194
pre
195
) {
196
margin: 0;
197
}
198
199
button {
200
background: transparent;
201
padding: 0;
202
}
203
204
fieldset {
205
margin: 0;
206
padding: 0;
207
}
208
209
:where(ol, ul) {
210
margin: 0;
211
padding: 0;
212
}
213
214
textarea {
215
resize: vertical;
216
}
217
218
:where(button, [role="button"]) {
219
cursor: pointer;
220
}
221
222
button::-moz-focus-inner {
223
border: 0 !important;
224
}
225
226
table {
227
border-collapse: collapse;
228
}
229
230
:where(h1, h2, h3, h4, h5, h6) {
231
font-size: inherit;
232
font-weight: inherit;
233
}
234
235
:where(button, input, optgroup, select, textarea) {
236
padding: 0;
237
line-height: inherit;
238
color: inherit;
239
}
240
241
:where(img, svg, video, canvas, audio, iframe, embed, object) {
242
display: block;
243
}
244
245
:where(img, video) {
246
max-width: 100%;
247
height: auto;
248
}
249
250
[data-js-focus-visible] :focus:not([data-focus-visible-added]):not(
251
[data-focus-visible-disabled]
252
) {
253
outline: none;
254
box-shadow: none;
255
}
256
257
select::-ms-expand {
258
display: none;
259
}
260
261
body {
262
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
263
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
264
color: undefined;
265
background: undefined;
266
transition-property: background-color;
267
transition-duration: 200ms;
268
line-height: 1.5;
269
}
270
271
*::-webkit-input-placeholder {
272
color: rgba(255, 255, 255, 0.24);
273
}
274
275
*::-moz-placeholder {
276
color: rgba(255, 255, 255, 0.24);
277
}
278
279
*:-ms-input-placeholder {
280
color: rgba(255, 255, 255, 0.24);
281
}
282
283
*::placeholder {
284
color: rgba(255, 255, 255, 0.24);
285
}
286
287
* {
288
border-color: rgba(255, 255, 255, 0.16);
289
}
290
291
*::before {
292
border-color: rgba(255, 255, 255, 0.16);
293
}
294
295
::after {
296
border-color: undefined;
297
}
298
299
.css-13az0h3 {
300
padding: 1.25rem;
301
max-width: 30%;
302
max-height: 30%;
303
border-width: 1px;
304
}
305
306
.css-1h5t4dr {
307
width: 150px;
308
height: 150px;
309
border-radius: 0.375rem;
310
}
311
312
.css-1safuhm {
313
display: -webkit-box;
314
display: -webkit-flex;
315
display: -ms-flexbox;
316
display: flex;
317
-webkit-align-items: baseline;
318
-webkit-box-align: baseline;
319
-ms-flex-align: baseline;
320
align-items: baseline;
321
margin-top: 0.5rem;
322
}
323
324
.css-1618c9b {
325
display: inline-block;
326
white-space: nowrap;
327
vertical-align: middle;
328
-webkit-padding-start: 0.25rem;
329
padding-left: 0.25rem;
330
-webkit-padding-end: 0.25rem;
331
padding-right: 0.25rem;
332
text-transform: uppercase;
333
font-size: 0.75rem;
334
border-radius: 0.125rem;
335
font-weight: 700;
336
background: #FED7E2;
337
color: #702459;
338
box-shadow: undefined;
339
}
340
341
.css-qigmjc {
342
margin-left: 0.5rem;
343
text-transform: uppercase;
344
font-size: 0.875rem;
345
font-weight: 700;
346
color: #702459;
347
}
348
349
.css-1x3wlpg {
350
margin-top: 0.5rem;
351
font-size: 1.25rem;
352
font-weight: 600;
353
line-height: 1.375;
354
}
355
356
.css-rltemf {
357
margin-top: 0.5rem;
358
}
359
360
.css-1myfyhp {
361
display: -webkit-box;
362
display: -webkit-flex;
363
display: -ms-flexbox;
364
display: flex;
365
-webkit-align-items: center;
366
-webkit-box-align: center;
367
-ms-flex-align: center;
368
align-items: center;
369
margin-top: 0.5rem;
370
}
371
372
.css-1jkapds {
373
color: #ED8936;
374
}
375
376
.css-1ipfgui {
377
margin-left: 0.25rem;
378
font-size: 0.875rem;
379
}
380
</style>
381
<div class="css-13az0h3">
382
<img src="https://bit.ly/2k1H1t6" class="chakra-image css-1h5t4dr" />
383
<div class="css-1safuhm">
384
<span class="chakra-badge css-1618c9b">Plus</span>
385
<p class="chakra-text css-qigmjc">Verified • Cape Town</p>
386
</div>
387
<p class="chakra-text css-1x3wlpg">Modern, Chic Penthouse with Mountain, City &amp; Sea Views</p>
388
<p class="chakra-text css-rltemf">$119/night</p>
389
<div class="css-1myfyhp">
390
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" class="css-1jkapds" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
391
<path fill="none" d="M0 0h24v24H0z"></path>
392
<path fill="none" d="M0 0h24v24H0z"></path>
393
<path 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"></path>
394
</svg>
395
<p class="chakra-text css-1ipfgui"><b>4.84</b> (190)</p>
396
</div>
397
</div>
398
<span></span>
399
<span id="__chakra_env" hidden=""></span>
400
</head>
401
</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 Onedoc 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 Onedoc:

1
import { Onedoc } from "@onedoc/client";
2
import { getHTML } from "./blog.tsx";
3
import fs from "fs";
4
5
const onedoc = new Onedoc(process.env.ONEDOC_API_KEY!); //
6
7
(async () => { const {file, error} = await onedoc.render({
8
html: await getHTML(),
9
});
10
11
if (error) {
12
console.error(error);
13
}
14
15
fs.writeFileSync("chakraUI_example.pdf", new Buffer(file));
16
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 Onedoc. 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!

Also on our blog