Writing a Router in CSS.

Writing a Router in CSS.

  • toucaan
  • 9 minutes
  • February 2, 2021

Welcome to the newest chapter on the Toucaan Intrinsic CSS framework.

In this chapter, we’ll implement a CSS router that allows you to import a medium-specific stylesheet according to the principles of Intrinsic Web Design.

Let’s get started.

Meet the Two States of Web Design

The first seemingly inconsequential but notable fact about all digital mediums is that everything is rectangular. Even the notched phones and bendable screens are rectangular under the glass. The only device I can find with a genuinely non-rectangular shape is the circular Moto 360 Watch, but it, too, renders in a rectangular mode like every other screen.

The point is that the final “design state” of an app’s view on the glass of glowing pixels is always a rectangle. And this rectangle can be viewed in only two modes: portrait or landscape.

Thus, only two states of web design exist.

The rectangular viewport (the above-the-fold region) can display content in either a standing position, the portrait orientation, or in the lying position (landscape mode).

Let’s kickstart our CSS router using this fact first:

<style>
  /* Inside the head tag of your HTML, paste the following rules: */
  
  /* The x-axis of Intrinsic Web Design */
  @media only screen and (orientation: portrait) {
    :root {
      --vmin: calc(100vw/100);
    }
  }

  /* The y-axis of Intrinsic Web Design */
  @media only screen and (orientation: landscape) {
    :root {
      --vmin: calc(100vh/100); 
    }
  }
  :root {
      --vmin: 1vmin;
      /*** Global custom properties 
      like the color palette here…  ***/
  }  
</style>

From the code above, you can see that we’ve split our styles along the two axes, or “states,” of web design intrinsic to rectangular mediums.

This much is simple enough.

Now let’s use an asynchronous css @import call to turn the orthogonal division of styles above into the first level of our router, like so:

<style>
    /* Inside the head tag of the document, paste the following rules: */
    
    /* The x-axis of Intrinsic Web Design */
    @import url('…/path/to/toucaan/router/landscape.css') only screen and (orientation: landscape);

    /* The y-axis of Intrinsic Web Design */
    @import url('…/path/to/toucaan/router/portrait.css') only screen and (orientation: portrait); 

    :root {
        /* Global custom properties like the color palette here…  */
    }
</style>

For any medium, our router will asynchronously serve only one stylesheet into the browser according to the device’s orientation or the shape of the browser window.

Meaning, if a user resizes their browser on the desktop and the shape of the window (rectangle) switches from landscape to portrait, our CSS route will match to portrait.css and fetch it. More often than not, however, our router will serve landscape.css on the desktop because the way we generally use the desktop browser.

Perfect.

We can now scale our UI along one axis and not worry about how the website would appear on the other.

The Curious Case of CSS @import.

There are plenty of articles on the web that claim that a CSS @import performs poorly. This isn’t remotely true!

The performance of CSS @import depends on how it is used. If an @import fetch uses an inline declaration from the head of a document without sequential chaining of multiple stylesheets that could lead to a waterfall, it would be just as fast as any link statement. In other words, if you’re importing just one stylesheet into the DOM, using an @import or a link statement doesn’t make a difference.

The Axes of Intrinsic Web Design

What our router has done so far is execute a top-level environment test and arrive at a suitable style resource that fits the way our eyes are physically looking at the screen.

We can now drill down further into each axis to understand how our UI scales to truly ‘belong to the device’ at hand to provide a more fulfilling experience to the user.

Consider the following design space:


Axes of Intrinsic Web Design. The axes of Intrinsic Web Design.


Splitting our design thinking along the two axes is akin to saying that our UI mockups have to meet the UI specifications of just one axis at a time without considering how the app would appear on the other.

This is almost what we do with responsive design.

With intrinsic design, however, the separation is explicit. It is almost absolute, to the point where even the HTML served from the app could be different depending on the matched CSS route.

Scaling the UI along portrait-axis

Let’s start with the y-axis of intrinsic web design.

Of course, the y-axis or the portrait axis of intrinsic web design is where we expect to start with a “mobile-first” UI. If we were to scale the UI along the portrait-axis according to the increasing physical size of devices, our stops or “breakpoints” would go something like this:

  • (A) Start with an Apple Watch (always viewed in portrait mode) to
  • (B) A smartphone held in portrait grip to
  • (C) A tablet held in portrait mode to
  • (D) A desktop mounted in portrait mode (developers or gamers) to
  • (E) A TV-set mounted on a wall in portrait mode (retail display or airport flight information display) to
  • (F) A 120” wall-mounted projector screen in portrait mode.


Device viewports along portrait axis. Increasing physical size of devices held or mounted in portrait orientation.


Does it look okay so far?

From the list above, you can make out that we’re heading towards “category-specific” breakpoints on our router.

Using category-specific breakpoints ensures the intrinsicality of physical size is included in our web designs. Moving up the medium’s physical size, with wearable → phone → tablet →… → projector in portrait orientation only.

The instance of a V9 browser displayed on a desktop-sized tablet fixed in portrait mode at the center console of a Tesla Model S is not shown above. It would fall somewhere in the middle of a tablet and a desktop.

Splitting routes this way helps our router deliver just the right amount of CSS on a given medium without falling into the trap of device-specific breakpoints. It’s similar to how we breakdown CSS with responsive web design, but by focusing on the category of the device instead of manufacturer’s specifications.

Starting from an Apple Watch.

Not every web-app has to meet the challenging design needs of an Apple Watch, but with Intrinsic Web Design, a designer can consider their app’s wireframe for the Apple Watch first.

The special thing about an Apple Watch is that it has a very tiny screen—a sub-inch rectangular viewport if the bezel area around the Watch is discounted.

Four UI quadrants on an Apple Watch. Finger-tips can cover a quarter of the screen on a Watch.


Designing for a medium so small can be particularly challenging. A user’s finger-tip covers almost 25% of the real-estate available on the screen, so the UI components need to be scaled up for accessibility by a lot.

Recommended reading: Designing Web Apps for Apple Watch.

This isn’t something that responsive web design can achieve because responsive cannot differentiate between a mobile and a watch. The option to contextualize web-design according to the environment exists with only intrinsic web design.

Since a Watch is viewed in portrait mode only, we add a route for watch.css on the “portrait-arm” of the router like so:

/* The portrait-axis router. */
/* The file at --> ./toucaan/router/portrait.css */

/*** Route-in or @import wearables first. ***/
/*** 1. Apple Watch 6 for men = 44mm. Resolution: 368 x 448 pixels ***/
/*** 2. Apple Watch 6 for women = 40mm.  324 x 394 pixels   ***/
/*** 3. Moto 360 Watch = 46mm. Resolution: 360 x 330 pixels ***/
@import url('./toucaan/app/portrait/watch.css') 
    only screen 
    and (max-device-width: 368px) 
    and (max-device-height: 448px) 
    and (-webkit-device-pixel-ratio: 3);

All our “watch-specific” styles will live inside watch.css and be served on the page when the router matches a wearable.

Moving up the portrait-axis of intrinsic web design, we add a route for category mobile. The following MQ range covers all the smartphones that are available on the market today (2021).

/* The portrait-axis router. */
/* The file at → ./toucaan/router/portrait.css */

1. /*** Route-in or @import watch.css ***/



/*** Route-in or @import mobile.css next. ***/
/*** 1. iPhone 4 to iPhone Pro Max 12. Resolution: ***/
/*** 2. Android phones                             ***/
@import url('toucaan/portrait/mobile.css') 
    only screen 
    and (
        ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)),
        ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)),
        ((min-device-width: 375px) and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 3)), 
        ((min-device-width: 428px) and (min-device-height: 926px) and (-webkit-device-pixel-ratio: 3)) 
    );

/*** Route-in or @import tablet.css below, and so on… ***/


Some of the newer smartphones can unfold to become a mini-tablet. If upon unfolding, such a device falls outside the mobile route’s scope, our router will match it to the appropriate tablet view on either the portrait or the landscape axis.

Intrinsic Web Design aims to cover nearly all permutations and combinations of digital screens available on the web today.

We’ll come back to this discussion in another chapter, but our router can handle foldable phones and deliver just the right amount of css, tablet.css or mobile.css, depending on which UI fits best; UI that belongs.

Continuing up according to the physical size of devices connected to the Internet with a modern web browser, we hash out the “portrait-arm” of our router like so:

/**** Portrait router.css ****/

/* Wearables */
@import url('toucaan/app/watch.css') only (max-device-width: 368px) and (-webkit-device-pixel-ratio: 3); 

/* Mobiles */
@import url('toucaan/app/mobile.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Tablets */
@import url('toucaan/app/tablet.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Tesla EVs */
@import url('toucaan/app/car.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Desktops */
@import url('toucaan/app/desktop.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Televisions & Projectors */
@import url('toucaan/app/television.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

An API glimpse to determine mobile (pointerless) from desktop (pointer-driven) scenario.

@media (pointer: fine) {
  /* The media query that limits the enclosed CSS rules to be used only when the primary pointing device allows accurate pointing. */
}
@media (pointer: coarse) {
  /* The media query that limits the enclosed CSS rules to be used only when the primary pointing device does not allow accurate pointing. */
}
@media (pointer: none) {
  /* The media query that limits the enclosed CSS rules to be used only when the primary interacting device is not capable of pointing (i.e., keyboard). */
}
@media (hover) {
  /*The media query that limits the enclosed CSS rules to be used only when the primary pointing device allows hovering over elements. */
}
@media (any-pointer: fine) {
  /* The media query that limits the enclosed CSS rules to be used only when any of the pointing devices available allows accurate pointing. */
}
@media (any-pointer: coarse) {
  /* The media query that limits the enclosed CSS rules to be used only when any of the pointing devices does not allow accurate pointing. */
}
@media (any-hover) {
  /* The media query that limits the enclosed CSS rules to be used only when any of the pointing devices allows hovering over elements.*/
}

The router rules above are matched to industry-wide categories instead of device-specific breakpoints and is a granular way to serve exact style rules according to the accessibility and capability of each category independently.

Contextualize properties like touch-action, pointer-events, for example. The accessibility of mobile is different from the accessibility of a Tesla Model S car, where the driver or the pilot may be constrained by a safety belt and focusing on the road.

Advantages, but a tad bit of disadvantage?

Routing CSS modules along the axes of IWD has several advantages:

  1. Allows designers to work on logically closer modules together. If the intended UX/UI on the tablet is closer to mobile, all the designer has to do is copy mobile.css into tablet.css and start there. It helps avoid the stretch required with responsive web design where the jump is straight from a mobile context to a desktop that sits on a completely different axis.

  2. Helps with the macro-optimization of delivering only relevant CSS on any device. Users on a mobile, for example, do not need to see six thousand lines of desktop css that fit the landscape mode.

  3. Separates the CSS modules according to industry-wide categories and not according to device specifications. If Apple were to release a new iPhone of a different resolution or pixel density tomorrow, our router would be able to serve the new phone with the same mobile.css because it falls under the mobile category.

  4. The design thinking accounts for the physical size, accessibility constraints, and screen-specific capabilities (touch or pointer driven) of devices/hardware according to industry-defined presets.

  5. Afford a better scope to address the usability of each device. For example, introduce a proper app-like interface on mobile if necessary, such as one without a website footer or the coarse navigation of links at the bottom.

As you can see there are a number of advantages, but are there any disadvantages?

Routing CSS this way appears to split a single app.css into several modules. Up to five or six CSS modules along the portrait-axis and at least four along the landscape-axis.

It appears like a lot of CSS to maintain, but you’re probably doing that already on real production scale apps with many workarounds to tide over the limitations of RWD. The only difference between a router-based delivery is that the routes replace the hardcoded MQ breakpoints.

Intrinsic vs. responsive

You can consider responsive web design a subset of intrinsic web design, but intrinsic can also differ greatly from responsive depending on how deep an implementation goes into building UIs that “belong.”

From the plot of intrinsic design space above, you can see that the “mobile-first” part of responsive web design is just one piece of design thinking along the y-axis of intrinsic web design. Similarly, the desktop dashboard is design thinking along the x-axis of intrinsic web design, and the intersection of the two is where RWD meets its MQ breakpoint.

All evidence suggests that RWD tries to follow the same pattern as IWD, as in the separation of “design states,” but RWD fails to establish a boundary. It does not push the designer to think of the UI independent of how it would appear on the other axis. This is a significant drawback of responsive web design, which is why it is already bursting at the seams.

Completing the router

As was done with our router’s portrait-arm, we scale our landscape UI along the x-axis of Intrinsic Web Design according to increasing physical size. Since a wearable like the Apple Watch does not support landscape orientation (yet), we start on our router like so:

  • (A) Start with a smartphone held in landscape orientation to
  • (B) A tablet held in landscape mode to
  • (C) A desktop in landscape (natural) setting to
  • (D) A TV-set mounted on a wall in landscape
  • (E) A 120” wall-mounted projector screen in landscape.
Device viewports along landscape axis. Increasing physical size of the viewport in landscape mode.


Ultra-widescreen monitors can also be thrown into the mix and targeted with an eccentric layout that fits the medium intrinsically.

The instance of a V9 browser displayed on a desktop-sized tablet dashboard fixed in landscape mode at the center console of a Tesla Model 3 is not shown above.

After adding all the routes on the “landscape-arm,” our CSS router and the overall application CSS structure looks something like this:

<style>    
   /* Orientation Switch Media Query or _switch.scss */
    @import url('…/path/to/toucaan/router/landscape.css') only screen and (orientation: landscape);
    @import url('…/path/to/toucaan/router/portrait.css') only screen and (orientation: portrait); 

    :root {
        /* Global custom properties like the color palette here…  */
    }
</style>
Intrinsic monorepo style CSS architecture with IWD router. Structuring CSS with an IWD router.


The portait-axis:

/**** Portrait router.css ****/

/* Wearables */
@import url('toucaan/app/watch/watch.css') only (max-device-width: 368px) and (-webkit-device-pixel-ratio: 3); 

/* Mobiles */
@import url('toucaan/app/mobile.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Tablets */
@import url('toucaan/app/tablet.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Tesla EVs */
@import url('toucaan/app/car.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Desktops */
@import url('toucaan/app/desktop.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Televisions & Projectors */
@import url('toucaan/app/television.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

The landscape-axis:

/*** Landscape router.css ***/

/* Mobiles */
@import url('toucaan/app/mobile.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Tablets */
@import url('toucaan/app/tablet.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Desktops */
@import url('toucaan/app/desktop.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Televisions & projectors */
@import url('toucaan/app/television.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

/* Ultrawidescreen monitors */
@import url('toucaan/app/ultrawide.css') only ((min-device-width: 320px) and (max-device-width: 414px) and (-webkit-min-device-pixel-ratio: 2)) or ((min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3)); 

Note that a route (for example, mobile) on the portrait-axis and the landscape-axis can link to the same mobile.css file. It’s left up to the application author to determine how they want to further breakdown their CSS modules according to the design states of their app.

Intertwining the Router with Color Preference

The orientation of the viewport is not the only top-level “environment determination” that you can use to route-in CSS resources.

You can also use the preferred color-scheme at the top, like so:

/*  CSS is a lot like environment variables that pertain to the in-browser environment. */

@import url('…/path/to/toucaan/router/dark/landscape.css') and (prefers-color-scheme: dark);

@import url('…/path/to/toucaan/router/light/landscape.css') and (prefers-color-scheme: light);

@media screen and (prefers-color-scheme: dark) {
  :root {
      --background: #343434;
      --text: #ffffec;
  }
}

@media screen and (prefers-color-scheme: light) {
  :root {
      --background: #fff;
      --text: #546645;
  }
}

With each such test of environment, we introduce a new complexity level to our router. It will require nesting our @import switch statements further and breakdown the CSS submodules into dark or light sub-sub-modules. While doing so may not pose a disadvantage of an additional request from the page, the overhead of managing hundreds of these sub-modules can become a pain on its own.

To avoid two-level nesting on our router, we intertwine the color preference query on the inline style switch at the head level of the document:

<style>
    /* Inside the head tag of the document, paste the following rules: */
    
    /* x-axis of Intrinsic Web Design */
    @import url('…/path/to/toucaan/router/landscape.css') only screen and (orientation: landscape);

    /* y-axis of Intrinsic Web Design */
    @import url('…/path/to/toucaan/router/portrait.css') only screen and (orientation: portrait); 

    /* Global custom properties like the color palette here…  */
    @media screen and (prefers-color-scheme: dark) {
      :root {
          --background: #343434;
          --text: #ffffec;
      }
    }

    @media screen and (prefers-color-scheme: light) {
      :root {
          --background: #fff;
          --text: #546645;
      }
    }
    :root {
      /* Globally common custom variables here. */      
    }
</style>

Move all the coloring or theme-specific variables into the <head> global namespace and tag them separately into the light and dark scopes as shown above.

That’s it.

Our CSS router is ready.

It can now replace the old and rather lifeless link tag in the head of the document to quickly transform into an intrinsically-enabled app.

A tentative implementation of the above router is available on the Toucaan repository.

What do you think? Share your thoughts in the comments below.


By:

Marvin Danig, Founder of Bubblin and the Red Goose with editing help from AJ Alkasmi and Sonica Arora. Follow me on Twitter or on Github if you like.

Artwork credit: Watch Doodlers by JedBridges.

About the author

Marvin Danig

I write code with my bare hands. 💪🏻 Yammer about Bubblin all day.

https://bubblin.io/marvin