DEV Community

Cover image for React x ChakraUI: How to Craft Stylish PDFs
Auguste for Onedoc

Posted on

React x ChakraUI: How to Craft Stylish PDFs

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.

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 }
  );
}
Enter fullscreen mode Exit fullscreen mode

When calling getHTML, we will get the following 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>
Enter fullscreen mode Exit fullscreen mode

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:

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));

})();
Enter fullscreen mode Exit fullscreen mode

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.

The card created using ChakraUI

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!

NOTE: This article has been taken from the Onedoc's blog

Top comments (0)