« Back to home

A Tale of Two States

Posted on

Modern Responsive Design (illustrated with Ember and Flexi)


This is a cross post of blog post I wrote and published for IsleOfCode on March 11, 2016 here


Author's Preamble

I was very tempted to name this post "the state of CSS is Awesome", except that the state of CSS is not awesome. This post is very "tale of two cities in nature". When it comes to layouts and CSS, it is both the best of times and the worst of times. The state of CSS IS awesome, but it IS also broken. CSSNext, PostCSS, ember-component-css, and better CSS practices should give us a lot of optimism for the future. And Flexi, the project which was created to solve the problems this post addresses shows us that even in our current problem state there are elegant, maintainable, performance minded solutions.


Sometimes there seems to be a tendency to think about responsive design as merely percentages, relative-units, and @media breakpoints.

This is fluid design, one of several subsets of responsive design; the other subsets include designs that reveal, reflow, reorder, and restructure.

Restructuring is a new category of responsive design enabled by the proliferation of Javascript web applications and Single Page Applications (SPAs).

On the surface, Responsive design is hard.

  • Layout needs differ widely by device
  • @media works for big picture alterations, but breaks down for smaller scopes.
  • Templates and web pages become a slog of CSS classes, extra wrapper elements, and repetitive HTML
  • Mixing layout elements and CSS with design styles is difficult to maintain long term
  • Not mixing your layout elements and css leads to verbose and cluttered layout classes
  • It's hard to reason about layout CSS you can't see
  • CSS is hard
  • All of this forces all developers to intimately know your CSS and layout logic
  • All of this forces you to prioritize one screen size, making all alternative layouts secondary and more complicated to maintain.

But responsive design doesn't need to be hard.

This post will walk through examples of how each of these forms of design is used to create fully responsive layouts, and how flexi solves these concerns and enables these designs for Ember applications.

How do these forms of design fit together?

Bootstrap 3's mobile-first grid when used with .container-fluid is a prime example of fluid design in action. Columns are % based, and breakpoint aware, allowing you to resize content to fit the viewport size. Fluid design helps you create features that will respond to fill their box.

Bootstrap also has an example of reveal design with it's set of responsive utilities. With this approach, content is always present in DOM, but hidden or revealed based on the viewport size.

This works well for hiding or showing content that may not fit on all screen sizes, but breaks down when used for larger layout changes because of the additional weight of all the elements that must be kept in DOM.

Bootstrap's .col-md-push-* and .col-md-pull-* classes are classic examples of using reflow design. Reflow utilizes floats and positioning to re-arrange portions of your DOM (in this case columns) for different viewport sizes. Reflow includes the use of classes like .offset-xs-6 to adjust margins and spacing.

Reorder design is similar to reflow, but it accomplished by using the CSS order property to alter the position of items displayed using flexbox for layout. Reorder is "cleaner" than reflow, but both suffer from significant browser layout and paint time performance issues.

Restructuring

Restructuring is the use of Javascript to display different layouts depending on the type or width of device.

Imagine we had a device service with boolean properties that were true when the viewport width matched our mobile tablet or desktop breakpoints.

Using Ember, we could take advantage of this service in our templates to alter (restructure) our DOM based on the active breakpoint.

A simple case of a minor alteration might look like this:

<h1>{{if device.isMobile 'Mobile!' 'Desktop!'}}</h1>

While a more major alteration might look something like this:

