Writing Modular UI Components

November 21, 2015

The benefits of writing modular UI components—if done correctly—are immediately apparent. Such as speeding up development time, reducing bugs and cross-browser inconsistencies, and enabling yourself to work on the aspects of a project that are unique. But when is it appropriate to write modular components and how do we do it correctly?

First, lets get some terminology out of the way:

  • Components: These are groups of elements that—when combined—serve a specific purpose. For example, if you take an application’s header, you can break it apart in elements such as a logo, navigation links, user actions and maybe a search form. The combination of these elements is what makes the header component. In the BEM methodology and frameworks like BaseWeb, this is called a “block”.
  • Modular: This refers to all the assets of a component being self incapsulated or independent. This includes the markup, the base styles and any modifiers it has available. The emphasis being that it creates a scalable, reusable component that can be placed in different contexts while retaining its function.

When deciding whether we should write a modular component, we ask “is this something I write often?” or “Can this be reused?” If a component is so project specific that you never see yourself using it again, it’s not worth writing it modular. But if—for example—you’re writing a pagination component, that’s common in a lot of websites and web applications, so it’s a great candidate. We’ll use it as our case study from here on.

Lets get started by defining the markup:

<nav class="pagination">
  <ul>
    <li><a href="#">1</a></li>
    <li class="active"><a href="#">2</a></li>
    <li><a href="#">3</a></li>
    <li><a href="#">4</a></li>
    <li><a href="#">5</a></li>
    <li class="sep">...</li>
    <li><a href="#">10</a></li>
    <li><a href="#">Next</a></li>
  </ul>
</nav>

This is pretty standard markup for pagination, lets look at the features. We have a class hook .pagination that gives us access to all the elements we’ll need to style. We have an unordered list that contains our page links and a few modifier classes for link separators and styling the active page. Everything looks good and semantic, so now we can put together some generic styles.

.pagination {
  display: block;
  font-family: helvetica, arial, sans-serif;
  font-size: 12px;
  text-align: center;
}
.pagination ul {
  display: inline-block;
  margin: 1em -0.5em 0.5em 0;
  padding: 0;
  list-style: none;

  &:after {
    content: '';
    display: table;
    clear: both;
  }
}
.pagination ul li {
  float: left;
  margin: 0 0.5em 0.5em 0;

  a,
  &.sep {
    display: block;
    padding: 0.75em 1em;
  }

  a {
    background: #efefef;
    border-radius: 3px;
    text-decoration: none;
    color: #16a6e4;
  }
  a:hover,
  &.active a {
    background: #16a6e4;
    color: white;
  }

  &.sep {
    color: #aaa;
  }
}

You can take a look at the output we have so far on SassMeister. Before be get involved with turning this component into a customizable and reusable module, lets make sure we browser test and iron out any possible bugs. This will save you time in the long run. Once that’s done, we can move on to making this code as easy to reuse as possible.

Making it Modular

If this was all we needed for our project, we could stop here and move on to the next item. But knowing that we want to make this a reusable component, we’re going to convert this into a stand-alone _pagination.scss file. This is where we want to figure out the variables and mixins we’ll need to manage, customize and scale our pagination component.

Lets get our file ready first by getting a pagination map and the initial mixins:

$pagination: (
  'classes' : true,

) !default;

@mixin make-pagination($options: ()) {
  $o: map-merge($pagination, $options);

}

@mixin add-pagination-style($options: ()) {
  $o: map-merge($pagination, $options);

}

This is the shell that I start all my SCSS component files with. We’ll add items to our pagination map later, for now lets focus on putting together our make-pagination mixin. When writing component mixins, I like using the naming convention of make- to denote a core mixin and add- for modifier mixins. This makes it very clear which styles are universal and which ones are simply there to modify or enhance the base styles.

Note also how we use the map-merge function in our mixins. This allows us to use the pagination map for our default settings, but also pass in a custom map with any variables we’d like to modify.

For our make-pagination mixin, we want only to place the most core features of the pagination styles. Simply the structure related styles like clear fixes, margins, resetting list styles and floats. This is because we want to control all none essential styles using the modifier mixins.

@mixin make-pagination($options: ()) {
  $o: map-merge($pagination, $options);

  display: block;

  ul {
    display: inline-block;
    margin: 1em -0.5em 0.5em 0;
    padding: 0;
    list-style: none;

    &:after {
      content: '';
      display: table;
      clear: both;
    }
  }

  ul li {
    float: left;
    margin: 0 0.5em 0.5em 0;
  }
}

Then we can take the rest of the styles and add them to our add-pagination-style mixin:

@mixin add-pagination-style($options: ()) {
  $o: map-merge($pagination, $options);

  font-family: helvetica, arial, sans-serif;
  font-size: 12px;
  text-align: center;

  ul li {
    a,
    &.sep {
      display: block;
      padding: 0.75em 1em;
    }

    a {
      background: #efefef;
      border-radius: 3px;
      text-decoration: none;
      color: #16a6e4;
    }
    a:hover,
    &.active a {
      background: #16a6e4;
      color: white;
    }

    &.sep {
      color: #aaa;
    }
  }
}

And now we can apply our styles like this:

.pagination {
  @include make-pagination();
  @include add-pagination-style();
}

Now that we’ve separated our styles into mixins, we can start adding variables to our pagination map and substitute them in our mixins.

