Dealing with significant white space in HTML

For most of the web’s content whitespace is not significant. Browsers will collapse multiple spaces or tabs into a single space and will only insert line breaks when they encounter the <br /> element or at the end of a paragraph (either with the closing tag or when they encounter a new element).

But there are times when whitespace is significant. Take poetry… depending on the type of poem you’re typesetting you may find different indentations and characters in different columns and things like that

A summary of the ways to handle white space is shown below (taken from CSS Tricks)

  New lines Spaces and tabs Text wrapping
normal Collapse Collapse Wrap
pre Preserve Preserve No wrap
nowrap Collapse Collapse No wrap
pre-wrap Preserve Preserve Wrap
pre-line Preserve Collapse Wrap

Pre

The oldest and most backward-compatible option is to use the pre element. This will ignore all restrictions regarding white space in the text, except for line breaks.

By default, it’ll render the text in a monospaced font. You can override this by using font-family to declare a web font or another pre-defined font family.

CSS: white-space: pre

using white-space: pre is the CSS equivalent of using the pre element except that it’s not as widely supported for older browsers.

The main difference is that you can attach the rule to any element, not just pre and save yourself the font declaration.

CSS: whitespace: no-wrap

The next white space variation is no-wrap. This option will remove all white spaces in the text and newlines.

This is what we want, however it may be too much for our purpose of typesetting text while preserving whitespace; new lines and line breaks are ignored so you’ll get long lines of text instead of the formatted text you’re looking for.

CSS: whitespace: pre-wrap

pre-wrap will preserve spaces and newlines but it will also wrap the text around to the next line. It’s the closest to what we want when typesetting material where whitespace is significant.

CSS: whitespace: pre-line

pre-line works similarly to pre-wrap; it will collapse spaces, preserve newlines and wrap text. The difference is that pre-line will collapse spaces and tabs in the text, making it less useful for typesetting.

Another Alternative: Upcoming in CSS

In the forthcoming CSS Text Level 4 developers will be able to use a combination of text-space-collapse, text-space-trim and text-wrap to tell the browser how they want to handle white space.

Be careful if you implement these properties in modern browsers, according to caniuse.com entry on CSS text-space-collapse property:

This CSS property (formerly known as white-space-collapse or white-space-collapsing) is not supported in any modern browser, nor are there any known plans to support it.

Which is alarming because the only reference in the specification is in section 3.1 where there is an outstanding issue:

Issue 4: This section is still under discussion and may change in future drafts.

So take it with a grain of salt and test whatever solution you end up using.

Links and Resources

Animation and effects with CSS

I’ve been become more comfortable playing with CSS features that are beyond just making sure that typography works well in all form factors.

Rotate On Hover

One of the things that I like is the subtle and fun rotation that Smashing Magazine does with the author’s photo when you first visit an article and how it moves to a fully vertical position when you hover over the image. I’ve always thought that that’s backward and that it should move to the diagonal position when you hover over the image… so let’s try to do it that way.

The default state for the image sets the dimensions and a border (I like borders on images).

img {
  border: 5px solid rebeccapurple;
  height: 150px;
  width: 150px;
}

The change happens in the hover state. Here we do two things:

  • We tell the browser that we want the rotation to happen from the bottom left corner
  • We use rotate to move the image -15 degrees (15 degrees counterclockwise)
img:hover {
  transform-origin: bottom left;
  transform: rotate(-15deg);
}

The basics work but there’s one more thing that I want to do. Transform allows us to chain effects so why not make the image slightly bigger as we rotate it? The modified code adds a scale transform to the existing rotation.

img:hover {
  transform-origin: bottom left;
  transform: scale(1.2) rotate(-15deg);
}

You can see the finished product in this pen

We can also choose to rotate the image, with our without the scaling, and do nothing with it on hover:

img {
  border: 5px solid rebeccapurple;
  height: 150px;
  width: 150px;
  transform-origin: bottom left;
  transform: rotate(-15deg);
}

Enlarging text on hover

One thing that I wish we could do is make the links slightly larger on hover so the text of the link is more readable when the user mouses over it.

Because we don’t want the reader to get confused between the enlarged link text and the surrounding content we made the background black and the text white.

