back

Generating PDFs with HTML and CSS

This is a quick introduction for web developers on using HTML and CSS to generate PDFs.
I'll briefly focus on a few issues specific to print media and how CSS can solve them.

The CSS described here is nothing specific to typeset.sh, but will also work with other renderers that conform to Print CSS.

Fragmentation

Unlike websites, which can theoretically have an infinite size, the content of a pdf may be split between multiple pages. This process, called fragmentation, can sometimes lead to ugly or unwanted text layouts if not addressed properly.

CSS allows you to control how elements are broken up when placed on pages. Below, we’ll introduce the most crucial CSS properties.

The problem of the abandoned headline

The break-before property

A common issue occurs when within a text flow when a headline gets placed at the bottom of the current page, but the corresponding paragraph begins on the next page, leaving the headline all by itself. To avoid this problem, CSS allows you to control and determine where a document can break, should not break or must break.

Let’s look at this headline example in detail: the code below will ensure that the red h2 headline is placed on the following page to avoid an abandoned headline.

Go ahead; feel free to change the code below. You will see that, by changing break-after:auto; to break-after:avoid;, headline 2 will get placed on the next page, and be reunited with its adjoining paragraph. Notice how this is also valid if the headline is the only or last child of a containing element. (e.g. <div><h1>Test</h1></div><p>…</p>)

By the way, this behavior is not just applicable for pages, but would also work for column layouts.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> html { font-size: 24pt; } /* Avoid page breaks after headlines */ h1, h2, h3, h4, h5, h6 { break-after:auto; /* change me */ } .title { color: red; } </style> </head> <body> <h2>Headline 1</h2> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna. Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> <div class="title"> <h2>Headline 2</h2> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna. Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

The break-after property

Above, we used the break-after property to influence the fragmentation behavior for the layout flow. Additionally, it is also possible to control the fragmentation of the flow before an element by using the property break-before.

This is easily done: Change the break-before property in the example below from auto to page. This will force a page break before all h1 elements.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> html { font-size: 24pt; } h1 { break-before:auto; } h1, h2, h3, h4, h5, h6 { break-after:avoid; } </style> </head> <body> <h1>Headline 1</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna.</p> <h2>Headline 1.1</h2> <p>Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> <h1>Headline 2</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna.</p> <h2>Headline 2.1</h2> <p>Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

Alternatively, one can also apply left or right to break-before. This can be helpful when rendering a book and new chapters should start on the left or right page.

Alternatively, you can also apply left or right to break-before. This can be helpful when rendering a book when new chapters should start on the left or right side of the page pair.

Orphans and widows

The orphans that are left behind

Another common text layout issue occurs if the headline has enough space to fit on the current page but only one line of the adjoining paragraph fits on that same page. Technically, the break-after: avoid; rule is not violated in this case. However, the resulting text flow layout does not look right, as a paragraph with only a single line looks odd.

Those lines are called orphans. The matching CSS property orphans: <int>; lets you define the minimum required number of lines of a new paragraph that must be placed together on a page to be displayed. The CSS default is 2 lines for paragraphs. In the example below, let’s change orphans: 2; to orphans: 5; to see how the paragraph and the corresponding headline get moved to the next page.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> html { font-size: 24pt; } h1 { break-after:avoid; } p { orphans: 2; } </style> </head> <body> <div style="background: salmon; height: 22em;">Spacer</div> <h1>Orphans</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna. Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

The widows that end alone

The opposite to the orphans issue can also occur frequently. Sometimes, a paragraph gets split, causing the last line(s) to end up alone on the following page. These lines are called widows and can be addressed with the CSS property widows.

Change the widows: 2 to widows: 4 to make the text appear more pleasing to the eye. This change will “steal” the missing lines from the previous page. However, it will do this without breaking the orphan-rule, that is, if one has been set. If there are not enough lines to accommodate the widows and orphans rule, all lines will be kept together and placed on the next page.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> html { font-size: 20pt; } h1 { break-after:avoid; } p { orphans: 5; widows: 2; } </style> </head> <body> <div style="height: 15em; background: red; color: white;">Spacer - scroll down</div> <h1>Widows</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna. Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

One more note about fragmentation

Having defined all those rules, there is one rule that will always apply and might overwrite your break control, and that is: the layout must make progress. That means if you have a larger section of elements that should not break but will also not fit entirely on a new page, it will break at the first best option, ignoring your control in order to make progress.

Now that you know all about page breaking, let’s look at how to define a page.

Styling pages

Page size