$pagination: (
  'classes': true,

  'class': 'pagination',
  'class-active': 'active',
  'class-sep': 'sep',

  'margin': 1em,
  'spacing': 0.5em,

  'font-size': 12px,
  'font-family': (helvetica, arial, sans-serif),
  'text-align': center,
  'color': #aaa,

  'link-color': #16a6e4,
  'link-color-hover': #fff,
  'link-background': #efefef,
  'link-background-hover': #16a6e4,
  'link-padding': 0.75em 1em,
  'link-border': null,
  'link-border-radius': 3px,

) !default;
@mixin make-pagination($options: ()) {
  $o: map-merge($pagination, $options);

  display: block;

  ul {
    display: inline-block;
    margin: map-get($o, 'margin') (-(map-get($o, 'spacing'))) (map-get($o, 'margin') - map-get($o, 'spacing')) 0;
    padding: 0;
    list-style: none;

    &:after {
      content: '';
      display: table;
      clear: both;
    }
  }

  ul li {
    float: left;
    margin: 0 map-get($o, 'spacing') map-get($o, 'spacing') 0;
  }
}
@mixin add-pagination-style($options: ()) {
  $o: map-merge($pagination, $options);

  font-family: map-get($o, 'font-family');
  font-size: map-get($o, 'font-size');
  text-align: map-get($o, 'text-align');

  ul li {
    a,
    &.sep {
      display: block;
      padding: map-get($o, 'link-padding');
    }
    a {
      background: map-get($o, 'link-background');
      border: map-get($o, 'link-border');
      border-radius: map-get($o, 'link-border-radius');
      text-decoration: none;
      color: map-get($o, 'link-color');
    }
    a:hover,
    &.active a {
      background: map-get($o, 'link-background-hover');
      color: map-get($o, 'link-color-hover');
    }
    &.sep {
      color: map-get($o, 'color');
    }
  }
}

.pagination {
  @include make-pagination();
  @include add-pagination-style();
}

You might be wondering where we use the $pagination('classes') variable. I typically use this when I want to give anyone who uses this component the option to strictly use the mixins themselves and disable the class output all together. We can write something like this around our style declarations:

@if (map-get($pagination, 'classes') == true) {
  .pagination {
    @include make-pagination();
    @include add-pagination-style();
  }
}

Now, when we set $pagination('classes') to false we disable any CSS output unless the developer uses a mixin directly. This is especially handy if we don’t want the bloat that may come with more complicated modules and their class based outputs.

We can also use the set of $pagination('class') variables, where we store strings of class names to use in our components. We can then output them using the #{} interpolation syntax. This gives us full control of the CSS output and doesn’t force a developer to use a predefined class naming convention.

.#{map-get($pagination, 'class')} {
  @include make-pagination();
  @include add-pagination-style();
}

Final Tips and Enhancements

That wraps up the basics of creating modular UI components. Now I want to go over a few things I like to add to my files which make them much easier to manage and hand off to other developers.

Keep an eye on your CSS

Output a set of CSS files in your build process. One should be the production file which would strip out all comments and minify the CSS so it’s ready for distribution. The second is just a full output—with comments—of our CSS. This lets us get a sense of what your Sass is actually outputting. If we see an issue in the output this makes it much easier to pin point the problem area in our SCSS files.

Make your CSS search friendly

I like to add “anchor comments” to my component files. The production CSS can end up being lengthy, pulling in over a dozen separate SCSS files depending on the project. so we want to be able to search and find specific areas in our CSS easily.


/*==============================================================================
  @Pagination
==============================================================================*/

...

/**
 * Some subheader
 * Maybe a short description here
 */
...

Now we can search for @pagination and be taken to exactly what we were looking for. Another awesome feature is including the file name inside of each anchor comment so someone working on your styles knows exactly what file is responsible for each section of CSS output:

// Set file variable
$filename: 'scss/blocks/_pagination.scss';

/*==============================================================================
  @Pagination - #{$filename}
==============================================================================*/

Scaling our mixins

Regarding mixins, we already went over the concept of using make- and add- naming conventions for core and modifier mixins respectively. But if you have created modifier styles that are not compatible, then they should not be mixed with other mixins. I like to toggle them within the same mixin using optional parameters. If we had two completely different sets of styles types for our pagination, we might re-write our add-pagination-style mixin like this:

@mixin add-pagination-style($options: (), $type: 'simple') {
  $o: map-merge($pagination, $options);

  // Base styles go here that are shared accross style types

  @if ($type == 'simple') {

    // Our simple specific styles go here

  } @else if ($type == 'fancy') {

    // Our fancy specific styles go here

  } @else {
    @warn 'The output type "#{$type}" is not supported in `add-pagination-style()`';
  }
}

// Here, we making these styles available through classes
.pagination.simple {
  @mixin include add-pagination-style($type: 'simple');
}
.pagination.fancy {
  @mixin include add-pagination-style($type: 'fancy');
}

If we pass in a $type option that’s not supported, we’ll output a warning message to the console on build. But now we can output the style types we want to use in our project, and expand our style types to our hearts content.

Conclusion

Now we have a modular pagination component that we can build on as needed. You can grab the final component from the GitHub Gist or you can play around with it over at SassMeister. This is a simple example, but it illustrates both the benefits of modular code and the methods I use when building reusable components.