Leveraging Maps for Scalable and Maintainable SASS

Tanner Langley

SASS Scalability and Maintenance Goals

  1. Quickly update existing code
  2. Know where to add new code
  3. Have a portable code base (project to project)
  4. Have complete control over specificity
  5. Reduce training time for new developers

I want to...

A tiny bit of history

CSS was released in 1994

  • Created to style documents
  • Not meant for applications
  • One layer of abstraction (markup/styles)

How can we make it better?

Step 1:

Separate Identity and Classification

Identifiers

  • Car
  • Tanner's car
  • Nissan
  • Nissan Altima
  • Tanner's Nissan Altima

Classifications

  • Wheels
  • Engine
  • Windshield
  • Seat
  • Trunk
  • Air conditioner

Defining a car

Markup based on Identity

<div class="paragraph--type--simple-cta">
  <h3>Heading</h3>
  <a href="/about">Learn more</a>
</div>

Markup based on Classification (kind of)

<div>
  <h3 class="heading large">Heading</h3>
  <a class="btn btn-medium orange" href="/about">Learn more</a>
</div>

Why "kind of?"

<div>
  <h3 class="heading large">Heading</h3>
  <a class="btn btn-medium orange" href="/about">
    Learn more
  </a>
</div>

Identifier

Classifier

  • Markup should describe identity
  • Styles should define classification

Separation of Concerns

What is the problem?

We usually use both identity and classification

What if we let SASS handle the classifiers?

Bright Idea:

SASS to the rescue!

  • Define mixins
  • Define functions
  • Use placeholders and extends
  • Write variables
  • Problem solved!
.paragraph--type--simple-cta {
  a {
    @include btn($background: $blue, $color: #fff);
  }
}

So simple!

.paragraph--type--simple-cta {
  a {
    // With named parameters
    @include btn($bg: $blue, $color: #fff, $border-color: $blue, $active-bg: transparent, 
        $active-color: $blue, $active-border-color: $blue, $size: small);

    // Without named parameters...
    @include btn($blue, #fff, $blue, transparent, $blue, $blue, small);
  }
}

The realistic example...

Why is this failing to scale?

.paragraph--type--simple-cta {
  a {
    @include btn($blue, #fff, $blue, transparent, $blue, $blue, small);
  }
}

Identifier

What kind of button?

.paragraph--type--simple-cta {
  a {
    @include btn(blue);
  }
}

Identifier

What if we could give that configuration a name?

Classifier

But how?

A lesson from Game Programming

(and mostly from Chris Coyier)

z-index sucks!

  • Hard to test
  • Hard to debug
  • Easy to break things

The Solution?

Use maps to keep all definitions in one place

$zindex: (
  modal     : 9000, 
  overlay   : 8000,
  dropdown  : 7000,
  header    : 6000,
  footer    : 5000
);

.header {
  z-index: map-get($zindex, header);
}

Taking this concept further

Config based buttons

Configuration

$buttons: (
  blue: (
    background: $blue,
    color: #fff,
    border: $blue,
    active-background: #fff,
    active-color: $blue,
    active-border: $blue,
  ),
  green: (
    background: $green,
    color: #fff,
    border: $green,
    active-background: #fff,
    active-color: $green,
    active-border: $green,
  )
);

Usage

.paragraph--type--simple-cta {
  a {
    @include btn(blue);
  }
}

Implementation

Creating a Standalone Framework

Default variables

Color is light blue

Color is dark blue

Mixins with default variables

// framework/_buttons.scss
$buttons: (
  default: (
    color: white,
    background: blue,
    border: blue
  )
) !default;

@mixin btn($key) {
  $config: map-get($buttons, $key);
  color: map-get($config, color);
  background: map-get($config, background);
  border: 1px solid map-get($config, border);
}

Settings

  • Colors
  • Font information
  • Global gutter sizes
  • Global animation duration

Config

Anything that configures pieces of your site

VS

Folder Structure

Base

  • Define global variables
    • colors
    • breakpoints
    • font names
    • etc.
  • Configure variations of similar elements

Framework

  • Define mixins and functions which make use of our configuration
  • Should be completely standalone
  • Never actually styles anything on the page!

Everything else

  • Responsible for applying styles to pages
  • Most things here are specific to your website

Example

Pulling everything together

Mixin definition

// framework/_buttons.scss
$buttons: (
  default: (
    color: white,
    background: blue,
    border: blue
  )
) !default;

@mixin btn($key) {
  $config: map-get($buttons, $key);
  color: map-get($config, color);
  background: map-get($config, background);
  border: 1px solid map-get($config, border);
}

Config definition

// base/_config.scss
$buttons: (
  blue: (
    background: $blue,
    color: #fff,
    border: $blue,
    active-background: #fff,
    active-color: $blue,
    active-border: $blue,
  ),
  green: (
    background: $green,
    color: #fff,
    border: $green,
    active-background: #fff,
    active-color: $green,
    active-border: $green,
  )
);

Usage

.paragraph--type--simple-cta {
  a {
    @include btn(blue);
  }
}

Things that might benefit from being configured in one place:

  • Buttons
  • Max widths
  • Link styles
  • Selector groups
  • Typography
  • Any custom styles which are mostly the same but have variants

Config based containers

Configuration

$max-widths: (
  site-container: (
    width: 1600px,
    gutters: false
  ),
  content: (
    width: 640px,
    gutters: (
      small: 15px,
      medium: 20px
    )
  ),
);

Usage

.paragraph--type--compound-logo-grid {
  @include max-width(content);
}

Revisiting Goals

  • Quickly update existing code
  • Know where to add new code
  • Have a portable code base (project to project)
  • Have complete control over specificity
  • Reduce training time for new developers

I want to...

Some helpful... helpers

The key() function

Key

@function key($map, $key, $sub-key: null) {
  @if map-has-key($map, $key) {
    $val: map-get($map, $key);

    @if $sub-key and map-has-key($val, $sub-key) {
      $val: map-get($val, $sub-key);
    }

    @return $val;
  }

  @warn "Unknown '#{$key}' in '#{$map}'.";
  @return null;
}

map-get() with an extra level

Usage

$nested-map: (
  first-key: (
    nested-key: "I'm a nested key!",
    another: "just another nested key :("
  )
);

@mixin cool-mixin() {
  // value: I'm a nested key!
  $nested-key: key($nested-map, first-key, nested-key); 
}

extend-in-map()

extend-in-map

@function extend-in-map($map-to-search, $sub-map-key) {
  $map-to-merge: key($map-to-search, $sub-map-key);

  @if (map-has-key($map-to-merge, extend)) {
    $key-of-map-to-extend: map-get($map-to-merge, extend);

    @if (map-has-key($map-to-search, $key-of-map-to-extend)) {
      @return map-merge(key($map-to-search, $key-of-map-to-extend), $map-to-merge);
    }
  }

  @return $map-to-merge;
}

Extend a subkey with another subkey

Mixin Usage

@mixin btn($button-key) {
  $button: extend-in-map($buttons, $button-key);
  background: key($button, background);
  color: key($button, color);
  // and so on...
}

Config Usage

$buttons: (
  blue: (
    background: $blue,
    color: #fff,
    border: $blue,
    active-background: #fff,
    active-color: $blue,
    active-border: $blue,
  ),
  blue-transparent: (
    extend: blue,
    active-background: transparent
  )
);

Know when to add parameters

.paragraph--type--simple-cta {
  a {
    @include btn(blue, small);
  }
}

Questions

Leveraging Maps for Scalable and Maintainable SASS

By Tanner langley

Leveraging Maps for Scalable and Maintainable SASS

  • 657