a:hover {
  display: inline-block;
  background: black;
  color: white;
  transform-origin: bottom-left;
  transform: scale(1.25);
}

Enlarge a search box

Some of my favorite sites expand the search box when users hover over the search box to a pre-set width.

Based on Searchbox Pure CSS Hover transition I built the following demo to test such an idea.

The markup

The example uses font awesome for the search icon. It would probably work better if we were to use an SVG icon… but for the demo, it’ll be fine.

<div class="search-container">
  <div class="search-box">
    <input
      class="s-box"
      type="text"
      name="search"
      placeholder="Enter search parameters">
    <a class="s-btn" href="">
      <i class="fas fa-search"></i>
    </a>
  </div>
</div>

In the (S)CSS side, we first defined the container as a flex container, set up as a row and aligned to the end of the document (right side in left-to-right languages).

.search-container {
  display: flex;
  flex-flow: row;
  justify-content: flex-end;
}

The next step is to set up the dimensions and look of the search-box container.

Using the ampersand to create nested selectors for the button and hover states. These are the styles that we’ll animate to when we hover over the element.

.search-box {
  height: 40px;
  border: 2px solid rebeccapurple;
  border-radius:40px;
  padding: 10px;
  &:hover{
    .s-box{
      width: 240px;
      padding: 0 6px;
    }
    .s-btn{
      background:#fff;
    }
  }

The button (class s-btn) contains the icon we’re using to represent the search. It is also where we would attach the click event for processing the form.

The button is also a flex container so we can center the icon both vertically and horizontally inside its parent.

.s-btn {
  color: rebeccapurple;
  float: right;
  width: 40px;
  height: 40px;
  transition: 0.4s;
  display: flex;
  justify-content: center;
  align-items: center;

  i {
    font-size: 20px;
  }
}

The final element to style is the input element where we’ll type what we want to search for.

  .s-box{
    border: none;
    background:none;
    outline:none;
    float: left;
    padding: 0;
    color:rebeccapurple;
    font-size:1rem;
    transition:.5s;
    line-height:40px;
    width: 0px;
  }
}

The product takes more work than I thought but the result still looks nice as I expected.

Center vertically and horizontally

I was working on typesetting poems when I thought what it would take to center a piece of content both vertically and horizontally?

For image and smaller blocks of text like poems, I choose to work with flexbox as it provides, in my opinion, the simplest way to center content on both axes without scripting.

The code looks like this:

.container {
  height: 100vh;
  display: flex;
  flex-flow: column;
  justify-content: center;
  align-items: center;
}

Some caveats:

  • You must set an explicit height for the container, in this case, 100vh. Otherwise, there is no height to center to
  • Because we’re centering all the content it may be a good idea to create an inner container or paragraphs and other individual elements may be centered and cause unexpected results.

You can see working examples of this in my poema 6 and poema 19 demos. An editable example of Poema 6 is in Codepen.

Generating footnotes on a web page

As I was working on my last post about paged media I discovered one thing we can do to make footnotes for the web a little more user-friendly. Paged Media has a way to hide the text of the footnote from being displayed on the page… wouldn’t it be nice to do that with web content as well without having to change the document we’re working with?

CSS on its own will take you part of the way there… so let’s explore how can Javascript help. What we want to accomplish:

  1. Hide all the elements with class footnotes
  2. Add a number corresponding to the number of the footnote
    • If possible make it a hyperlink
  3. Create an ordered list
  4. Create a link for each footnote pointing to the footnote number

First, we define the code we’ll use to generate the footnotes. By using a span with an associated class

<span class='footnote'>Another footnote</span>

On the Javascript side, we capture all the footnotes using querySelectorAll to get a list of all the elements that match the .footnote selector.

Even though the querySelectorAll returns a nodeList, not an array. While we can still use the forEach method, it has none of the other array methods. If we need other array methods we can covert the list into an array using destructuring assignments or array.form to create an array you can operate on. To be on the safe side, I used destructuring to convert the list into an array.

const footnotes = document.querySelectorAll('.footnote');
fnArray = [...footnotes];

Now that we have the array, the first step is to generate the footnote marker and link. We use (i+1) instead of just i because arrays start at 0 instead of 1.

