React x ChakraUI: How to Craft Stylish PDFs
Tags: react, design, node, webdev
Published: 2024-04-19 01:42:42

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](https://github.com/OnedocLabs/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. 
```tsx
import React from 'react';
import { MdStar } from "react-icons/md";
import {Box, Image, Flex, Badge, Text, ChakraProvider } from "@chakra-ui/react";
import {compile} from "@onedoc/react-print";

export const getHTML = () => {
  return compile(
    // A simple example of a Chakra UI Component that will be rendered to a PDF
    <ChakraProvider>
      <Box p="5" maxW="30%" maxH="30%" borderWidth="1px">
        <Image boxSize='150px' borderRadius="md" src="https://bit.ly/2k1H1t6" />
        <Flex align="baseline" mt={2}>
          <Badge colorScheme="pink">Plus</Badge>
          <Text
            ml={2}
            textTransform="uppercase"
            fontSize="sm"
            fontWeight="bold"
            color="pink.800"
          >
            Verified &bull; Cape Town
          </Text>
        </Flex>
        <Text mt={2} fontSize="xl" fontWeight="semibold" lineHeight="short">
          Modern, Chic Penthouse with Mountain, City & Sea Views
        </Text>
        <Text mt={2}>$119/night</Text>
        <Flex mt={2} align="center">
          <Box as={MdStar} color="orange.400" />
          <Text ml={1} fontSize="sm">
            <b>4.84</b> (190)
          </Text>
        </Flex>
      </Box>
    </ChakraProvider>
    , { emotion: true }
  );
}
```

When calling `getHTML`, we will get the following HTML:

```html
<!doctype html>
<html>
<head>
<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(
html {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    font-family: system-ui, sans-serif;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    -moz-osx-font-smoothing: grayscale;
    touch-action: manipulation;
}

body {
    position: relative;
    min-height: 100%;
    margin: 0;
    font-feature-settings: "kern";
}

:where(*, *::before, *::after) {
    border-width: 0;
    border-style: solid;
    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 */
    word-wrap: break-word;
}

main {
    display: block;
}

hr {
    border-top-width: 1px;
    box-sizing: content-box;
    height: 0;
    overflow: visible;
}

:where(pre, code, kbd, samp) {
    font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: 1em;
}

a {
    background-color: transparent;
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
}

abbr[title] {
    border-bottom: none;
    -webkit-text-decoration: underline;
    text-decoration: underline;
    -webkit-text-decoration: underline dotted;
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
}

:where(b, strong) {
    font-weight: bold;
}

small {
    font-size: 80%;
}

:where(sub, sup) {
    font-size: 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(
    button::-moz-focus-inner,
    [type="button"]::-moz-focus-inner,
    [type="reset"]::-moz-focus-inner,
    [type="submit"]::-moz-focus-inner
) {
    border-style: none;
    padding: 0;
}

fieldset {
    padding: 0.35em 0.75em 0.625em;
}

legend {
    box-sizing: border-box;
    color: inherit;
    display: table;
    max-width: 100%;
    padding: 0;
    white-space: normal;
}

progress {
    vertical-align: baseline;
}

textarea {
    overflow: auto;
}

:where([type="checkbox"], [type="radio"]) {
    box-sizing: border-box;
    padding: 0;
}

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none !important;
}

input[type="number"] {
    -moz-appearance: textfield;
}

input[type="search"] {
    -webkit-appearance: textfield;
    outline-offset: -2px;
}

input[type="search"]::-webkit-search-decoration {
    -webkit-appearance: none !important;
}

::-webkit-file-upload-button {
    -webkit-appearance: button;
    font: inherit;
}

details {
    display: block;
}

summary {
    display: -webkit-box;
    display: -webkit-list-item;
    display: -ms-list-itembox;
    display: list-item;
}

template {
    display: none;
}

[hidden] {
    display: none !important;
}

:where(
    blockquote,
    dl,
    dd,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6,
    hr,
    figure,
    p,
    pre
) {
    margin: 0;
}

button {
    background: transparent;
    padding: 0;
}

fieldset {
    margin: 0;
    padding: 0;
}

:where(ol, ul) {
    margin: 0;
    padding: 0;
}

textarea {
    resize: vertical;
}

:where(button, [role="button"]) {
    cursor: pointer;
}

button::-moz-focus-inner {
    border: 0 !important;
}

table {
    border-collapse: collapse;
}

:where(h1, h2, h3, h4, h5, h6) {
    font-size: inherit;
    font-weight: inherit;
}

:where(button, input, optgroup, select, textarea) {
    padding: 0;
    line-height: inherit;
    color: inherit;
}

:where(img, svg, video, canvas, audio, iframe, embed, object) {
    display: block;
}

:where(img, video) {
    max-width: 100%;
    height: auto;
}

[data-js-focus-visible] :focus:not([data-focus-visible-added]):not(
    [data-focus-visible-disabled]
) {
    outline: none;
    box-shadow: none;
}

select::-ms-expand {
    display: none;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
        sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    color: undefined;
    background: undefined;
    transition-property: background-color;
    transition-duration: 200ms;
    line-height: 1.5;
}

*::-webkit-input-placeholder {
    color: rgba(255, 255, 255, 0.24);
}

*::-moz-placeholder {
    color: rgba(255, 255, 255, 0.24);
}

*:-ms-input-placeholder {
    color: rgba(255, 255, 255, 0.24);
}

*::placeholder {
    color: rgba(255, 255, 255, 0.24);
}

* {
    border-color: rgba(255, 255, 255, 0.16);
}

*::before {
    border-color: rgba(255, 255, 255, 0.16);
}

::after {
    border-color: undefined;
}

.css-13az0h3 {
    padding: 1.25rem;
    max-width: 30%;
    max-height: 30%;
    border-width: 1px;
}

.css-1h5t4dr {
    width: 150px;
    height: 150px;
    border-radius: 0.375rem;
}

.css-1safuhm {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    -webkit-align-items: baseline;
    -webkit-box-align: baseline;
    -ms-flex-align: baseline;
    align-items: baseline;
    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>
<div class="css-13az0h3">
    <img src="https://bit.ly/2k1H1t6" class="chakra-image css-1h5t4dr" />
    <div class="css-1safuhm">
        <span class="chakra-badge css-1618c9b">Plus</span>
        <p class="chakra-text css-qigmjc">Verified • Cape Town</p>
    </div>
    <p class="chakra-text css-1x3wlpg">Modern, Chic Penthouse with Mountain, City &amp; Sea Views</p>
    <p class="chakra-text css-rltemf">$119/night</p>
    <div class="css-1myfyhp">
        <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">
            <path fill="none" d="M0 0h24v24H0z"></path>
            <path fill="none" d="M0 0h24v24H0z"></path>
            <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>
        </svg>
        <p class="chakra-text css-1ipfgui"><b>4.84</b> (190)</p>
    </div>
</div>
<span></span>
<span id="__chakra_env" hidden=""></span>
</head>
</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`](https://github.com/MatthewHerbst/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`](https://pptr.dev/api/puppeteer.page.pdf) 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](https://github.com/gotenberg/gotenberg).

If you're interested into the difference between these methods, you can read [this article](https://www.onedoclabs.com/blog/html-to-pdf-renderers-comparison-guide) that I wrote on the subject.

Here is an example on how to convert the HTML to PDF using Onedoc:

```tsx
import { Onedoc } from "@onedoc/client";
import { getHTML } from "./blog.tsx";
import fs from "fs";

const onedoc = new Onedoc(process.env.ONEDOC_API_KEY!); //

(async () => { const {file, error} = await onedoc.render({
    html: await getHTML(),
});

if (error) {
    console.error(error);
}

fs.writeFileSync("chakraUI_example.pdf", new Buffer(file));

})();
```

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](https://react.onedoclabs.com/introduction) for more information.


![The card created using ChakraUI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7hm4kdx6u12mbykmzqvo.png)

# 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](https://www.onedoclabs.com/blog/react-tailwind-dynamic-pdf) , written by Titouan Launay, that explains how to create PDFs with Tailwind and React.

Happy coding!

NOTE: This article has been taken from the [Onedoc's blog](https://www.onedoclabs.com/blog/react-chakraui-dynamic-pdf)