By default, typeset.sh will render the pdf using the A4 page size. The page size can be adjusted using the @page at-rule.

Here are few common page size examples.

size: A3; size: A4 landscape; size: A2 portrait; size: JIS-B4; size: letter; size: 100mm; size: 100mm 200mm;

Let’s keep the A4 size. Along with the size we can also define the margin, background, bleed area and print markers.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> @page { size: A4; margin: 50mm 20mm 10mm 20mm; bleed: 20mm; marks: crop cross colors; background: lightblue; } html { font-size: 20pt; } </style> </head> <body> Hello A4 page </body> </html>

Named pages

You can also define named pages, that you references in your content flow and make sure that certain elements are always rendered on a specific named page.

For example, assume that our document has a few tables that are too wide to fit on an A4 portrait page, but would fit on an A4 landscape page. We make the table appear on the landscape page by creating a class for table, telling it to force a page break to a named page called “wide” or any other arbitrary name.

Then all we have to do is, create a named page with the name of “wide” set its size to A4 landscape.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> @page { size: A4; margin: 2cm; } @page wide { size: A4 landscape; background: lightblue; } html { font-size: 20pt; } table.wide { border: 2px solid black; page: wide; } table.wide td, table.wide th{ padding: 2mm 5mm; text-align: right; border: 1px solid black; } </style> </head> <body> <h1>Headline 1</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna. Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> <table class="wide"> <tr> <th></th> <th>1</th> <th>2</th> <th>3</th> <th>4</th> <th>5</th> </tr> <tr> <th>1</th> <td>1</td> <td>2</td> <td>3</td> <td>4</td> <td>5</td> </tr> <tr> <th>2</th> <td>2</td> <td>4</td> <td>6</td> <td>8</td> <td>10</td> </tr> <tr> <th>3</th> <td>3</td> <td>6</td> <td>9</td> <td>12</td> <td>15</td> </tr> </table> <h1>Headline 2</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna. Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

Empty pages

Remember when I mentioned that by using left or right for the break-after property above, you could end up generating a blank page? That is what the :empty page selector is for. It allows you to customize an empty page where no content is placed.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Blank page selector</title> <style> @page { size: 200px 100px; margin: 10px; } @page:left { background: red; } p { break-after: left; } </style> </head> <body> <p>Hello World 1</p> <p>Hello World 2</p> <p>Hello World 3</p> </body> </html>

Page margin regions