Once we have the footnote marker we insert it into the document adjacent to the footnote text; only then we can remove the footnote text from the document, otherwise, we would have nowhere to insert the marker.

fnArray.forEach(function(elem, i) {
  let reference = document.createElement('sup');
  reference.setAttribute('id', i + 1);

  // Create the link and attributes
  let link = document.createElement('a');
  link.setAttribute('href', '#fn' + (i + 1));
  link.innerText = i + 1;

  // Append the link to the reference element
  reference.appendChild(link);

  // Insert the linked reference to the page after
  // the footnote text
  elem.insertAdjacentElement('afterend', reference);

  // Hide the footnote text
  elem.style.display = 'none';
});

With the markers in place and the footnote text removed we can build the list of footnotes at the end of the document.

We first create an hr element to display a horizontal rule to distinguish the footnote list from the body of the text.

We then open an ol element for the numbered/ordered list that will contain the footnotes.

// Create hr element and append it to the page
const hr = document.createElement('hr');
document.documentElement.appendChild(hr);
// Create ol element and append it to the page
const ol = document.createElement('ol');
document.documentElement.appendChild(ol);

For each footnote:

  • We create the li element that will contain the footnote
  • We create an a element that will link to the footnote marker
    • We set an href attribute pointing to the footnote marker ID
    • Set the content of the link to be the content of the footnote element we removed
  • We append the link to the list item element
  • We append the list item to the ol object
// For each footnote in the array
fnArray.forEach(function(elem, i) {
  // Create the list item
  let item = document.createElement('li');
  // Create the link, set href attribute and the text
  let link = document.createElement('a');
  link.setAttribute('href', '#' + (i + 1));
  link.innerText = elem.textContent;

  // Append the link to the list item
  item.appendChild(link);
  // Append the list item to the list
  ol.appendChild(item);
});

One thing we need to do is research the performance implications of using this script inline versus calling it from an async script tag.

A working demo is at this Codepen

Revisiting paged media stylesheets for the web

One thing that the web is sorely lacking is the ability to create print-ready content from our web pages. CSS provides specifications for paged media but the support in browsers leaves a lot to be desired, forcing people into tools that accomplish the goals of creating high-quality print content.

In many instances, I will cut pieces of the code where they are not relevant. You can find the full stylesheet along with the resulting PDF from the Github Repo

One thing we need to remember is that for this particular project we’re doing everything in the command line so download speed is not as big a concern as if we were doing this online at the same time as trying to serve our regular content.

Getting started

The first thing we do is to load the fonts using a simplified @font-face syntax. Rather than loading all the font formats as we would for a regular web page, we only load WOFF (compressed with Zopfli) to make our lives easier.

No, we cannot use Variable Fonts with Paged Media Processors.

In the next step, we’ll define some global parameters that will apply to all content.

We use the html element to define the global font for the document.

The h1 element is interesting. We capture the content of the element (the value) to use later as the running header for our different types of content.

Finally, we define the widows and orphans for the entire document.

Widow
A paragraph-ending line that falls at the beginning of the following page or column, thus separated from the rest of the text.
Orphan
A paragraph-opening line that appears by itself at the bottom of a page or column, thus separated from the rest of the text.

Be mindful that setting the values for widows and orphans to high can generate large blocks of empty space in your pages.

html {
  font-family: 'PT Serif', serif;
}

h1 {
  string-set: doctitle content();
  line-height: 1.3;
}

p {
  widows: 4;
  orphans: 4;
}

Next, we define the global page for the document. This is what all the other pages will inherit from so we save ourselves from having to retype blocks of CSS over and over.

We define the size of the printed page to be American Letter (8.5 by 11 inches) with a 1-inch margin all around.

The margin attribute takes one to four values and follows the same rules as regular CSS:

  • When one value is specified, it applies the same margin to all four sides
  • When one value is specified, it applies the same margin to all four sides
  • When two values are specified, the first margin applies to the top and bottom, the second to the left and right
  • When three values are specified, the first margin applies to the top, the second to the left and right, the third to the bottom
  • When four values are specified, the margins apply to the top, right, bottom, and left in that order (clockwise)

