Advertisement

Getting the Hang of Hanging Punctuation

by
This post is part of a series called A-Z of Web Typography.
Taking the “Erm..” Out of Ems
A Beginner’s Guide to Pairing Fonts

Hanging Punctuation is a powerful typographic tool for creating optically aligned bodies of text. Unfortunately, it has been largely forgotten on the web … until now. We’ll take a look at the value of hanging punctuation and how you can partially implement it using a little javascript and a CSS rule which has been around for years.

Hanging Punctuation: A Primer

When Gutenberg was creating his Bible in the 1400’s he developed a style of typesetting punctuation marks which has endured to this day, called “hanging punctuation” (also known as optical alignment).

What is Hanging Punctuation?

Hanging punctuation is a method of setting punctuation marks outside the margins of a body of text. This creates the appearance of a uniform edge in the text and allows for a better optical flow.

Hanging punctuation impacts they way you perceive the layout of text on a page.

Where Hanging Punctuation Occurs

Hanging punctuation occurs in the margins outside the alignment of the rest of the text. You can hang punctuation in left-aligned, right-aligned, or justified text.

Why We Hang Punctuation

The theory behind why hanging punctuation is important goes something like this:

Punctuation marks in text only take up a small amount of cap height. Ordinarily this isn’t a problem, but when the punctuation mark is the first character on the straight edge of a body of text it leaves an abnormal amount of vertical spacing which can disrupt the visual flow of a body of text.

Hanging punctuation fixes this disruption. Naturally, some marks will fall on the edge of the text. Rather than leaving them there and disrupting the optical flow of the body of text, the typographer alleviates the disruption by hanging the punctuation marks in the margins.

Which Punctuation Marks Can Be Optically Aligned?

As hanging punctuation alleviates the disruption from small cap height marks, it follows that you should generally hang small cap height marks such as quotation marks (” “ ’ ‘) and hyphens (– —). You should avoid hanging full cap height punctuation marks such as the exclamation point (!) and question mark (?).

Hanging Punctuation: In Print and on the Web

Support for hanging punctuation in desktop publishing programs, like Adobe InDesign, has come a long way. InDesign provides great tools for hanging punctuation in bodies of text. Of course, that’s great for people who design using desktop publishing software, but what about web designers?

The problem with achieving hanging punctuation on the web is that everything is built with boxes. Style rules in CSS use the “box model” which wraps all HTML elements in containing boxes. These boxes consist of margins, borders, padding and content.

By default, all text characters on the web reside within a box. Hanging punctuation, by contrast, requires that characters be outside the box. This has been a tricky problem to solve with software, so implementing the typographic trick of hanging punctuation has largely been ignored on the web, by designers and browser makers alike. As Mark Boulton states, it’s a great shame that such an important aspect of typesetting has just been swept under the rug.

Hanging Punctuation and the Spec

It may surprise you, but hanging punctuation is actually in the CSS3 text module. The hanging punctuation property, however, is marked optional which contributes to the fact that no one has implemented it yet. In fact, if not implemented, the property is “at risk and may be cut from the spec during its CR period if there are no (correct) implementations”.

Since no browsers have implemented this property yet, we won’t go into implementation details for the hanging-punctuation property. However, if you are curious, you can read the spec itself or Chris Coyer’s overview on CSS Tricks for more information.

Punctuation Marks for Hanging

There is one piece of valuable information we can glean from the spec. It details which punctuation marks should be hung:

U+002C , COMMA
U+002E . FULL STOP
U+060C ، ARABIC COMMA
U+06D4 ۔ ARABIC FULL STOP
U+3001 IDEOGRAPHIC COMMA
U+3002 IDEOGRAPHIC FULL STOP
U+FF0C FULLWIDTH COMMA
U+FF0E FULLWIDTH FULL STOP
U+FE50 SMALL COMMA
U+FE51 SMALL IDEOGRAPHIC COMMA
U+FE52 SMALL FULL STOP
U+FF61 HALFWIDTH IDEOGRAPHIC FULL STOP
U+FF64 HALFWIDTH IDEOGRAPHIC COMMA

The spec states that other characters may be hung if appropriate, which is good as this list is not comprehensive. For example, the spec doesn’t mention dashes, which traditional typography denotes as punctuation worthy of hanging.

As recommended at The Recovering Physicist, traditional Western languages will likely only hang the following punctuation marks:

