New on our Blog: Generate Signable PDF Forms with React
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 @ Fileforge. 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 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 &bull; 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 &amp; 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!

Related products

Also on our blog