Next, we set what we want to put in the top right corner of the document. We indicate that we want to pull the doctitle string from h1 element for the corresponding section and that we want it to be 9 points (where 1pt equals 1/72 of an inch)

Finally, we set footnote attributes that we want to carry throughout the document.

// DEFINE THE DEFAULT PAGE */
@page {
  size: 8.5in 11in;
  margin: 1in;
  @top-right {
    content: string(doctitle);
    font-size: 9pt;
  }
  @footnote {
    counter-increment: footnote;
    float: bottom;
    column-span: all;
    height: auto;
  }
}

We use data- attributes to indicate what part of our book each element corresponds to. We avoid name collisions between stylesheets where, for the same specificity, the last rule wins.

We use CMYK colors rather than RGB(a) or HSL. This is not a requirement but I thought it was cool.

Once again we use points (pt) to define the default font size of the document.

body[data-type='book'] {
  color: cmyk(0%, 0%, 100%, 100%);
  hyphens: auto;
  font-size: 14pt;
}

We will rely extensively on counters so we need to define them and reset them the first time.

The rules below say that if the first child is a part, a chapter or an appendix to reset the counters for the appendix, chapter, figures, and tables.

However, if there’s a chapter with a sibling chapter, we don’t want to reset any counters.

body[data-type='book'] > div[data-type='part']:first-of-type,
body[data-type='book'] > section[data-type='chapter']:first-of-type,
body[data-type='book'] > section[data-type='appendix']:first-of-type {
  counter-reset: chapter;
  counter-reset: appendix;
  counter-reset: figure;
  counter-reset: table;
}

body[data-type='book'] > section[data-type='chapter'] + div[data-type='part'] {
  counter-reset: none;
}

We associate elements to pages that we’ll define later. It is at this stage where we define page breaks, counter increases and other elements that are specific to some types of pages and not others.

The code is not DRY and I’m ok with that. I’ve traded ease of reading and debugging for brevity.

// Title Page */
section[data-type='titlepage'] {
  page: titlepage;
  page-break-before: always;
  page-break-after: always;
}

// Copyright page */
section[data-type='copyright'] {
  page: copyright;
  page-break-before: always;
  page-break-after: always;
}

// Dedication */
section[data-type='dedication'] {
  page: dedication;
  page-break-before: always;
  page-break-after: always;
}

// Foreword */
section[data-type='foreword'] {
  page: foreword;
  page-break-before: always;
  page-break-after: always;
}

// Preface*/
section[data-type='preface'] {
  page: preface;
  page-break-before: always;
  page-break-after: always;
}

// Part */
div[data-type='part'] {
  page: part;
  page-break-before: always;
  page-break-after: always;
}

// Chapter */
section[data-type='chapter'] {
  counter-increment: chapter;
  page: chapter;
  page-break-before: always;
  page-break-after: always;
}

// Appendix */
section[data-type='appendix'] {
  counter-increment: appendix;
  page: appendix;
  page-break-before: always;
  page-break-after: always;
}

// Glossary */
section[data-type='glossary'] {
  page: glossary;
  page-break-before: always;
  page-break-after: always;
}

// Bibliography */
section[data-type='bibliography'] {
  page: bibliography;
  page-break-before: always;
  page-break-after: always;
}

// Index */
section[data-type='index'] {
  page: index;
  page-break-before: always;
  page-break-after: always;
}

// Colophon */
section[data-type='colophon'] {
  page: colophon;
  page-break-before: always;
  page-break-after: always;
}

The table of contents creates a leader for each item in the table of contents (a line of dots) pointing towards the page number at the far right of the entry.

// TOC */
section[data-type='toc'] {
  page: toc;
  page-break-before: always;
  page-break-after: always;
}

// Leader for toc page */
section[data-type='toc'] nav ol li a:after {
  content: leader('.') target-counter(attr(href), page);
}

Now that we’ve done the association we can define the different pages. Because we’re working with two-sided pages we have to define the items for both the right and left side pages.

All the front matter pages use lower-case roman page numbers while the rest of the content uses Arabic numbers for page numbering.

@page toc:right {
  @bottom-right-corner {
    content: counter(page, lower-roman);
  }
  @bottom-left-corner {
    content: normal;
  }
}

