Streamlining CSS Print Design with Sass
Three years ago, my colleagues and I in O’Reilly Media’s Production department made the decision to rearchitect our print-publishing software toolchain to support typesetting print books in HTML and CSS. Doing print layout with web technology was a fairly radical notion at the time (and still is today!), especially in traditional publishing-industry circles where commercial desktop-publishing software continues to hold sway. But we were convinced that aligning our publishing tech with the web stack would pay dividends. Short-term, we knew it would enable us to simultaneously produce print and digital media more efficiently. And long-term, we felt that placing our bets on HTML+CSS was the best way to future-proof our workflows as electronic publishing, both online and offline, continued to evolve.
Transitioning to HTML as our canonical content source format immediately allowed us to realize many benefits, including Web-based authoring and a digital-first approach to next-gen ebook development. Building print templates in CSS also proved to be surprisingly straightforward, once we got up to speed on Paged Media, and the particular dialect of it spoken by our PDF formatter software. We were able to quickly implement CSS templates for a wide variety of book series interior designs:
However, as we completed a critical mass of templates and moved from the proof-of-concept phase to production, CSS maintenance considerations rose to the fore. In our initial rollout of Paged Media designs, we built one set of core CSS files that contained a complete implementation of the styling for all standard book elements (headers, footers, chapter openers, tables, figures, etc.) for one of our simplest and most popular interior designs. CSS for all other interiors designs then @imported this core.css and performed selective overrides to adjust styling as appropriate.
This approach served us well for a while, but as we added more and more templates, the monolithic nature of our CSS architecture became a liability. We had amassed a mountain of customizations that were piled on top of — and somewhat precariously balanced on — the style base. For any given template, it required effort and care to distinguish the aspects of the design that had dependencies on the base versus those that were overrides of the base. Changes to core.css were thus especially fraught, as they could have unanticipated ripple effects on numerous designs.
With each new template, the compromise here felt increasingly unpleasant, so we swung the pendulum in the other direction and rearchitected our CSS to take the opposite approach: eliminate core.css entirely, and implement a distinct, independent set of CSS stylesheets for each interior design.
Now, happily, each design was completely siloed. There was zero chance of code changes to one template having side effects on another. But sadly, each design was completely siloed. The common CSS that designs formerly shared was now replicated in multiple independent codebases, which made global updates a tedious, error-prone chore.
It was clear we needed a more nuanced, middle-ground approach to CSS development. Stepping back a bit to look at the big picture, these were the goals we wanted to accomplish:
- Keep templates independent to avoid convoluted cascading, but share common code to eliminate redundancy.
- Refactor templates encompassing 2,000+ lines of CSS to make them more human-readable and -editable.
- Make it easier for non-designers on the Production team to make simple “oneoff” customizations for individual books (such as adjusting color scheme) without having to wade through those 2,000+ lines in the first place.
While our overarching aim was to make our print-design workflows more efficient, none of the above challenges was at all print-specific, which led us toward exploring CSS maintenance solutions adopted by the greater web design community. Inspired by Lea Verou, who designed the print stylesheets for CSS Secrets using Sass, we decided to explore the Sass language to see if we could leverage CSS preprocessing to tackle the above challenges.
Over the course of several months of experimentation, we found that we were indeed able to apply many Sass language features within a print-design context to achieve our goals of streamlining template development and maintenance. Here’s a look at some key principles we put into practice when refactoring our stylesheets using Sass. (Note: all examples in the following sections use Sass’s SCSS syntax, which is a superset of CSS syntax.)
Encapsulate shared code using mixins
Our first objective in refactoring our stylesheets with Sass was to eliminate duplicate code across our templates. Sass’s @mixin functionality was a great solution to this problem. Mixins allow stylesheet developers to define modular styling blocks, which can contain a set of properties and/or full rules, e.g.:
@mixin heading-style {
font-family: Arial, sans-serif;
font-weight: 600;
text-transform: capitalize;
}
that can then be inserted via @include statements:
h1, h2, h3 {
@include heading-style;
}
When the above Sass is compiled to CSS, this is the result:
h1, h2, h3 {
font-family: Arial, sans-serif;
font-weight: 600;
text-transform: capitalize;
}
Mixins are far more flexible than standard CSS @imports, because they can be interpolated anywhere within a stylesheet, as many times as needed. This makes it possible to create a library of frequently used styling modules that can be dropped in whenever and wherever appropriate.
We did an audit of our stylesheet corpus and identified many such modules that were being replicated verbatim in every template. These styling blocks were clearly globally applicable to a wide variety of designs, and encompassed handling ranging from hyphenation rules and PDF bookmark configuration to syntax highlighting for code listings and MathML equation handling. We removed all this CSS from individual template files and placed it into a new mixins library that was accessible to every theme:
@mixin hyphenation-rules {
hyphens: auto;
hyphenate-before: 3;
hyphenate-after: 3;
-ah-hyphenate-hyphenated-word: false;
-ah-hyphenate-lines: 2;
}@mixin pdf-bookmarks {
figure[data-type="cover"] {
bookmark-level: 1;
bookmark-state: closed;
bookmark-label: "Cover";
} section[data-type="copyright-page"] {
bookmark-level: 1;
bookmark-state: closed;
bookmark-label: "Copyright";
}
}
@mixin syntax-highlighting {
/* Redacted for space; you get the idea... */
}
This solved our duplicate-code problem, while simultaneously modularizing shared code into manageable, clearly labeled blocks. We invoked mixins only when needed, at the exact locations they were needed, making it much easier to pinpoint dependencies in any given template and foresee the implications of changes to these shared resources.
Use variables to enhance readability/maintainability
Someday native CSS syntax for variables will be broadly implemented, but that day is not today. Until then, one of Sass’s most compelling features is its support for variables. In Sass, variable names start with a $ and are defined using the same syntax as properties:
$my-favorite-color: #008700;
Somewhat ironically, we found Sass variables immensely beneficial when employed as constants, allowing us to attach human-friendly names to otherwise opaque Unicode and color values.
For example, take the following snippet:
blockquote p.attribution::before {
content: "\2014";
}
Unless you have the Unicode codepoint memorized, it’s likely not readily apparent that the character being inserted before p.attribution elements (U+2014) is an em dash.
By contrast, if the following variable is defined in the Sass stylesheet:
$em-dash: "\2014";
The above rule can now be rewritten as:
blockquote p.attribution::before {
content: $em-dash;
}
Which is much easier to grok at a glance, relieving much cognitive overhead for the beleaguered designer.
In addition to augmenting readability, Sass variables-as-constants also enhance stylesheet maintainability. Take the following example, where a variable for a company’s logo background color is defined:
$logo-background-color: $trademarked-shade-of-crimson;
And then invoked potentially dozens of times throughout the stylesheet. Should the company change the logo background color from $trademarked-shade-of-crimson to $trademarked-shade-of-chartreuse, a single variable declaration can be updated, obviating the need for global find-and-replace. 👏
Use variables to enable end-user customization
Our Production team often needs to make template customizations on a book-by-book basis to perform minor design adjustments. So when we first rolled out our new CSS book templates, we knew that we’d need to build in the capability to override styling for individual books. Our original solution was to add support for a oneoff.css file that could be added to any book project. The oneoff.css @imported the designated template CSS, and then overrode styling as appropriate.
This served us reasonably well, but the main shortcoming of this approach was that it required the Production team to be conversant with the base template CSS to successfully implement overrides. We did our best to lower overhead by including boilerplate snippets in the oneoff.css that could be toggled on/off as needed, e.g.:
/*---- Uncomment to turn on automatic code wrapping
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
----*/
But we wanted to see if we could leverage Sass to implement a more elegant solution that eliminated the need to maintain that extra level of CSS. Perhaps instead of modifying styling via oneoff CSS override files, we could define parameters for frequently used design customizations (e.g., code-wrap=true) and pass them into our Sass stylesheets as variables?
Unlike Less, which offers a --global-var option for passing variable values into stylesheets, the Sass preprocessor does not directly accept custom parameters. So we hacked the same functionality by replacing our oneoff.css with a _variables.scss partial file that defined only the custom design parameters (no styling) needed for a given book:
$trim-width: 6in;
$trim-height: 9in;
$code-wrapping: false;
/* And so on... */
Then the template Sass stylesheets @imported _variables.scss, incorporating its parameter values.
Here’s an example showing this approach in action. The following @page declaration uses the $trim-width and $trim-height values defined in _variables.scss to dynamically set PDF trim size:
@page {
size: $trim-width $trim-height;
}
And then the following SCSS employs Sass math operations to calculate $figure-width based on $trim-width, and set image max-width accordingly:
$figure-width: $trim-width - 2.2in;img {
max-width: $figure-width;
}
As we built out our custom-parameter architecture, we realized that requiring users to configure a full set of parameters in _variables.scss for each individual book would likely be a real nuisance. To make our lives easier, we defined default values for all parameters in the template CSS using Sass’s !default flag:
$trim-width: 7in !default;
$trim-height: 9.1875in !default;
Then we only needed to configure a parameter in _variables.scss when we wanted to override one of the defaults. Not only was this architecture much more efficient, but more extensible, too. If we needed to add new custom parameters in the future, we could populate them with default values in the template stylesheets, making it unnecessary to retroactively update all the _variables.scss files for legacy books.
DIY “media queries” with SassScript
When developing responsive stylesheets for the Web, the key tool in the designer’s arsenal is the media query, which makes it possible to adapt styling based on features of the browser environment — width and height, orientation (portrait or landscape), resolution, and so on:
@media (min-width: 900px) {
/* CSS HERE IS APPLIED ONLY IF VIEWPORT WIDTH EXCEEDS 900px */
}
Ever since our early days of print typesetting with CSS, we desperately wanted the ability to craft our own custom media queries for different “print book environments.” For example, if a book were to be printed with a one-color (black-and-white) interior, we might want table header rows to be shaded gray, but if it were printed with a four-color (CMYK) interior, we might want table header rows shaded $trademarked-shade-of-chartreuse.
Ideally, we wanted to be able to write queries like the following:
@media print and (print-book-color-count: 4) {
th { background-color: $trademarked-shade-of-chartreuse }
}
Sadly, the print-book-color-count media feature did not exist in the W3C Media Queries spec or in our PDF formatter’s documentation; it was viable only in our hearts.
But when we started exploring SassScript, we realized we had an equivalent solution at our fingertips. We could create the media features we wanted by defining custom parameters (see previous section), and then use @if/@else directives to craft our own queries.
To implement the above example, we defined the interior color count in our _variables.scss:
$print-book-color-count: 4;
And then added an @if/@else block to specify styling for each possible condition:
@if $print-book-color-count == 4 {
th { background-color: $trademarked-shade-of-chartreuse; }
/* More 4-color styling here */
} @else {
th { background-color: gray; }
/* More 1- or 2-color styling here */
}
With this DIY “media query” approach in place, our stylesheets could easily adapt to any combination of print-book specs. This allowed us to take what had formerly been multiple distinct templates — “trade book,” “trade book four-color,” “trade book small trim,” “trade book four-color small trim” — and collapse them into one “smart” parameterized template (“trade book”). Huge boon for maintainability!
Except the unexpected
By opening up our stylesheets to accept user-supplied values for parameters like $print-book-color-count, we knew we were also introducing the possibility of unexpected inputs, e.g.:
$print-book-color-count: "pizza!";
To address this, we wanted to add error handling to our stylesheets to validate parameter values and raise exceptions in cases where inputs did not meet template requirements.
One area where validation was essential was for our parameters defining the trim size for book pages: $trim-width and $trim-height. Many of our templates were intended to be compatible with only a handful of whitelisted trims. For example, one template was designed to support a trim of either 6in×9in or 7in×9.1875in, and we wanted a stylesheet error to be thrown if $trim-width and $trim-height did not conform to one of these specifications.
SassScript provides two directives that can be used for exception handling: @warn, which can be used to output warning messages to stderr, and @error, which both outputs an error messages to stderr and terminates the Sass preprocessor.
To ensure preprocessing failed on bad trim inputs, we added the following SCSS to our template stylesheet:
@if not (($trim-width == 6in and $trim-height == 9in) or ($trim-width == 7in and $trim-height == 9.1875in)) {
@error("Invalid trim size. Trims supported are 6in×9in or 7in×9.1875in")
}
In other circumstances, bad input values did not necessitate terminating the preprocessor, as we could gracefully recover by using appropriate defaults. In these cases, we used @warn to alert users to the fallback behavior:
@if $color-count > 4 {
@warn("Invalid color count of #{$color-count}. Defaulting to maximum color count of 4");
$color-count: 4;
}
Having @error and @warn handling in place protected the integrity of our designs and provided salient feedback to users configuring them in inappropriate ways.
Conclusion
At time of publication, we have officially deployed and launched Sass support in our publishing toolchain, and are releasing our first set of Sass stylesheets into production. In the coming months, we’ll be continuing to add new designs to our Sass stylesheet corpus, as well as iteratively improve existing templates. We’re looking forward to sharing more best practices we learn along the way!