Code Point Character Name HTML Entity Unicode Category
U+00AB « LEFT-POINTING DOUBLE ANGLE QUOTATION MARK « Pi
U+00BB » RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK » Pf
U+2018 RIGHT SINGLE QUOTATION MARK ’ Pi
U+2018 LEFT SINGLE QUOTATION MARK ‘ Pf
U+201C LEFT DOUBLE QUOTATION MARK “ Pi
U+201D RIGHT DOUBLE QUOTATION MARK ” Pf
U+2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK ‹ Pi
U+203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK › Pf
U+2010 HYPHEN ‐ PD
U+2013 EN DASH – PD
U+2014 EM DASH — PD

For Western languages, punctuation marks like dashes, periods, commas, colons, semicolons, etc, are generally hung at the right margin as they don’t normally begin paragraphs or appear on the left margin.

Bringing Hanging Punctuation to the Web Now

Ideally you’d hang all qualifying punctuation in a paragraph of text, but this is the web and incomplete CSS support makes it hard to fully implement hanging punctuation in left- and right-aligned texts as well as justified texts.

Hanging punctuation in a single margin, for example the left margin for left-aligned text, can be easier to control. Block level elements like <blockquote>, <p>, <h1> through <h5> tags that begin with qualifying punctuation marks can use a negative text-indent value to hang the punctuation.

It’s worth noting that hanging punctuation using a negative text-indent will only allow you to hang the punctuation at the very beginning of a paragraph. Because text re-flows on the web depending on screen size, you never know where punctuation marks will fall. The beginning punctuation mark is the only one whose position you can be sure of.

In spite of these short comings, exploring the possibilities of hanging beginning punctuation can still be valuable. It’s a typographic choice you can make as the designer.

In this tutorial we will look at how to hang beginning punctuation in traditional left-aligned Western English texts. Remember that code is malleable. You can take the examples in this tutorial and tweak them to fit whatever need you have. Hopefully, in the future, browsers will better solve the issue of hanging punctuation marks.

Step 1: A Quick Overview

The idea behind implementing hanging punctuation for left-aligned text is rather simple. Use JavaScript to find all eligible DOM elements within the context of an article layout that begin with hangable punctuation. When found, apply an HTML class to the element that has a negative value for the text-indent rule.

Step 2: Examining The Markup

Let’s look at a basic example of markup used to layout a simple blog post. The text of the article would be laid out in headings, subheadings, lists, and quotes all as sibling elements, like this:

<article class="post">
    <h1>Article Title</h1>
    <p>Some text here ...</p>
    <p>"A paragraph" that begins with hangable punctuation ...</p>
    <h2>A Subheading for the Article</h2>
    <p>Text here ...</p>
    <blockquote>
        <p>Quoted text ...</p>
        <p>In multiple paragraphs ...</p>
    </blockquote>
    <p>More text ...</p>
    <h2>"A Subheading" Beginning With Punctuation</h2>
    <p>Some more text ...</p>
    <ul>
        <li>List text ...</li>
        <li>A second list item</li>
    </ul>
    <p>More text ...</p>
    <p>Last paragraph.</p>
</article>

Your own markup pattern may differ from this, in which case you can later tweak the javascript to find DOM elements according to how your markup is laid out. For this tutorial, we’ll use this simple markup pattern.

In order to hang beginning punctuation for elements within a block of text we must first delineate which punctuation marks can be hung, which we analyzed earlier in the tutorial. It’s important to remember that different languages use quotation (and punctuation) marks in widely varying manners. For example, take a look at how English and Welsh use primary and secondary quotation marks:

  • English: I told Johnny, “Mark Twain once said, ‘If you tell the truth, you don’t have to remember anything.’”
  • Welsh: I told Johnny, ‘Mark Twain once said, “If you tell the truth, you don’t have to remember anything.”’

Notice how they are complete opposites? Those differences are only the beginning. Take a look at this example of primary and secondary quotation mark usage in just a handful of languages (you can see a larger compilation at Wikipedia):

Language Primary Secondary
English “…” ‘…’
Welsh ‘…’ “…”
Croatian „…” ‚…’
German „…“ ‚…‘
Greek «…» “…”
French «…» ‹…›

It’s good to be mindful of these differences, so if you ever have to work with languages other than Western English you’ll have a firmer grasp on which punctuation marks you should be hanging.

Step 3: Setting Up the CSS