Within the page-at-rule, you can also define 16 margin-regions and add content to them. Take a look at the example below which defines all 16 margin-regions.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>page-margins.html</title> <style> @page { size:A4; margin: 20mm; @top-left { content: "left"; background: yellow; } @top-right { content: "right"; background: red; } @top-center { content: "center"; background: green; } @bottom-left { content: "left"; background: yellow; } @bottom-right { content: "right"; background: red; } @bottom-center { content: "center"; background: green; } @left-top { content: "top"; background: yellow; } @left-bottom { content: "bottom"; background: red; } @left-middle { content: "middle"; background: green; } @right-top { content: "top"; background: yellow; } @right-bottom { content: "bottom"; background: red; } @right-middle { content: "middle"; background: green; } @bottom-left-corner { content: "bl"; background: #587b80; } @bottom-right-corner { content: "br"; background: #587b80; } @top-left-corner { content: "tl"; background: #587b80; } @top-right-corner { content: "tr"; background: #587b80; } } html { font-size: 20pt; } </style> </head> <body> <h1>Page margins</h1> <p>You should see all 16 page margins, 3 at the top, bottom, left and right plus 4 in each corner.</p> </body> </html>

Play with the page margin and see how it affects the margin-regions size.

The rule, on how much space a region can take is defined here. It’s a bit complicated, but basically, if you only use the top-left and top-right regions, then both regions will take the content of top-center region.

Content property

Simply setting static content is a bit useless. Luckily, CSS allows a few dynamic content functions for your page margin regions.

The page and pages counter

The page and pages counters are reserved counters that will always return the current page number and total number of pages respectively.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> @page { size: A5; margin: 20mm; @bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 15pt; color: red; } } html { font-size: 24pt; } /* Begin new chapter with a new page */ h1 { break-before:page; } /* Avoid page breaks after headlines */ h1, h2, h3, h4, h5, h6 { break-after:avoid; } </style> </head> <body> <h1>Headline 1</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna.</p> <h2>Headline 1.1</h2> <p>Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> <h1>Headline 2</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna.</p> <h2>Headline 2.1</h2> <p>Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

Running elements

As well as being able to add static CSS content to your margin areas that you want repeated on each page, you can also add content from your document flow and place it in your margin regions.

This is still a draft in CSS and might be subject to change, but nevertheless, it is really useful and we have implemented it as it is with the current state.

In the example below, you can see how the top-center margin region always shows the first major headline of the current page. You can control which element to use with the second parameter, in case a single page contains more than one possible element.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> <style> @page { size: A5; margin: 20mm; @top-center { content: element(top-title); background: #eee; } } html { font-size: 24pt; } .title { position: running(top-title); font-size: 15pt; color: red; text-align: center; } h1, h2, h3, h4, h5, h6 { break-after:avoid; } </style> </head> <body> <div class="title">Headline 1 - foo bar</div> <h1>Headline 1</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna.</p> <h2>Headline 1.1</h2> <p>Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> <div class="title">Headline 2 - bar foo</div> <h1>Headline 2</h1> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec augue neque, mattis in dolor sit amet, pellentesque pulvinar felis. Sed dignissim malesuada ipsum, at dignissim elit. Aliquam quis elit finibus, eleifend lacus a, laoreet odio. Vivamus vel ligula tincidunt magna tempus porta vitae id ligula. Maecenas viverra, ante eu congue tristique, justo mi iaculis mi, id fringilla nisi enim eu magna.</p> <h2>Headline 2.1</h2> <p>Cras porttitor, elit sed rutrum mollis, turpis sapien faucibus justo, ac sollicitudin orci ante a neque. Vestibulum porta est ac leo pretium, vel sollicitudin mauris sollicitudin. Vestibulum vel volutpat magna. Proin sed orci nec lacus elementum maximus. Nunc tempor vel turpis ac tempus. Nunc elementum elementum porta. </p> </body> </html>

Target page counters

Remember the special page counter from above? Well, you can also retrieve the page number of an element within the document. This is extremely helpful if you want to make a table of contents.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>page-counter.html</title> <style> @page { size: 100mm 150mm; margin: 10mm; @bottom-right { content: "Page: " counter(page) " of " counter(pages); font-size: 0.7em; } } h2 { break-inside: avoid; break-after: avoid; } .pages { content: counter(pages) } #toc > li > a::after { content: "(page: " target-counter(attr(href url), page) ")"; display: inline; white-space: nowrap; } </style> </head> <body> <h1>Page counter(s)</h1> <p>The page counter lets you display the current page number or the total number of pages. This can be used even with a text flow, for example, this document hast <span class="pages"></span> pages.</p> <p>Another lovely CSS feature is the target-counter() function, which gives you back the page number at which an element is first displayed. Combined with the attr() function, you can get the page number for different elements based on the on a attribute. This lets you create simple table of contents with the page number.</p> <ol id="toc"> <li><a href="#topic1">The rise of pudding </a></li> <li><a href="#topic2">The fundamentals of pudding </a></li> <li><a href="#topic3">Major historical pudding events </a></li> <li><a href="#topic4">Famous people who don't like pudding </a></li> <li><a href="#topic5">Why pudding? </a></li> </ol> <h2 id="topic1">The rise of pudding</h2> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ac tempor orci, in pharetra leo. Aliquam rhoncus vulputate turpis. Vestibulum sed eros eu dolor pellentesque suscipit. Sed vitae elit dui. Sed finibus eleifend rhoncus. Curabitur aliquam interdum elit, ut dignissim lorem facilisis eu. In hac habitasse platea dictumst. Quisque at dui ante. Suspendisse et tortor aliquam enim tristique viverra vitae et risus. </p> <h2 id="topic2">The fundamentals of pudding</h2> <p>Nam at ligula lacinia, pretium ante eget, euismod neque. Curabitur vitae lectus nisl. Nullam finibus quam justo, ac interdum arcu congue a. Proin finibus imperdiet semper. Aliquam ac lacus quis sem accumsan gravida vel a diam. Pellentesque sed ex mollis, sagittis massa quis, tempus libero.</p> <h3>Proin finibus imperdiet semper</h3> <p> Etiam venenatis elit eget quam consectetur tincidunt. Aenean sed justo faucibus, vulputate lorem eget, malesuada nulla. Nullam interdum, ante eget elementum pulvinar, nulla nunc sodales tellus, sit amet porta odio orci a ex. Morbi ullamcorper urna non lectus faucibus fringilla nec a eros. Duis euismod congue lacus in luctus. Sed elementum vitae diam sed consectetur. Praesent id varius nisi. Aenean placerat tristique ex. </p> <h3>Class aptent taciti sociosqu</h3> <p> Etiam venenatis elit eget quam consectetur tincidunt. Aenean sed justo faucibus, vulputate lorem eget, malesuada nulla. Nullam interdum, ante eget elementum pulvinar, nulla nunc sodales tellus, sit amet porta odio orci a ex. Morbi ullamcorper urna non lectus faucibus fringilla nec a eros. Duis euismod congue lacus in luctus. Sed elementum vitae diam sed consectetur. Praesent id varius nisi. Aenean placerat tristique ex. </p> <h2 id="topic3">Major historical pudding events</h2> <p>Curabitur egestas mauris non fringilla maximus. Integer eleifend consequat nisl et porta. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed eget ipsum in lorem consectetur vehicula. Integer porta felis a malesuada lobortis. Quisque vulputate ligula non enim rutrum tempor. Suspendisse eget sapien lectus. Aenean non mattis felis. Aliquam eget iaculis augue, et placerat magna. Donec vestibulum lectus ut laoreet condimentum. </p> <h2 id="topic4">Famous people who don't like pudding</h2> <p>Nam auctor malesuada blandit. Nam tincidunt, massa at sodales interdum, elit justo iaculis libero, quis varius sem nibh id purus. Duis rhoncus volutpat laoreet. Suspendisse imperdiet erat eget purus lacinia, id finibus magna scelerisque. Morbi malesuada convallis vestibulum. Etiam rutrum eget mi eu venenatis. Donec eget egestas ligula, id feugiat dui. Praesent a magna vel justo molestie bibendum in quis ligula. Etiam sodales nunc et libero aliquam faucibus sit amet non eros. Vivamus pellentesque neque id molestie fringilla. Integer gravida ultricies efficitur. Integer in nibh sit amet turpis fermentum gravida. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi elementum volutpat sem, vel fermentum ligula molestie gravida. </p> <h2 id="topic5">Why pudding?</h2> <p>Aliquam erat volutpat. In varius nibh sit amet tempor auctor. Quisque massa ipsum, ornare at augue vel, bibendum condimentum purus. Sed fringilla consectetur velit, eget dapibus mi gravida id. Ut eget tincidunt lacus. Quisque leo velit, aliquam in orci a, dignissim molestie ex. Nunc vulputate est sit amet lacus sagittis, sit amet condimentum nunc blandit. Donec varius lacus laoreet, posuere purus eu, mollis risus. Pellentesque facilisis nibh nec feugiat blandit. Nunc a lacus at purus porttitor aliquet vel ac nisi. Nullam sit amet nisl at quam molestie ornare. Pellentesque sit amet tortor fringilla, facilisis nisl vitae, porta est. </p> </body> </html>

Let us quickly take the content value apart (content: "(page: " target-counter(attr(href url), page) ")";). The attr(href url) is a reference to the href attribute of the current element, the url is the type or unit keyword, in this case we expect a url string. The target-counter() will then retrieve the counter named page from the element.

Bookmarks

PDFs support bookmarks which are sometimes referred to as outlines. These are generic tables of contents that can be accessed by the viewer, so that they can easily navigate through a document. CSS lets you define which elements should be added to the bookmarks. By default, all headlines are added according to their level.

There are three CSS properties that can be used to fine tune your bookmarks.

  1. bookmark-label
  2. bookmark-level
  3. bookmark-state
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>bookmarks.html</title> <style> @page { size: 100mm 150mm; margin: 10mm; } h1 { bookmark-label: attr(title) ": " content(); } </style> </head> <body> <h1 title="Foo">H1 1</h1> <h2>H2 1.1</h2> <h2>H2 1.2</h2> <h3>H3 1.2.1</h3> <h3>H3 1.2.2</h3> <h2>H2 1.3</h2> <h2>H2 1.4</h2> <h2>H2 1.5</h2> <h4>H4 1.5.X.1</h4> <h1 title="Bar">H1 2</h1> <h4>H4 2.X.X.1</h4> </body> </html>

By default, the elements h1, h2, etc. are already configured for the bookmark-levels and labels, but you can always overwrite this default behavior. In the example above, we prefix the bookmark label of the h1 element by the elements title.

Named references

Similar to bookmarks, there are also named references. Named references are created automatically and work in a similar way as in html, when using an id attribute on elements. These ids can then be used in the url and allow you to jump straight to the part given by the id.

e.g. /catalog.pdf#prices

I hope you enjoyed this quick introduction to print CSS and that it’s given you everything you need to get started on your next pdf project.