Introducing Construct.css

Avatar of Nathan Smith
Nathan Smith on (Updated on )

It began, as many things do, with a silly conversation. In this case, I was talking with our Front End Technology Competency Director (aka “boss man”) Mundi Morgado.

It went something like this…

Mundi Morgado I want you to build a visual screen reader.

Nathan Smith Who what now?

Mundi Morgado I want a CSS library that allows you to see the structure of a document. It shouldn’t use class so that you’re forced to focus on semantics. Also, make it theme-able.

Nathan Smith Sure, let me see what I can come up with.

Fast-forward a week, and we’ve got what we are now calling:

Construct.css: The throwaway CSS library with no class

Why throwaway? Well, it’s not really meant to be a fully fledged, production-ready style framework. Rather, it’s like training wheels for document semantics, with some bumper lanes (think: bowling) to keep you on the right track.

It’s part of our ongoing effort at TandemSeven to promote code literacy throughout our organization. As more of our UX designers are beginning to share responsibility for the accessibility and semantics of a project, it makes sense that we would build in such a way that allows UX and development to be more collaborative.

There are four main aspects to Construct.

Construct: “Basic”

First is the base Construct.css file, which applies a passably basic look and feel to the majority of common HTML elements. Check out this example of a basic page.

Construct: “Boxes”

Second, there is construct.boxes.css. This visually distinguishes otherwise invisible semantic elements, such as: header, main, nav, etc. That way, one can see where those elements are (or aren’t) as a page is being authored. Here’s a page with semantic boxes outlined.

Construct: “Theme”

Thirdly, theming is possible via construct.theme.css. This is mostly just an example — inspired by PaperCSS) — of how to make UI look “hand drawn,” to prevent observers from being too fixated on the look and feel, rather than the semantics of a document. Here’s the example theme in action.

Construct: “Debug”

Lastly, there is a construct.debug.css file that highlights and calls out invalid and/or problematic markup. Here’s a gnarly example of “everything” going wrong on a single HTML page. This was inspired by a11y.css, though it diverges in what is considered noteworthy.

For instance, we call out “div-itis” when multiple div:only-child have been nested within one another. We’re also opinionated about type="checkbox" being contained inside of a <label>. This is for maximum click-ability within an otherwise dead whitespace gap, between a checkbox (or radio) and its textual label.

Any or all of these CSS files can be applied individually or in conjunction with one another. There is also a bookmarklet (labeled “GET Construct!”) on the Construct home page you can use to get results for any webpage.

The bookmarklet will remotely pull in:

  • sanitize.css
  • construct.css
  • construct.boxes.css
  • construct.debug.css

Our “reset” – Sanitize.css

A quick word on Santitize.css: It was created by my coworker Jonathan Neal (author of Normalize.css), as a way to have a semi-opinionated CSS “reset.” Meaning, it puts in a lot of defaults that we find ourselves writing as developers anyway, so it makes for a good starting point.

Technically speaking

Okay, so now that we’ve covered the “why,” let’s dig into the “how.”

Basically, it all revolves around using (abusing?) the ::before and ::after pseudo elements. For instance, to show the name of a block level tag, we’re using this CSS.

Semantic tags (using ::before)

Here is an abbreviated version of the construct.boxes.css file, for succinctness. It causes the <section> tag to have a dashed border, and to have its tag named displayed the top left corner.

section {
  border: 1px dashed #f0f;
  display: block;
  margin-bottom: 2rem; /* 20px. */
  padding: 2rem; /* 20px. */
  position: relative;
}

section > :last-child {
  margin-bottom: 0;
}

section::before {
  background: #fff;
  color: #f0f;
  content: "section";
  font-family: "Courier", monospace;
  font-size: 1rem; /* 10px. */
  letter-spacing: 0.1rem; /* 1px. */
  line-height: 1.3;
  text-transform: uppercase;
  position: absolute;
  top: 0;
  left: 0.3rem; /* 3px. */
}

Warning messages (using ::after)

Likewise, here’s a snippet of code from the construct.debug.css file that drives the markup warning messages. This particular example causes <script> tags to be displayed, which may be further optimized or need the developer’s attention: inline JavaScript and/or external src without async.

In the case of inline JS, we set the font-size and line-height to 0 because we’re making the element visible. We yank the text off the page via text-indent: -99999px just to make sure it doesn’t show up. We wouldn’t want that random JS code displayed alongside legit page content.

(Please don’t ever do this for “real” content. It’s just a hack, mkay?)

To get our error message to show up though, we have to set the font-size and line-height back to non-zero values, and remove our text-indent. Then, with a bit more absolute positioning, we ensure the message is visible. This lets us see, amidst the rest of the page content, whereabouts to check (via DOM inspector) for the <script> insertion point.

