Back to the front page

Styles across frameworks

Splitting our monolithic app into micro-services created a major challenge for the front-end and design teams: keeping styles and user experience unified across different platforms and frameworks. Some of our client-side products are built with Ember.js, some use Rails, some Rails and React and others just pure CSS. They all serve the same purpose, so they must have the same look and feel.

The first step we took to simplify the process was to create Paint, our own internal styles library. Each application uses Paint with a default set of settings that allow customisation of colours or component behaviour.

Since having just a styles library was not enough, we published Canvas (the all-you-can-eat style guide) to let everyone know how to integrate the front-end components into any project.

Why another CSS framework

One thing that we’ve learned from working with a 5+ years old application is that removing a dependency is hard and, within large teams and numerous ongoing projects, it could become a big feature blocker.

There are two apocalyptic scenarios that could occur while building and maintaining a large and growing project. These practices should be avoided if you’re building to scale from the very beginning:

Markup. Now what?

So you’ve prototyped a new app using Bootstrap. Everything works fine, implementation of new features goes really fast, maintaining is a breeze. Then after reaching 100 partials and views you look at the code and realise it’s all .panel.panel-primary.col-xs-12.col-md-6.col-lg-3. Imagine having 5 separate client-side applications that followed the same format, just because it was easier to prototype them in the same way.

The moral: Building and building some more when working with a specific framework might not scale. Try to step back and look at the bigger picture.

Include. Then extend some more

You knew taking a semantic approach with the selectors would pay off one day. So you’re including compass, then start building. Down the line you realise that compass is not fit, but you have already too many dependencies in your code. You’ve now refactored 80 Sass files to use Neat mixins instead. Then you discover Susy grid and want it in. Add the 80 Sass files to the backlog again…

The moral: Keep dependencies as isolated as possible.

To solve the second challenge we’ve created an abstraction layer that keeps all our applications away from using Compass, Bourbon, Neat, Foundation or Bootstrap directly. So the only styles dependency that we have in our apps is Paint. This way if we decide to update the grid system, there will be no changes to any of the components or app-specific styles, we just bump the Paint version.

Paint in detail

Paint is currently released as a bower package. For most of our apps it’s as easy as including the package in bower.json.

Paint uses a set of general settings, functions and mixins and a custom set of components.

Let’s say we want to access the grid component. Paint provides a set of sass placeholders like %grid-row or %grid-column-xx that can be used by the apps within their own custom stylesheets:

Application-specific projects.scss

.projects {
  @extend %grid-row;

  .project {
    @extend %grid-column-6;
  }
}

We also have a responsive set of placeholders to call in need, for example

.project {
  @extend %grid-column-small-12;
  @extend %grid-column-medium-6;
  @extend %grid-column-large-4;    
}

Using these placeholders makes the compiled css quite small and, more importantly, it allows us to use a grid system of our choice to generate all the placeholders dynamically. Foundation for this example below:

$grid-prefix: 'grid-column';

@mixin grid-column-placeholders($prefix: $grid-prefix) {
  @for $i from 1 through $total-columns {
    %#{$prefix}-#{$i} {
      @include grid-column($columns: $i, $collapse: false, $float: true);
    }

    %#{$prefix}-#{$i}-centered {
      @include grid-column($columns: $i, $collapse: false, $float: false, $center: true);
    }
  }
}

@include grid-column-placeholders;

The responsive bit was done as easy as

$grid-responsive-media-queries: $small-up, $medium-up, $large-up, $xlarge-up, $xxlarge-up;
$grid-responsive-sizes: 'small', 'medium', 'large', 'xlarge', 'xxlarge';

@for $i from 1 through length($grid-responsive-media-queries) {
  $query: nth($grid-responsive-media-queries, $i);
  $size: nth($grid-responsive-sizes, $i);
  $prefix: '#{$grid-prefix}-#{$size}';

  @media #{$query} {
    @include grid-column-placeholders($prefix: $prefix);
  }
}

%grid-row {
  @include grid-row;
}

If we want to switch to Neat, we just change the include to @include span-columns($i);

With this approach, you only depend on Paint's placeholders and not on Foundation or Neat's mixins, which you can change at any point in time.

The History of Paint

Past - Paint 0.0.1

The very first version of our internal framework was basically an extension of Zurb's Foundation that allowed us to kickstart prototyping real features. The global setting variables set a great ground for our components, while the default components (like buttons or alerts) were enough to save us development time until the final designs were finished.

Present - Paint 0.8.0

Slowly moving away from foundation, with v0.8.0 we’ve started refactoring the components to fully take advantage of the Sass Maps.

One of our code guidelines is to prefix variables based on their purpose, so for a side panel component we would have $side-panel-actions-bar-icon-size.
With the sass maps everything is a lot easier to follow, as we can define:

$side-panel-default-settings: (
  actions-bar: (
    background-color: #e1e5ea,
    button-size: 35px,
    height: 60px
  ),

  footer-bar: (
    height: 40px
  ),

  actions: (
    navigation: (
      previous: angle-left,
      next: angle-right
    )
  )
);

Then use a Sass function side-panel to read the settings map and output the result.
So instead of using width: $side-panel-actions-bar-icon-size we can now use width: side-panel(actions-bar, icon-size), which is a lot easier to understand and maintain.

Customisations are a lot easier to make, since our apps use a paint-settings.scss file where all the settings can be tweaked. Let’s say we want Application Dragon to use a different actions bar height. Adding a

$side-panel: (
  actions-bar: (
    height: 90px
  )
)

overrides the height of the actions bar. Then in Paint we use a custom function to merge custom and default settings into one single map.

This comes in handy especially when dealing with color palettes. So to use green instead of orange as a primary color, we add a

$colors: (
  primary: (
    base: #47C51D
  )
)

then access color(primary) or even shade settings like color(primary, dark) or color(primary, light). Adding app-specific color palettes is also as easy as setting

$colors: (
  dragon-skin: (
    base: #47C51D,
    dark: dragon-skin (darken: 20%, saturate: 20%),
    light: dragon-skin (lighten: 30%)
    transparent: dragon-skin (transparentize: 0.6)
  )
)

then using the internal color(dragon-skin, transparent) to get the computed color.

Future - Paint 1.0

Starting with version 0.9.x, paint is not going to depend on Foundation, moving to smaller library dependencies (like Bourbon / Susy / Neat). It will become more and more modular, include Css regression testing. Also extendable placeholders are going to become optional. This way we allow more room for customisation by making the components less dependable on a certain markup structure or selector types.

Paint is an currently an open-source project and Canvas is going to be released as an open source Ember.js App with Paint 1.0.

Comments

comments powered by Disqus