There are plenty of options for generating PDFs - printing anything from a browser as PDF (using the print stylesheet), exporting from Word or Google Docs, or from design tools such as Illustrator or InDesign. But what if you want to generate them programmatically in React? The good news is it’s incredibly straightforward using React PDF.
I decided to try this and wanted a sample project to work on, ideally a single page document with some styling and various components, but nothing too complex, so decided to create an invoice for the fictitious John Doe Inc. The documentation on the React PDF website is pretty comprehensive, so I’m not going to repeat everything it says here, but will highlight some key parts here from my sample project.
Before you start creating PDFs locally, I highly recommend using the REPL to get the basic structure and design in place, and then move to working locally once you want to start feeding in data or breaking things up into components.
Below is an example of a PDF I generated after about half an hour of experimenting.
Building blocks in React PDF
There are a number of components React PDF makes available to you, which you use to create the structure of your PDF document. You can’t use HTML components like div
or p
, but need to use the specific components, which if you’re familiar with React Native, will feel very natural.
Beyond the document setup, the 2 components I found myself using the most were View for block level structure and Text for text elements. You might also need to use Image
, Link
etc depending on the document.
My approach was to get the initial layout in place with the View
block and then label the sections with the Text
block. I would then move onto the main styling of each component, although you could also work on each component getting the layout and styles completed before moving on - it’s a matter of preference.
Styling elements in React PDF
Styling elements is very similar to using inline CSS, although there are some caveats, such as:
- As is the convention for print design, the default unit of measurement is
pt
, notpx
- Limited subset of CSS, although for 99.9% of cases what is available is sufficient
- Limited shorthand, so you can’t do
margin: '0 10 20 30'
, but you can domarginHorizonatal
andmarginVertical
etc
After my first pass, the style code was a bit of a mess, and would need to tidying up, extracting common elements, possibly even using styled components as things are broken down to be more manageable.
Using fonts
Instead of using just the default fonts, you can use custom fonts. You will want to import each weight/style of a font as a separate named font family. For example, you can’t do https://fonts.googleapis.com/css?family=Montserrat:400,700
to import both regular and bold.
Also, you can’t just import them in the same way you would for a regular website, as React PDF expect a direct link to the font, where as Google Fonts links to a stylesheet which has links to the correct font based on the device. For a modern browser, this would be a woff2
formatted font, but you actually need the ttf
version.
The easiest way I found to get this was to request the font through curl
, which would always give back the correct format, and grab the URL from there.
$ curl https://fonts.googleapis.com/css?family=Montserrat:400
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459Wlhzg.ttf) format('truetype');
}
Complete example
Based on the above, below is a complete example of what a document might look like. You can copy it into the REPL as a starting point if you want to experiment, or just get a sense of how things work for your own implementation.
As mentioned previously, this is just an example and not something which is production ready by any means. If I was to continue working with this, I would:
- Break it up into a number of compoents, so that it’s easier to manage the structure of the overall document
- Refactor the styles, so that common things like fonts, colors and spacing are abstracted to ensure consistency
- Extract the content into props instead of it being hard coded (although you could leave some of the data such as header/footer as hard coded, you could have that data also fed in via props)
- Ensure it still looks good if the content flow into extra pages
Font.register('https://fonts.gstatic.com/s/kadwa/v2/rnCm-x5V0g7ipiTAT8M.ttf', { family: 'KadwaRegular' } );
Font.register('https://fonts.gstatic.com/s/kadwa/v2/rnCr-x5V0g7ipix7atM5kn0.ttf', { family: 'KadwaBold' } );
Font.register('https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459Wlhzg.ttf', { family: 'MontserratRegular' } );
const Invoice = () => (
<Document>
<Page style={styles.body}>
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text>John Doe Inc</Text>
</View>
<View style={styles.headerRight}>
<Text>John Doe</Text>
<Text>123 Fake Street</Text>
<Text>Fakeville</Text>
<Text>A12 3BC</Text>
</View>
</View>
<View style={styles.ribbon}>
<View style={styles.ribbonBox}>
<Text style={styles.ribbonLabel}>Date</Text>
<Text style={styles.ribbonValue}>06/01/2019</Text>
</View>
<View style={styles.ribbonBox}>
<Text style={styles.ribbonLabel}>Invoice #</Text>
<Text style={styles.ribbonValue}>123</Text>
</View>
<View style={styles.ribbonBox}>
<Text style={styles.ribbonLabel}>Amount</Text>
<Text style={styles.ribbonValue}>£4500.00</Text>
</View>
</View>
<View style={styles.table}>
<View style={styles.tableRowB}>
<Text style={styles.tableHeadingA}>
Service
</Text>
<Text style={styles.tableHeadingB}>
Description
</Text>
<Text style={styles.tableHeadingC}>
Amount
</Text>
</View>
<View style={styles.tableRowA}>
<Text style={styles.serviceName}>
Design
</Text>
<Text style={styles.serviceDescription}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
</Text>
<Text style={styles.serviceAmount}>
£1000.00
</Text>
</View>
<View style={styles.tableRowB}>
<Text style={styles.serviceName}>
Development
</Text>
<Text style={styles.serviceDescription}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
</Text>
<Text style={styles.serviceAmount}>
£2000.00
</Text>
</View>
<View style={styles.tableRowA}>
<Text style={styles.serviceName}>
Consultation
</Text>
<Text style={styles.serviceDescription}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
</Text>
<Text style={styles.serviceAmount}>
£1500.00
</Text>
</View>
<View style={styles.summary}>
<Text style={styles.summaryMeta}>
Due Date: 19/01/2019
</Text>
<Text style={styles.summaryAmount}>
£4500.00
</Text>
</View>
</View>
<Text style={styles.howToPay}>Paying your invoice</Text>
<View style={styles.ribbonGrey}>
<View style={styles.ribbonBox}>
<Text style={styles.ribbonLabel}>Bank Name</Text>
<Text style={styles.ribbonValue}>Lloyds</Text>
</View>
<View style={styles.ribbonBox}>
<Text style={styles.ribbonLabel}>Sort Code</Text>
<Text style={styles.ribbonValue}>00-00-00</Text>
</View>
<View style={styles.ribbonBox}>
<Text style={styles.ribbonLabel}>Account Number</Text>
<Text style={styles.ribbonValue}>1234 5678</Text>
</View>
</View>
<View style={styles.footer}>
<Text>
example.com •
johndoe@example.com •
0123 456 7890
</Text>
</View>
</Page>
</Document>
);
const styles = StyleSheet.create({
header: {
borderTopWidth: 4,
borderTopColor: '#0084B4',
color: '#898989',
padding: 20,
display: 'flex',
flexDirection: 'row',
},
headerLeft: {
textAlign: 'left',
flex: 1,
alignSelf: 'center',
fontSize: 32,
fontWeight: 900,
lineHeight: 1,
color: '#0084B4',
fontFamily: 'KadwaBold',
},
headerRight: {
textAlign: 'right',
fontSize: 12,
flex: 1,
fontFamily: 'MontserratRegular',
},
ribbon: {
backgroundColor: '#0084B4',
display: 'flex',
flexDirection: 'row',
padding: 20,
marginBottom: 20,
textAlign: 'center',
color: '#FFF',
},
ribbonGrey: {
backgroundColor: '#EDEDED',
display: 'flex',
flexDirection: 'row',
padding: 20,
marginBottom: 20,
textAlign: 'center',
color: '#0084B4',
},
ribbonBox: {
width: '33.333333%',
},
ribbonLabel: {
fontSize: 14,
fontFamily: 'KadwaBold',
},
ribbonValue: {
fontSize: 28,
fontFamily: 'KadwaBold',
},
table: {
paddingHorizontal: 20,
display: 'flex',
marginBottom: 20,
},
tableRowA: {
backgroundColor: '#EDEDED',
display: 'flex',
flexDirection: 'row',
padding: 10,
},
tableRowB: {
padding: 10,
display: 'flex',
flexDirection: 'row',
},
serviceName: {
fontSize: 10,
width: '25%',
fontFamily: 'KadwaBold',
},
serviceDescription: {
fontSize: 10,
width: '50%',
fontFamily: 'MontserratRegular',
},
serviceAmount: {
fontSize: 20,
width: '25%',
textAlign: 'right',
fontFamily: 'KadwaBold',
},
tableHeadingA: {
width: '25%',
fontSize: 14,
color: '#0084B4',
fontFamily: 'KadwaBold',
},
tableHeadingB: {
width: '50%',
fontSize: 14,
color: '#0084B4',
fontFamily: 'KadwaBold',
},
tableHeadingC: {
width: '25%',
textAlign: 'right',
fontSize: 14,
color: '#0084B4',
fontFamily: 'KadwaBold',
},
summary: {
backgroundColor: '#0084B4',
color: '#FFF',
padding: 20,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
summaryMeta: {
width: '75%',
fontFamily: 'KadwaBold',
},
summaryAmount: {
fontSize: 20,
width: '25%',
textAlign: 'right',
fontFamily: 'KadwaBold',
},
howToPay: {
paddingHorizontal: 20,
textAlign: 'center',
fontSize: 20,
color: '#0084B4',
fontFamily: 'KadwaBold',
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 20,
borderTopColor: '#EDEDED',
borderTopWidth: 1,
textAlign: 'center',
fontSize: 10,
fontFamily: 'MontserratRegular',
},
});
ReactPDF.render(<Invoice/>);
Overall, React PDF is very easy to work with and produces great results. I look forward to experimenting with it more in the future. The example PDF can be downloaded here.