script:not([src]),
script[src]:not([async]),
script[src^="http:"] {
  color: transparent;
  display: block;
  font-size: 0;
  height: 2rem; /* 20px. */
  line-height: 0;
  margin-bottom: 2rem; /* 20px. */
  position: relative;
  text-indent: -99999px;
  width: 100%;
}

script:not([src])::after,
script[src]:not([async])::after,
script[src^="http:"]::after {
  background: #f00;
  color: #fff;
  display: inline-block;
  font-family: Verdana, sans-serif;
  font-size: 1.1rem; /* 11px. */
  font-weight: normal;
  left: 0;
  line-height: 1.5;
  padding-left: 0.5rem; /* 5px. */
  padding-right: 0.5rem; /* 5px. */
  pointer-events: none;
  position: absolute;
  text-decoration: none;
  text-indent: 0;
  top: 100%;
  white-space: nowrap;
  z-index: 1;
}

body script:not([src])::after,
body script[src]:not([async])::after,
body script[src^="http:"]::after {
  top: 0;
}

script:not([src])::after {
  content:
    'Move inline <script> to external *.js file'
  ;
}

script[src]:not([async])::after {
  content:
    'Consider [async] for <script> with [src="…"]'
  ;
}

script[src]:not([src=""]):not([async])::after {
  content:
    'Consider [async] for <script> with [src="' attr(src) '"]'
  ;
}

script[src^="http:"]::after {
  content:
    'Consider "https:" for <script> with [src="' attr(src) '"]'
  ;
}

Note: Some scripts do need to be loaded synchronously. You wouldn’t want your “app” code to load before your “framework” code. Heck, some inline JS is probably fine too, especially if you’re doing some super speed optimization for the critical rendering path. These warnings are “dumb” in the sense that they match only via CSS selectors. Take ‘em with a grain of salt, your mileage may vary, etc.

JS bookmarklet

The aforementioned JS bookmarklet finds all <link rel="stylesheet"> and <style> tags in the page, and causes them to be ineffective. This allows us to add in our own CSS to take over, so that the only styles applied are those provided directly via Construct.

The magic is accomplished by setting rel="stylesheet" to rel="". If an external stylesheet is not identified as such, even if it’s cached, the browser will not apply its styles to the page. Similarly, we destroy the contents of any inline <style> tags by setting the innerHTML to an empty string.

This leaves us with the tags themselves still intact, because we still want to validate that no <link> or <style> tags are present within <body>.

We also check that there aren’t too many <style> tags being used in the <head>. We allow for one, since it could be used for the critical rendering path. If there’s a build process to generate a page with consolidated inline styles, then it’s likely they’re being emitted through a single tag.

<a href="javascript:(
  function (d) {
    var f = Array.prototype.forEach;
    var linkTags = d.querySelectorAll('[rel=\'stylesheet\']');
    var styleTags = d.querySelectorAll('style');

    f.call(linkTags, function (x) {
      x.rel = '';
    });

    f.call(styleTags, function (x) {
      x.innerHTML = '';
    });

    var newLink = d.createElement('link');

    newLink.rel = 'stylesheet';

    newLink.href =
      'https://t7.github.io/construct.css/css/bookmarklet.css';

    d.head.appendChild(newLink);
  }
)(document);">
  GET Construct!
</a>

Go forth, get Construct!

Get Construct

That pretty much covers it. It’s a simple drop-in to help you visualize your document semantics and debug possibly problematic markup.

I have also found it to be super fun to use. A quick click of the bookmarklet on any given site usually yields lots of things that could be improved upon.

For instance, here’s what CNN’s site looks like with Construct applied…

P.S. Call security!

If you try this with Twitter’s site, you might wonder why it doesn’t work. We were initially perplexed by this, too. It has to do with the site’s Content Security Policy (CSP). This can be used to disallow CSS and/or JavaScript from being loaded externally, and can whitelist safe domains.

This can be set either at the server level, or by a <meta> tag in the <head>.

<meta http-equiv="Content-Security-Policy" content="…" />

Read more about CSP here.

I know what you’re thinking…

Can’t you just destroy that meta tag with JS?

In the case of Twitter, it’s emitted from the server. Even if it were in the HTML, destroying it has no effect on the actual browser behavior. It’s locked in.

Okay, so…

Can’t you insert your own security meta tag and override it?

You’d think so. But, thankfully, no. Once a CSP been accepted by the browser for that page, it cannot be overridden. That’s probably a good thing, even though it ruins our fun.

I suppose that’d be like wishing for more wishes. It’s against the genie rules!