{{#if device.isMobile}}

  <ul>
    {{#each list as |item|}}
      <li>{{item}}</li>
    {{/each}}
  </ul>

{{else}}

<div class="container-fluid">
    <div class="row">
      <div class="col-sm-4">
        <ul>
          {{#each list as |item|}}
            <li>{{item}}</li>
          {{/each}}
        </ul>
      </div>
      <div class="col-sm-8">
         ... individual item view ...
      </div>
    </div>
  </div>

{{/if}}

Even this simple case should reveal to you some of the problems with this approach.

  • template files are suddenly exploding in size
  • this sorta works ok for two layout variants, but what happens as more layout variants are added?
  • when moving between mobile and non-mobile layouts, all the content of one layout must be torn down and new content created, even though much of the content could theoretically be repurposed. For instance, couldn't the ul simply move locations?

This approach also should cause you to pause and ask a few soul searching questions.

  1. Isn't DOM generation slower than toggling between display: none; and display: initial;?
  2. Is this really any more maintainable than a bunch of CSS classes using @media queries to hide and show large blocks of DOM?
  3. Doesn't this introduce a high overhead on the App for creation and garbage collection at a critical time in the render cycle?

This feels a lot like putting a sticker on a band-aide and telling the wounded person that "everything is better now".

Maybe everything is wrong.

Right now, at this point in the post, it may feel as though the state of things is that we just can't win.

  • "fluid" only understands viewport boundaries, but often needs to know about the boundaries of some smaller container
  • "reveal" slows down renders by weighing down the DOM tree
  • "reflow" and "reorder" cause expensive layouts and paints.
  • "reveal", "reflow", and "reorder" can be hard to see and reason about.
  • "restructuring" is just as messy, and likely less performant.
  • layout CSS you can't see makes maintenance hard and adds a taxing mental overhead to every project developer.
  • layout CSS you can see (via layout classes) is overly verbose, and often difficult to wade through.

So let's ask ourselves this: what can we do better?

Here are a few good truths we can pull out from the mess above.

  • restructuring pays an expensive layout cost once, but "reorder" and "reflow" pay it every time.
  • we need @container breakpoints
  • we need clean separation of layouts (no messy and bulky if ... else if ... else setups)
  • we need to keep the DOM tree svelte
  • we need to keep our layout syntax explicit, intuitive, and accessible
  • we shouldn't mix our design (feature) CSS and our layout (structure) CSS.

There are other lessons we ought to learn too, such as "component-ize" your CSS (i.e. have CSS that targets and scopes to your Components), but just taking this list, of truths, let's solve our problem.

Doing Better

What if we stop designing to our features?

...and start featuring our designs?

Let me unpack that.

Consider the following image:

layout demo

This image could represent a lot of common layouts. In the blue, we have a header, that's probably a component, and could be our "logo and navigation" feature.

Features - logo and navigation

The orange is likely a list of content we can select from.

Features

  • logo and navigation
  • content list

The yellow box is probably the detail view of the currently selected item in the content list, while the white box might be additional meta information about something in the yellow box.

Features

  • logo and navigation
  • content list
  • detail view
  • detail meta

To separate our features from our layout, we implement each of these four features as components, and design them to always "fill" their container. Here is where we apply "fluid design" and, at the micro level, "reveal design".

Typically this just means coding something with

width: 100%;
height: 100%;

And (sometimes).

overflow-y: scroll;

Featuring our designs refers to this process of designing features that fill their box and are layout agnostic. Features shouldn't need to know in advance whether their container is 500px or 300px wide, they should just fill it, and respond to the width they were given. @container breakpoints would make this extra easy to accomplish, but this is possible if constrained to viewport breakpoints too.

Once we have components that will respond to their boxes, we need a way to quickly lay them out with the position and proportions we want them to have.

Designing this way is the most important piece of the puzzle. When this piece falls into place, it turns out that all the other questions and problems can be dealt with elegantly. Here's how:

Explicit Layouts

The trouble with inline styles is that you get a LOT of needless repetition, the browser is unable to optimize and reuse the styles, and you have to wade through the style definition to find the meaty layout stuff you were probably just skimming for.

The trouble with classes is that they are verbose, not always descriptive, and could be mixed with classes for theming or behavior that have nothing to do with layout.

The trouble with just doing it all in a stylesheet for your feature is that your layout is entirely hidden, and therefore easy for a developer or maintainer to break and hard for a developer or maintainer to develop with.

However, there are two things we could target easily to make layout logic easy to spot and use: Elements, and Attributes.

For instance, what would be a good way to delineate a portion of the layout that should fill the viewport?

How about a <page></page>?

What if we wanted to put two items side by side, horizontally?

How about putting them in a box that will always align items horizontally?

<hbox></hbox>

or even

<box horizontal></box>

Let's take this a step further. How would we build our layout from the image earlier in this post?

Remember, we had four features:

  • logo and navigation
  • content list
  • detail view
  • detail meta

Let's take it a step further and use a sticky footer too.

<screen>
  <page>
    <hbox fit>
       ... logo and navigation ...
    </hbox>
    <hbox>
      <vbox md="3">
        ... content list ...
      </vbox>
      <box md="7">
        ... detail view ...
      </box>
      <box md="2">
        ... detail meta ...
      </box>
    </hbox>
    <hbox fit>
       ... sticky footer ...
    </hbox>
  <page>
</screen>

With just a few elements, nearly all imaginable layouts become trivial to achieve.

We didn't just use expressive elements here though, we also used a few attributes. If you guessed that md="n" were breakpoint-column number definitions, you were correct. These elements are flex based, and fit is an attribute modifier that tells the box that it should fit to it's content, and not grow to match the size of it's siblings.

Other layout attributes might include things such as vertical horizontal wrap nowrap justify and align.

The real advantage comes with the use of breakpoint attributes like xs and md.

<box xs="12" sm="6" md="4" lg="3"></box>

We could even use the breakpoint attributes to change layout direction and behavior.

<box
  xs="12 vertical nowrap"
  sm="6 horizontal wrap"
  md="4"
  lg="3"></box>

This makes our layout markup much easier to see, maintain, and reason about. But...

How could this possibly work?

Aren't these invalid elements? Aren't attribute selectors slow?

Yes, to the first question, but they WILL work correctly in all browsers as far back as anyone can remember. This takes advantage of the HTMLUnknownElement interface, but if strict html validation really matters to you, on more modern browsers you can register these tags so they won't be unknown.

Yes, to the second question too: attribute selectors are slow. Which is why in Flexi's case, an AST Walker transforms this syntax in your templates into class names at build time (before you ever ship your templates), giving the benefit of a declarative syntax but the performance of classes and without affecting your runtime.

This is great, but aren't our template files still huge? Aren't our mobile vs. desktop layouts still competing for focus?

What can we do about layouts?

For starters, ditch reflow and reorder design patterns. These are hacks, they are difficult and probably impossible to optimize well. They are equally hard to reason about for humans and machines alike.

This leaves us reveal and restructure as our two alternatives. Up to now, this has seemed like a difficult choice.

Reveal comes with a heavy DOM tree, heavy DOM trees, even when items are set to display: none; bloat the memory in use by an app and slow down the browser's render speed.

On the other hand, restructure renders only the desired content, nothing more, giving it a faster render speed and a lower memory profile. But, restructure also comes with a sudden intensive memory and CPU spike when a breakpoint triggers because it needs to quickly generate a huge amount of DOM and the Javascript Components backing it, and needs to tear down an equally significant amount of DOM and associated Javascript components.

So the tradeoff appears to be between optimizing for the case in which breakpoints trigger often (to be honest, usually this only happens with orientation changes on phones and tablets), and the case in which breakpoints trigger rarely.

Reveal handles rapid firing breakpoints better, but at a significant cost to all renders, even minor updates to content within your features.

Restructure chokes on rapid firing breakpoints, but keeps all your other renders smooth and clean.

Can we get the best of both worlds? Yes.

By utilizing components, and separating your features from your layouts, you also encapsulated and delineated the "views" and "view controllers" that matter from the structural layout portions that don't.

This is the same example from earlier with the ul that appears in two locations, except this time it uses layout elements and encapsulates the list as the content-list component.

{{#if device.isMobile}}

  {{content-list}}

{{else}}

<hbox>
  <box sm="4">
    {{content-list}}
  </box>
  <box sm="8">
     {{item-detail}}
  </box>
</hbox>

{{/if}}

What we want is to simply pick up the component from the first location and stick it in it's new location when we toggle between these layouts. That way, while we still have to generate a very small amount of new DOM, the hefty bulk of our Component instances and DOM is recycled from one layout organization to the next.

In Flexi, you can do this using a sustain.

{{sustain 'content-list'}}

Sustain is a lot like a dynamic component helper, except instead of rendering the component, it acts as a template marker for the component by that name. For a given component name it only generates a single instance, then moves that instance from location to location as the marker appears in new spots.

Now that your features are encapsulated, it's extremely easy to drop markers for where those features should appear, and just move the feature around as needed. Using sustain, features can even move across route (url) boundaries, the benefit of which I'll demonstrate in a moment.

Sustain has the added benefit that a layout change no longer spikes your CPU or memory usage, you'll notice the memory allocated for DOM stays much lower that it otherwise would.

This solves our layout re-organization problems, but we're still left with a really bulky template file that contains all of our layouts in one place.

Clean Separation of Layout Concerns

Why not...

... just break them into multiple files and insert the conditionals automatically?

Boom, done. Clean separation of concerns.

That leaves just one layout concern unaddressed: the messy mechanics of basing so much logic on the size of the viewport instead of the size of the bounding box.

@container breakpoints

In order to provide booleans for breakpoints such as device.isMobile, Flexi adds a service which measures the windows then listens for window resize events and exposes the current width and height.

This means we're able to write components that subscribe to the current width of the window, and recalculate their own width when the window's width changes. We don't need a separate listener for each component, we just need to subscribe to the width parameter provided by the device service.

The Component's width is then compared to the list of breakpoints, and a class corresponding to the active breakpoint is set on the component's element.

Flexi provides two element primitives that are actually Components with this behavior: <container></container> and <grid responsive></grid>.

Much like you would write @media CSS, you encapsulate styles within .container-<breakpoint-prefix> classes to activate or deactivate them when necessary.

In Flexi, this makes all responsive classes (columns and breakpoint attribute modifiers) capable of working against a parent container instead of the viewport.

Responsive Routing

Responsive design goes beyond just the layouts. Take the following route structure:

/docs
/docs/overview
/docs/<section-name>

This is in fact the url structure of the documentation for Flexi, which, anti-climactic surprise, is built with Flexi.

On mobile, visiting the docs/ url ought to render a list of documentation links, but on tablet or desktop we want to redirect the user to /docs/overview and render those same links as a left hand navigation.

On mobile, visiting /docs/<section-name> ought to just render the given section, but on tablet or desktop we also want the list of documentation links on the left side.

This is (literally) the code that Flexi required to do this.

For the docs screen, the top banner and list are feature components.

/docs/-layouts/tablet.hbs

<screen>
  <page>
    <box fit class="site-banner">
      {{sustain 'docs/components/nav-banner'}}
    </box>
    <hbox>
      <box sm="3">
        {{sustain 'docs/components/nav-menu'}}
      </box>
      <box sm="9">
        {{liquid-outlet "main"}}
      </box>
    </hbox>
  </page>
</screen>

In this tablet layout, our nav menu is on the left side, and any child route (index|overview|<section-name>) will render into {{liquid-outlet "main"}}.

/docs/-layouts/mobile.hbs

<screen>
  <page class="site-banner">
    <box fit>
      {{sustain 'docs/components/nav-banner'}}
    </box>
    <box xs="12">
      {{liquid-outlet "main"}}
    </box>
  </page>
</screen>

On mobile, we drop the nav menu, leaving only the top nav bar and the {{liquid-outlet "main"}}. This has the result that the most important content (the child route associated with the URL) will always be the primary focus on mobile or on tablet, but we also get to enhance the experience for the tablet size.

The default behavior of docs/ is to render the docs/index route into {{liquid-outlet "main"}}.

On mobile, our template will just be the feature.

{{sustain 'docs/components/nav-menu'}}

While on tablets, we fill out the content detail area with a nice welcome message.

<centered class="bg-light-green">
  <h1>Welcome to Flexi</h1>
</centered>

We then do users the favor of automatically redirecting them from the index to the overview if they've come to this url while on a tablet.

import Ember from 'ember';

export default Ember.Route.extend({
  device: Ember.inject.service('device/layout'),

  redirect() {
    if (!this.get('device.isMobile')) {
      this.transitionTo('docs.overview');
    }
  }
});

An important note here, if this feature require a model (say a list of items retrieved via a network request), we could share the model for this feature between the docs/ route and the docs/index/ route. All we'd need to do is fetch the data in the docs/ route, then change this last file to the following:

import Ember from 'ember';

export default Ember.Route.extend({
  device: Ember.inject.service('device/layout'),

  model() {
    return this.modelFor('docs');
  },

  redirect() {
    if (!this.get('device.isMobile')) {
      this.transitionTo('docs.overview');
    }
  }
});

Yep, that's all it took to share the model between the parent and the child route to avoid duplicating any work.

As the screen resizes, the primary focus remains the state indicated by the URL, while feature code is seamlessly swapped from one location to the next.

You can view the entire source code for the docs here.

Conclusion

Flexi is just beginning. Flexi is already fast, really fast, but the ability to recycle even more content and to do so with ever increasing speed and decreased overhead is in the works.

It's also likely that some of the concepts become better with Glimmer2 landing soon in Ember, possibly even implemented directly by Ember itself.

Approaching layouts with clear separation of concerns, an explicit declarative syntax, and intelligent component reuse enables use to write elegant, maintainable and performant responsive layouts today. It doesn't solve how dirty and entangled our CSS can get, but it's a giant leap in the direction of solving that too.