Now we have some basic markup for our text layout. We will use JavaScript to parse each of those DOM objects and find the ones that begin with hangable punctuation. For the ones that begin with qualifying punctuation, we will apply a HTML class with a negative value for text-indent rule which will hang the punctuation in the left margin.

The tricky part here comes in defining the value of your text-indent rule. Different fonts will require different values, so this will take some tweaking and customizing on your part. For example, a smart double quote (“) has a thicker width than a smart single quote (‘). Hence, if you're hanging punctuation, the DOM element that begins with a smart double quote will require a larger value for the text-indent rule than a DOM element beginning with a smart single quote.

A Note on Fonts

Not only will the text-indent value be different depending on the width of the punctuation mark you’re hanging, but it will also depend on the typeface you’re using. Of course, this is the web, and you can’t always ensure a user will be using a certain typeface. However, web safe fonts have a good ubiquity while non web safe fonts have some pretty bullet-proof methods of serving specific fonts to clients. Either way, you can come pretty close to ensuring everyone is seeing the same font.

Whatever font or font stack you end up using, it’s good to preview and tweak your text-indent values on a per-font basis (and preferably at various sizes). This ensures that your hanging punctuation marks will align correctly.

The Hanging Punctuation Classes

Assuming that all the elements of our post are using the same typeface, we can setup three classes defining three widths: small, medium, and large. Depending on which type of punctuation it begins with, our JavaScript will apply one of these three classes to the DOM element thus giving us hanging punctuation!

.indent-small {text-indent: -.2325em}
.indent-medium {text-indent: -.4125em} 
.indent-large {text-indent: -.6125em} 

Notice we are using em measurements. This ensures the text-indent will always be relative to the font-size of the text in the article. So if the font size is increased, you won’t have to change the text-indent values.

The Hanging Punctuation Classes: Two Typefaces

But what if you are using two different typefaces in your layout? In the example we are using, our body text elements (<p> <ul> <blockquote>) use one sans-serif typeface (Helvetica Neue, Arial fallback) while our headers (<h1> <h2> <h3>) use a different sans-serif typeface (Source Sans Pro). The punctuation marks of Source Sans Pro are, by design, wider than those of Helvetica Neue and Arial. Thus, DOM elements set in Source Sans Pro (headers) are going to require slightly larger text-indent values than DOM elements set in Helvetica Neue (lists, paragraphs, etc).

This is easy enough to handle by adding additional classes specifically for headers. We will just use JavaScript to detect the element type, then apply the appropriate class.

.indent-header-small {text-indent: -.325em}
.indent-header-medium {text-indent: -.5125em} 
.indent-header-large {text-indent: -.7125em} 

Step 4: Setting Up a Function in JavaScript

Now that we have the markup and CSS we need, we are going to use JavaScript to discover any DOM elements that are direct descendants of our article container. We then check to see if those DOM elements begin with punctuation marks that should be hung.

To begin, we will create a function that will contain all our code. This function will take one argument, which is the DOM element containing the text we want to parse for hanging punctuation, for example the container of a blog post.

/*
 * Hanging Punctuation
 * This function takes a DOM element,
 * searches each of its direct descendants,
 * and, if the element begins with hangable punctuation,
 * the appropriate HTML class is applied to the element.
 *
 * Then the parent DOM element get's a class to activate
 * the child classes we applied. 
 */
function hangPunctuation(container) {
    // code goes here
}

Note: hanging punctuation is an aspect of progressive enhancement. In this example, we are going to use native JavaScript that is not fully supported in legacy browsers (looking at you IE). If you wanted to get this to work in older browsers, you could do it. Most of the stuff we’ll be doing is selecting elements from the DOM and changing their classes. You could easily incorporate jQuery to do this, or you could use helper functions from youmightnotneedjquery.com

Step 5: Defining the Punctuation Characters

We need to create a list of hangable punctuation. Using the characters we defined earlier as a reference, we can create an object in JavaScript defining our punctuation marks (by their Unicode points):

// Punctuation marks that qualify to be hung
var marks = {
    '\u201c': 'medium',     // “ - ldquo - left smart double quote
    '\u2018': 'small',      // ‘ - lsquo - left smart single quote
    '\u0022': 'medium',     // " - ldquo - left dumb double quote
    '\u0027': 'small',      // ' - lsquo - left dumb single quote
    '\u00AB': 'large',      // « - laquo - left double angle quote
    '\u2039': 'medium',     // ‹ - lsaquo - left single angle quote
    '\u201E': 'medium',     // „ - bdquo - left smart double low quote
    '\u201A': 'small',      // ‚ - sbquo - left smart single low quote
};

As you can see, we define a list of punctuation marks we want to hang with a corresponding width definition that matches our HTML classes. This object tells us which punctuation marks to hang and how large their negative text-indent value should be. This list is not comprehensive. The beauty is that we will loop over the entire list, so you can add additional punctuation marks to this list as needed.

Step 6: Looping Over Each Child Element

Now we are going to loop over all direct descendants of our containing element (which was passed into the function). To do so, we will first setup a for loop:

// Loop over all direct descendants of the container
// If it's a blockquote, loop over its direct descendants
for(i=0; i<container.children.length; i++) {
    // code goes here
}

Next (inside the for loop) we will create a variable called el which represents each direct descendant element of the container we are looping over (get an element’s descendants using the .children property):

var el = container.children[i];

Now we will pass our el variable to another function (hangIfEligible()), which will check to see if it begins with a punctuation mark that can be hung. If it does, this other function will apply the appropriate HTML class to the element.

if (el.tagName === 'BLOCKQUOTE') {
    for (var k = 0; k < el.children.length; k++) {
        hangIfEligible(el.children[k]);
    };
}
else {
    hangIfEligible(el);
}

Notice what we did with the blockquote tag? Because blockquote elements may have descendant elements beginning with hanging punctuation (like the <p> tag), we need to loop over each of its children as well. So if the current element we’re looping over is a blockquote tag, we loop over each of its children and pass them to our hangIfEligible() function. If it’s not a blockquote element, we just pass the current element itself to the hangIfEligible() function.

If we didn’t specifically loop over the children of the blockquote element, we would miss its children and a chance to hang punctuation.

Our for loop should now looking something like this:

// Loop over all direct descendants of the container
// If it's a blockquote, loop over its direct descendants
for(i=0; i<container.children.length; i++) {

    var el = container.children[i];

    if (el.tagName === 'BLOCKQUOTE') {
        for (var k = 0; k < el.children.length; k++) {
            hangIfEligible(el.children[k]);
        };
    }
    else {
        hangIfEligible(el);
    }
}

Step 7: Hanging the Punctuation

We’ve passed the current child element to our hangIfEligible() function. So now let’s define what that function does:

// Check to see if the passed-in element 
// begins with one of the qualifying punctuation types
// If it does, apply the appropriate class depending on the tag type
function hangIfEligible(el) {
    // code goes here
}   

As you can see, we have a function named hangIfEligible() which takes one parameter: the DOM element we’re checking for beginning punctuation.

Getting the Element’s Text

Now let’s create the variables we’ll need. First we need to get the text inside the DOM element. To do so, we'll use the innerText JavaScript property. Because this doesn’t work in Firefox, we’ll also use the textContent property which does.

var text = el.innerText || el.textContent;

Setting Up the HTML Class Name

Because our example uses two different fonts (one for headings, one for copy) we’ll need two different classes for the two different element types.

If you remember, our HTML classes which control the negative text-indent follow the naming pattern indent- and then the width of the punctuation mark (small, medium, or large). If the element is a header, we add header- between them. So the class for small width punctuation marks is indent-small for normal copy and indent-header-small for headings.

In our JavaScript, we’ll create a variable for our HTML class and append header- later on if the current tag is a heading element.

var htmlClass = 'indent-';

Checking the Element for Beginning Punctuation

Now let’s check and see if the text of our current DOM element begins with one of the punctuation marks we defined earlier. To do so, we’ll loop over each of the entries in our marks variable where we defined the punctuation marks we want to hang. Then we’ll use JavaScript’s indexOf property to check and see if the first character the element’s text matches one of our punctuation marks.

for(var mark in marks) {
    if ( text.indexOf(mark) === 0 ){
        // If it matches, code here
    }
}

A refresh on indexOf: JavaScript’s indexOf looks at a string where each character is an element in an array. For example, var text = 'A String' would look like this ['A', ' ', 'S', 't', 'r', 'i', 'n', 'g']. The indexOf property takes a character and searches the entire array looking for that character. If it finds it, it returns the character’s position in the array. So, in our case, we search for a certain punctuation mark. If it’s found in the very first position of the array (index 0) then our text begins with hangable punctuation.

Adding a Class

Now all we have to do is add a class to the element if the first character of its text matches one of our punctuation marks.

In our example, if the element begins with a qualifying punctuation mark, we then need to check if the current element is a header (as our headers need modified HTML classes). So, in the part where we had // if it matches, code here we will put this:

if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3' || el.tagName === 'H4' || el.tagName === 'H5' )
    htmlClass += 'header-';
el.classList.add(htmlClass + marks[mark]);

See what we did? If the current element was a heading of some kind, we appended header- to the HTML class name. The HTML class that gets added to our DOM element is constructed like this:

  • indent- by default
  • header- appended if the current element is a header
  • small, medium, or large added depending on the current punctuation mark’s defined width

All the Code Together

All of that code together would look like this:

// Check to see if the passed-in element 
// begins with one of the qualifying punctuation types
// If it does, apply the appropriate class depending on the tag type
function hangIfEligible(el) {
    var text = el.innerText || el.textContent;
    var htmlClass = 'indent-';

    for(var mark in marks) {
        if ( text.indexOf(mark) === 0 ){
            if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3' || el.tagName === 'H4' || el.tagName === 'H5' )
                htmlClass += 'header-';
            el.classList.add(htmlClass + marks[mark]);
        }
    }
}

Step 8: Run the Function on Page Load

Now all you have to do is run the function when the page loads. You can do this with a few lines of code:

window.onload = function() {
    var container = document.querySelector('.post')
    hangPunctuation( container );
    container.classList.add('hang-punctuation');
}

See what we did here? Call the hangPunctuation function and pass in the containing DOM element of the body of text within which you want to hang punctuation.

Notice that we add a class to the container element. You can use this class to hang <ul> lists if you want. Simply add some CSS like so: .hang-punctuation ul {margin:0; padding:0: list-style-position: outside;}

The Final Code

That‘s it! We are all finished. Here’s the final code:

/*
 * Hanging Punctuation
 * This function takes a DOM element,
 * searches each of its direct descendants,
 * and, if the element begins with hangable punctuation,
 * the appropriate HTML class is applied to the element.
 *
 * Then the parent DOM element get's a class to activate
 * the child classes we applied. 
 */
function hangPunctuation(container) {

    // Punctuation marks that qualify to be hung
    var marks = {
        '\u201c': 'medium',     // “ - ldquo - left smart double quote
        '\u2018': 'small',      // ‘ - lsquo - left smart single quote
        '\u0022': 'medium',     // " - ldquo - left dumb double quote
        '\u0027': 'small',      // ' - lsquo - left dumb single quote
        '\u00AB': 'large',      // « - laquo - left double angle quote
        '\u2039': 'medium',     // ‹ - lsaquo - left single angle quote
        '\u201E': 'medium',     // „ - bdquo - left smart double low quote
        '\u201A': 'small',      // ‚ - sbquo - left smart single low quote
    };
        
    // Loop over all direct descendants of the container
    // If it's a blockquote, loop over its direct descendants
    for(i=0; i<container.children.length; i++) {

        var el = container.children[i];

        if (el.tagName === 'BLOCKQUOTE') {
            for (var k = 0; k < el.children.length; k++) {
                hangIfEligible(el.children[k]);
            };
        }
        else {
            hangIfEligible(el);
        }
    }

    // Check to see if the passed-in element 
    // begins with one of the qualifying punctuation types
    // If it does, apply the appropriate class depending on the tag type
    function hangIfEligible(el) {
        var text = el.innerText || el.textContent;
        var htmlClass = 'indent-';

        for(var mark in marks) {
            if ( text.indexOf(mark) === 0 ){
                if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3' || el.tagName === 'H4' || el.tagName === 'H5' )
                    htmlClass += 'header-';
                el.classList.add(htmlClass + marks[mark]);
            }
        }
    }
}

window.onload = function() {
    var container = document.querySelector('.post')
    hangPunctuation( container );
    container.classList.add('hang-punctuation');
}    

It’s good to remember that this solution is in no ways comprehensive. It will most likely require tweaking for each project it’s used on, because each project differs in typeface choices, markup structure, etc. However, if you are laying out longform text, this solution could help add a little bit of typographic refinement to your layout.

Supplementary Reading:

An Alternative Approach

Since publishing this article, others have chimed in with their ideas too. Take a look at the comments and you'll find Gunnar Bittersmann's inspiring take on the CSS we've discussed. Lane Olson even combined those thoughts with my JavaScript to make a demo on Codepen.

Advertisement