Skip to content

Instantly share code, notes, and snippets.

@ry5n
Created March 13, 2012 04:06
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save ry5n/2026666 to your computer and use it in GitHub Desktop.
Save ry5n/2026666 to your computer and use it in GitHub Desktop.
An alternative to Compass's built-in vertical-rhythm module. Only supports output values in rem, with pixel fallbacks
// Configurable variables
// ⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻
// Absolute height of body text, in pixels
$base-font-size: 16px !default;
// Absolute height of one line of type, in pixels
$base-line-height: 24px !default;
// The font unit to use when returning values in rhythm functions
$rhythm-font-unit: px !default;
// Allows the `adjust-font-size-to` mixin and the `lines-for-font-size` function
// to round the line height to the nearest half line height instead of the
// nearest integral line height to avoid large spacing between lines.
$round-to-nearest-half-line: true !default;
// Ensure there is at least this many pixels
// of vertical padding above and below the text.
$min-line-padding: 2px !default;
// Constants
// ⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻
// Most (all?) browsers use a default of 16px for type.
$browser-default-font-size: 16px;
// The height of one line of type, in rems.
$rem-line-height: $base-line-height / $base-font-size * 1rem;
// Moving parts
// ⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻⁻
// Given pixel inputs, print rem values with pixel fallbacks.
// Based on Bitmanic's rem mixin (https://github.com/bitmanic/rem)
//
// $property - The css property name
// $px-values - The value or values (space-separated list) for $property, in pixels
@mixin px-to-rem($property, $px-values) {
// Number of pixels in 1rem (default: 16px/rem)
// When converting to rems, we must divide by this ratio.
$px-per-rem: $base-font-size / 1rem;
// Print the pixel fallback declaration first so we can override it in capable browsers.
#{$property}: $px-values;
// If there is only one value, print the declaration with the value converted to rems.
@if type-of($px-values) == "number" {
#{$property}: $px-values / $px-per-rem;
}
@else {
// Otherwise, we've got a list and we'll need to convert each value in turn.
// Create an empty list that we can dump values into.
$rem-values: ();
@each $value in $px-values {
// If the value is zero, a string or a color, leave it be.
@if $value == 0 or type-of($value) == "string" or type-of($value) == "color" {
$rem-values: append($rem-values, $value);
} @else {
$rem-values: append($rem-values, $value / $px-per-rem);
}
}
// Print the property and its list of converted values.
#{$property}: $rem-values;
}
}
// Return the height of n baselines.
// Returns px or rem based on the value of $rhythm-font-unit.
@function rhythm($lines: 1) {
$line-height: if($rhythm-font-unit != px, $rem-line-height, $base-line-height);
@return $line-height * $lines;
}
// Calculate the number of baselines required to accomodate a
// given pixel-based font size.
@function lines-for-font-size($font-size) {
$lines: if(
$round-to-nearest-half-line,
ceil(2 * $font-size / $base-line-height) / 2,
ceil($font-size / $base-line-height)
);
@if $lines * $base-line-height - $font-size < $min-line-padding * 2 {
$lines: $lines + if($round-to-nearest-half-line, 0.5, 1);
}
@return $lines;
}
// Set type size and baseline grid on the root element.
@mixin establish-baseline {
html {
$new-font-size: $base-font-size / $browser-default-font-size * 100%; // eg. 16px ÷ 16px * 100%
// Only set the font size if it differs from the browser default
@if $new-font-size != 100% {
font-size: $new-font-size;
}
@include set-leading(1);
}
}
// Set the font size to the specified number of pixels while
// maintaining the vertical rhythm.
//
// $to-size - Desired font size, in pixels
// $lines - Desired leading, expressed in baselines (can fractional)
@mixin set-font-size($to-size, $lines: lines-for-font-size($to-size)) {
@include px-to-rem(font-size, $to-size);
@include set-leading($lines);
}
// Adjust the leading to a new multiple of the baseline
@mixin set-leading($lines) {
@include px-to-rem(line-height, $base-line-height * $lines);
}
// Apply leading whitespace
@mixin leader($lines: 1, $property: margin) {
@include px-to-rem(#{$property}-top, rhythm($lines));
}
// Apply trailing whitespace
@mixin trailer($lines: 1, $property: margin) {
@include px-to-rem(#{$property}-bottom, rhythm($lines));
}
// Apply leading whitespace as padding
@mixin padding-leader($lines: 1) {
@include leader($lines, padding);
}
// Apply trailing whitespace as padding
@mixin padding-trailer($lines: 1) {
@include trailer($lines, padding);
}
// Apply leading and trailing whitespace together.
// Defaults to equal margins above and below and no padding.
@mixin rhythm-spacing($leader: 1, $trailer: $leader, $padding-leader: 0, $padding-trailer: $padding-leader) {
@include leader($leader);
@include trailer($trailer);
@include padding-leader($padding-leader);
@include padding-trailer($padding-trailer);
}
// Apply a border to one side of an element without throwing off
// the vertical rhythm. The available space ($lines) must be
// greater than the width of your border.
@mixin side-rhythm-border($side, $lines: 1, $border: $rule-width $rule-style $rule-color) {
$width: nth($border, 1);
$style: nth($border, 2);
$color: nth($border, 3);
@include px-to-rem(border-#{$side}, $width $style $color);
$padding: ($base-line-height - $width) * $lines;
@include px-to-rem(padding-#{$side}, $padding);
}
// Apply a leading rhythm border
@mixin leading-border($lines: 1, $border: $rule-width $rule-style $rule-color) {
@include side-rhythm-border(top, $lines, $border);
}
// Apply a trailing rhythm border
@mixin trailing-border($lines: 1, $border: $rule-width $rule-style $rule-color) {
@include side-rhythm-border(bottom, $lines, $border);
}
// Apply borders equally to all sides with padding to maintain the vertical rhythm
@mixin rhythm-borders($lines: 1, $border: $rule-width $rule-style $rule-color) {
$width: nth($border, 1);
$style: nth($border, 2);
$color: nth($border, 3);
@include px-to-rem(border, $width $style $color);
$padding: ($base-line-height - $width) * $lines;
@include px-to-rem(padding, $padding);
}
@drejohnson
Copy link

This is great, thanks

@drejohnson
Copy link

Just noticed that using 'set-font-size' mixin doesn't keep to the same vertical rhythm as using 'adjust-font-size-to'. The line-height is not the same. Any ideas why?

For example, if I add:
h1 { @include adjust-font-size-to(42px) }
It ouputs:
h1 {
font-size: 2.625rem;
line-height: 1.238em;;
}
But if I add:
h1 { @include set-font-size(42px) }
It ouputs:
h1 {
font-size: 42px;
font-size: 2.625rem;
line-height: 52px;
line-height: 3.25rem;
}

@ry5n
Copy link
Author

ry5n commented Jul 8, 2012

@drejohnson Thanks for taking an interest. It's a good question.

The adjust-font-size-to mixin outputs line-height in em units (by default), which are relative to font size of the element on which they're applied: for a font-size of 42px, a line-height of 1.238em computes to 52px (1.238 × 42px = 52px). The custom set-font-size mixin outputs rem units, which are always relative to the font-size on the root element (<html>); rem is short for "root-em". So assuming a base font size of 16px, a line-height of 3.25rem computes as 3.25 × 16px = 52px. Note that those computed values are the same, and they should look the same on the rendered page. If they're not, please let me know. There might be something else going on, or I might have a bug.

To step back a bit from the details, basically the goal here is to make baseline grids and vertical rhythm easier. The central goal of setting type to a vertical rhythm is that the baseline remains constant regardless of other typographic changes. By using rems we get scalable text (like with ems) but we avoid the extra complication that comes from the line-height being dependent on the current font-size.

Last but not least, this gist is an older version of what I currently use. Although simpler, I don't like the fact that it forks something in Compass core, and makes API changes. I've since integrated the advantages of this gist into my fork of Compass, and it's in review for possible inclusion in Compass core. Take a look if you're interested. I'd also be happy to answer any other questions.

@ry5n
Copy link
Author

ry5n commented Jul 8, 2012

To clarify why rems help even when we have Sass to help us with the math, consider the following situation: I have an h1 set to 42px size, but within that heading I have some secondary text that I want to be 38px. If I'm using ems (as Compass' vertical rhythm module does by default), the following won't work:

$base-font-size: 16px;
$base-line-height: 24px;
// etc.
.subhead {
  @include adjust-font-size-to(38px);
}

This will result in the following CSS

.subhead {
  font-size: 2.375em;
  line-height: 1.263em;
}

The resulting font-size is intended to compute to 38px, but because its parent element's font-size is 42px, not 16px, the computed font-size will be gigantic. Likewise, the line-height won't be correct. Compass solves this by requiring us to tell it what the current font-size is, if it's not the default:

$base-font-size: 16px;
$base-line-height: 24px;
// etc.
.subhead {
  // using mixin adjust-font-size-to($to-size, $lines, $from-size)
  @include adjust-font-size-to(38px, 2, 42px);
}

That's a lot of finicky work for something that's trying to make our lives easier! Using rems we can always say @include adjust-font-size-to(38px); and not worry about the font-sizing context.

@darekrossman
Copy link

This is all I needed to get away from Compass completely. Thanks so much.

One thing that I found was that @mixin side-rhythm-border didn't calculate padding like I thought it would (unless I'm missing something intentional).

$padding: ($base-line-height - $width) * $lines;

The rem values aren't being calculated properly. For example:

(24px - 1px) * 0.5 = 11.5  // expected 11
(24px - 2px) * 0.25 = 5.5  // expected 4

Just swapping the order of operations fixed it right up:

$padding: ($base-line-height * $lines) - $width;

Again, this is fantastic stuff, thanks!

@paolodina
Copy link

I used this v/r alternative to create a simple typography module. I noted that vertical rhythm maintains perfectly in Firefox (30/Linux) but not in Chrome (35/Linux). I wonder if I did something wrong or if it's something in the vertical rhythm mixins. If someone happen to have a look and advise I'd really appreciate that, here is the minimal code showing the issue.

@raahede
Copy link

raahede commented Jan 31, 2015

Hey @ry5n - nice little set of mixins you've made.

I've made an addition in my own fork because I needed to address <hr>s. Check it out:

@mixin rhythm-rule($lines: 3, $rule: $rule-width $rule-color) {
    $width: nth($rule, 1);
    $color: nth($rule, 2);
    background-color: $color;
    @include px-to-rem(height, $width);

    $margin: ( $base-line-height * $lines - $width ) / 2;
    @include px-to-rem(margin-bottom, $margin);
    @include px-to-rem(margin-top, $margin);
}

I've used it like this:

hr {
    border: none;
    @include rhythm-rule;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment