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 will implement a CSS router that allows importing of medium-specific stylesheet according to principles of Intrinsic Web Design.

Let us 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 with a genuinely non-rectangular shape is probably the circular Moto 360 Watch, but it, too, renders in a rectangular mode as usual.

The point here 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 ways:

  1. In portrait or in
  2. Landscape.

Thus, there are only two states of web design that 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–the landscape mode.

Let us 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, one can see that we have split our styles along the two axes or “states” of web design intrinsic to rectangular mediums.

This much is simple enough.

Now let us 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 serve only one stylesheet asynchronously into the browser according to the device’s orientation.

Or the shape of the browser window.

Meaning, if a user were to resize their browser on the desktop and the shape of the window (rectangle) were to switch from landscape to portrait, our CSS route would match to portrait.css and fetch it. More often than not, however, our router will serve landscape.css only on the desktop.

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. Meaning, if you are importing just one stylesheet into the DOM, using an @import or a link tag call 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 arrived at a suitable style resource that would fit the way our eyes are physically looking at the screen.

We can now drill down further into each axis to understand how our UI would scale to truly ‘belong to the device’ at hand. And thus 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 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.

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

Scaling the UI along portrait-axis

Let us 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 meet “mobile-first.” If one 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 from 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 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.


Looks okay so far?

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

Using category-specific breakpoints ensures the intrinsicality of physical size 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 is similar to how we breakdown CSS with responsive web design, but by focusing on the category of the device instead of its specifications.

Starting from an Apple Watch.

While not every web-app may require 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.

Not something that responsive web design could achieve because responsive cannot differentiate between a mobile and a watch. The option to contextualize web-design according to the environment of Watch 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 next. 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 a tablet view on either portrait or the landscape-axis instead.

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

We will 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 pretty granular way to serve exact style rules according to the accessibility and capability situation of each category independently.

Contextualize properties like touch-action, pointer-events, for example. The accessibility situation of mobile is different from the accessibility situation of a Tesla Model S car, where the driver or the pilot (user) 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:

It,

  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 one has had to make 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-wise capabilities (touch or pointer driven) of the devices/hardware according to industry-defined presets.

  5. Afford a better scope to address usability situations 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.

However, is there a disadvantage?

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 much CSS to maintain, but we are 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

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

From the plot of intrinsic design space above, one 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. The desktop dashboard similarly 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 the IWD, as in the separation of “design states,” but it fails to establish a boundary. It does not push the designer to think of 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 bursting at the seams already.

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 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 is 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 one can use to route-in CSS resources.

One can use the preferred color-scheme at the top, too, 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;
  }
}

However, with each such test of environment, we introduce a new complexity level on 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, but the overhead of managing hundreds of submodules 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, like so:

<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 is it.

Our CSS router is ready.

It can now replace the old and rather lifeless link tag in the head of the document and quickly turn it 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