Rethinking the wehkamp.com grid
Using the Sass flexibility to serve the business.
One stable grid
As a developer, every once in a while you get to the point where you have to completely rewrite your own code from scratch. Because of a new platform architecture, or due to new business needs.
When we began scaffolding the grid-system for the wehkamp.com interface, we started out with the same basic principle Twitter used with Bootstrap for their responsive grid: a fairly simple, twelve-column structure to accommodate a wide range of viewports. Columns could live in rows, and were housed in a central container spacing unit. Although we could’ve implemented the whole Bootstrap library, we chose to start with only their methodology and start writing a lightweight, scalable User Interface kit ourselves without all the bloat that comes with an existing UI framework.
At wehkamp.com, everything revolves around showcasing our products to our visitors in the best possible way, which of course depends on a robust yet flexible grid. On our search and category pages, we have a vertical navigation tree and filtering on the left hand side from which you drill down your search and view the products on the right, three in a row.
At the largest supported width (1280 pixels — mostly displayed on desktop-like screens), the twelve column structure provides four sections covering three columns each: one section for the navigation tree and three sections for the products and other features like sorting, swap view and paging.
<div class="container">
<div class="row">
<div class="col-md-span3 nav-section">
<!-- nav component -->
</div>
<div class="col-md-span9 product-section">
<div class="row">
<div class="col-md-span4">
<!-- product -->
</div>
<div class="col-md-span4">
<!-- product -->
</div>
<div class="col-md-span4">
<!-- product -->
</div>
</div>
</div>
</div>
</div>
Nothing really fancy here. Everyone who worked with Bootstrap before can relate to the code, which was one of the main advantages when working with several teams in the same codebase.
“col-” as component identifier, “md-” as breakpoint handler, and “span* ” to handle the eventual width being adopted.
Rethink your code
As features and business requirements of the project progressed, we found ourselves having quite a debate when this one user story came along:
As a customer
I want to see more products in a row
So I can get a better glance at the catalog
So basically it meant we had to add an extra product in to each row. This way products were shown big enough on desktop view but still had the perfect dimension on smaller devices. Sure, we could have changed the product width in the product section alone, from it’s original 33% to 25% and be done with it. But that also meant that the widths of the product columns in relation to the navigational column were not in sync.
In addition, when you stack other components below the productlist like ‘Last viewed products’ or general product recommendations which span the total container width, an unwanted misalignment would give the page a messy look & feel.
Grid wireframing
While staying structurally and visually sane, we had to think of a way to accommodate five columns and at the same time be flexible enough to answer four-three-two column needs on smaller devices. But although we have a clean and distinct project codebase, changing grid related css-classes in every template a year in production would definitely require some regression testing, both manually and automatically.
So to make things more clear, we made three sketches to visualize the change before we started altering all templates in the application: one which represents the current situation as a baseline, one that could be done with our current grid setup — but with its drawbacks, and one that what turned out to be the one that we build eventually: Multi-gridded!
The original setup where product columns as well as navigational sections span equal widths.
The most easy way to get the user story done: rename the product column css-class from col-xl-span4 to col-xl-span3 where needed, producing an unwanted side-effect: a global difference between columns and components.
The eventual column setup with the new, flexible multi-width-column grid-system, providing every possible solution while remaining consistent across the whole website.
Getting Sassy with it!
Since we had Sass as our css-preprocessor from the beginning of the project, the idea was to have the new grid requirements work for us in a simple function which loops over every variable.
Setup basic variables
First, we define a basic setup to handle all widths that can be used across the whole User Interface library for mixins, media-queries and containers. We already had these values in our settings file from day one, so we did not alter these when we started working on the new grid setup.
$width: 0;
$width-xs: 480px !default;
$width-sm: 640px !default;
$width-md: 768px !default;
$width-lg: 992px !default;
$width-xl: 1280px !default;
Thanks to the !default flag, a you can define an own $width variable before the library includes without seeing your value redefined.
Define grid types
Secondly, we define an array that is responsible for handling the total amount of columns and is being used in the iterator later on as multiplier in the main Sass function to parse the total grid. This was the first extension we made in comparison with the old grid setup.
$grid-types: (
10,
12
);
The setup is very easy to extend for when new grid-types should be created.
Arrayify labels and widths
To implement the eventual css-classes in our HTML markup we need an easy handler that represents the specific breakpoint. We define it as the first parameter in the array as label, and add the corresponding width variable right next to it, which we declared earlier.
We used this same Bootstrap naming concept with the old grid-system but that was mainly hard-coded and not very easily adjustable. Using a global array was the new way to go.
$breakpoints: (
"" $width,
"xs" $width-xs,
"sm" $width-sm,
"md" $width-md,
"lg" $width-lg,
"xl" $width-xl
);
Creating arrays in Sass is very straight-forward. You can use nested lists without braces using different separators to distinguish levels, and add aliases to them in your loops and functions later on.
The media-query mixin
Since before our grid rebuild, along with the basic width variables, we had a simple mixin we could use within any css-selector.
/* Breakpoint mixin */
@mixin breakpoint($width, $type: min-width) {
@if (str-index($type, max-)) {
$width: $width - 1;
}
@media screen and ($type: $width) {
@content;
}
}/* Usage */
.nav {
padding: $base-spacing; @include breakpoint($width-md) {
padding: $base-double-spacing;
}
}
Most of the time we only use this to define things mobile-first, where your notation represents a specific width and up, but optionally you can add a second parameter to define a max-width where the ‘minus one’ is automatically calculated. The @content takes care of all declarations being injected in the new media-query.
The grid generator
So much for the setup. Let’s get calculating. We have a set of breakpoints with corresponding widths, a set of grid-types, and per grid type all column-spans to be calculated.
@each $breakpoint in $breakpoints {
$alias: nth($breakpoint, 1);
$width: nth($breakpoint, 2);
}
First, we loop over the breakpoints array. This is the first handler we encounter after the “col-”. Within this array, two parameters are set, so we can access those by defining an alias. The nth(var, 1) notation represents the first parameter within the first level.
@include breakpoint($width) {
}
Second, we implement the breakpoint mixin so we can parse the $width from the loop over every breakpoint handler.
@each $grid-type in $grid-types {
}
Third, we loop over the grid-types. In our case, this is the 10 and 12 factor we declared earlier. This array can easily be extended as new widths could be designed in the future.
$breakpoint-handler: if( $alias != "", $alias + "-" , "" );
If an alias is empty — from 0 to the first breakpoint-handler xs, no additional dash for readability has to be inserted so we add an extra if-statement to evaluate this. The $breakpoint-handler takes the $alias we defined earlier and puts this in the eventual column declaration.
@for $i from 1 through $grid-type {
}
To generate the correct amount of columns per grid-type per breakpoint segment, a for-loop with $grid-type as max value for the amount of columns is the ideal candidate. The $i variable from the iterator can be used as index for each column and as a multiplier in the width property.
.col-#{$breakpoint-handler}#{$i}-#{$grid-type} {
width: #{(100 / $grid-type * $i) + '%'};
}/* without breakpoint handler */
.col-1-10 {
width: 10%;
}/* with breakpoint handler */
.col-xs-1-10 {
width: 10%
}
When all loops and aliases are in place, we can actually begin to write some lines that eventually render to real CSS. The main selector handles the width, as were the additional selectors are responsible for pulling, pushing or offsetting whitespace for flexibility.
We use the Sass interpolation notation #{ } to make sure everything is calculated before it is finally rendered in to CSS.
The final function
Putting everything together, our Sass grid generator looks like this:
@each $breakpoint in $breakpoints {
$alias: nth($breakpoint, 1);
$width: nth($breakpoint, 2);
@include breakpoint($width) {
@each $grid-type in $grid-types {
$breakpoint-handler: if( $alias != "", $alias + "-" , "" );
@for $i from 1 through $grid-type {
.col-#{$breakpoint-handler}#{$i}-#{$grid-type} {
width: #{(100 / $grid-type * $i) + '%'};
}
.col-#{$breakpoint-handler}push#{$i}-#{$grid-type} {
left: #{(100 / $grid-type * $i) + '%'};
}
.col-#{$breakpoint-handler}pull#{$i}-#{$grid-type} {
right: #{(100 / $grid-type * $i) + '%'};
}
.col-#{$breakpoint-handler}offset#{$i}-#{$grid-type} {
margin-left: #{(100 / $grid-type * $i) + '%'};
}
}
}
}
}
The column properties are an addition to the general properties declared in the base column selector before the grid generator function:
*[class*="col-"] {
float: left;
min-height: 1px;
padding-left: $base-grid-spacing;
padding-right: $base-grid-spacing;
position: relative;
width: 100%;
}
Sprint demo
The user story was reviewed and pushed to production in no-time. And although during the sprint demo not every stakeholder understood what was going on under the hood, it was nice to share some of the logic that was implemented in this story. Giving some extra context brings better understanding to everyone inside and outside the team that it is not always as simple as just putting an extra product in each row.
Before
Fixed twelve-column structure only.
<div class="col-md-span4">One Third from 768px and up</div>
After
Self-explanatory multi-column structure. Scalable, maintainable, simple.
<div class="col-md-4-12">One Third from 768px and up</div>
<div class="col-xl-2-10">One Fifth from 1280px and up</div>
There you have it. A compact and extendable Sass function driven by two simple arrays and one list of width variables with a very flexible outcome: a robust and future-proof grid-system which can easily be extended when needed. Happy stakeholders, happy developers.
And some nice story points to add to our Jira burn down chart too.
This entry was originally posted on wehkamplabs.com