@page toc:left {
  @bottom-left-corner {
    content: counter(page, lower-roman);
  }
  @bottom-right-corner {
    content: normal;
  }
}

@page chapter {
  @bottom-center {
    vertical-align: middle;
    text-align: center;
  }
}

@page chapter:right {
  @bottom-right-corner {
    content: counter(page);
  }
  @bottom-left-corner {
    content: normal;
  }
}

@page chapter:left {
  @bottom-left-corner {
    content: counter(page);
  }
  @bottom-right-corner {
    content: normal;
  }
}

The next block covers footnotes. These are PrinceXML specific extensions covered in their footnotes documentation

// Footnotes */
span.footnote {
  float: footnote;
}

::footnote-marker {
  content: counter(footnote);
  list-style-position: inside;
}

::footnote-marker::after {
  content: '. ';
}

::footnote-call {
  content: counter(footnote);
  vertical-align: super;
  font-size: 65%;
}

Cross References are also possible using generated content and Prince-specific extensions.

We can target any counter available on the document and, with it, create interesting cross-references.

// XReferences */
a.xref[href]::after {
  content: ' [See page ' target-counter(attr(href), page) ']';
}

One cool thing that AntennaHouse and Prince can do is generate PDF bookmarks from the headings in your document.

We are generating bookmarks for the chapter, appendix glossary, bibliography and index sections.

h1 through h3 are open and will display the content of the h1 element as indicated.

section[data-type='chapter'] h1,
section[data-type='apendix'] h1,
section[data-type='glossary'] h1,
section[data-type='bibliography'] h1,
section[data-type='index'] h1 {
  -ah-bookmark-level: 1;
  -ah-bookmark-state: open;
  -ah-bookmark-label: content();
  prince-bookmark-level: 1;
  prince-bookmark-state: open;
  prince-bookmark-label: content();
}

h4 through h6 do not need to be open mostly because we want to use the bookmarks as navigation references so we just ass the bookmark without a state or a label.

section[data-type='chapter'] h4,
section[data-type='apendix'] h4,
section[data-type='glossary'] h4,
section[data-type='bibliography'] h4,
section[data-type='index'] h4 {
  -ah-bookmark-level: 4;
  prince-bookmark-level: 4;
}

Chapters and Appendices require some extra work. Because we usually have more than one chapter and can have multiple appendices, we are using counters and we want to indent all paragraphs after the first one we take some extra work to make sure it works correctly.

For all h1 elements inside chapter sections we want to do three things:

  • Capture the text of the element
  • Reset the figure counter
  • Reset the table counter

Using the :before pseudo-element we add a string containing the word Chapter and the current value of the chapter counter.

We repeat the same process for the appendix section or sections.

section[data-type='chapter'] h1 {
  string-set: doctitle content();
  counter-reset: figure;
  counter-reset: table;
}

section[data-type='chapter'] h1:before {
  content: 'Chapter ' counter(chapter) '. ';
}

section[data-type='chapter'] p:not(:first-of-type) {
  text-indent: 0.5in;
}

section[data-type='appendix'] h1 {
  string-set: doctitle content();
  counter-reset: figure;
  counter-reset: table;
}

section[data-type='appendix'] h1:before {
  content: 'Appendix ' counter(appendix, upper-alpha) ':  ';
}

Putting it all together

I build the files from the command line using iTerm in MacOS. The instructions should also work in Linux and Windows using WSL.

Download Prince for your operating system and install it.

I use (Dart) SASS to generate the stylesheets.

Once it’s installed run the following commands:

  • rm -rf book2.pdf to remove the pdf file. It will return without a message if the file doesn’t exist
  • sass sass:css converts files in the sass directory to the corresponding file in the css directory
  • prince index.html -s css/paged-media.css -o book2.pdf runs princeXML on index.html using the css/paged-media.css stylesheet and producing book2.pdf as the final output

Looking Forward

The version of the stylesheet we’ve presented in this post is geared towards books and other complex forms of printed content.

There is no reason why we couldn’t simplify it to work on one or more articles without having to carry the full book baggage.

There is a simplified article stylesheet in the Github Repo at sass/article.scss along with the resulting article.pdf.

Links and Resources