Sass Map Magic

@EricMSuzanne | @OddestBirds | @SassSusy

Sass Maps != Sourcemaps

[please exit quietly]

Sass Maps!

$map: (
  key: value,
  another key: another value,
);

No Magic

Hipsters used maps before Sass existed.

# Ruby
phonebook = {
    'Sally Smart' => '555-9999',
    'John Doe' => '555-1212',
}
# Python
phonebook = {
  'Sally Smart' : '555-9999',
  'John Doe' : '555-1212',
}
# YAML
Sally Smart: 555-9999
John Doe: 555-1212
J. Random Hacker: 555-1337
# JSON
{
  "Sally Smart": "555-9999",
  "John Doe": "555-1212",
}
# Javascript
var myObject = {
  "Sally Smart"      : "555-9999",
  "John Doe"         : "555-1212",
};
# PHP
$phonebook = [
  'Sally Smart'      => '555-9999',
  'John Doe'         => '555-1212',
];
Samples from Wikipedia.

Remember CSS?

.selector {
  background: white;
  border: 1em solid pink;
}

MapCSS!

$variable: (
  background: $white,
  border: 1em solid $pink,
);

Mundane Sass

Looks for lists of related variables

// Before
$color-black: #333;
$color-white: #EEE;
$color-pink: #FF0080;
$color-blue: #1AC6FF;

Organized Maps

// After
$colors: (
  black: #333,
  white: #EEE,
  pink: #FF0080,
  blue: #1AC6FF,
);

Sass Insanity

@include span(3, 10, 12, $output: isolate, $math: static, $flow: rtl, $gutter-position: inside);

Flexible Syntax

@include span(isolate 3 at 10 of 12 static rtl inside);

Let's Make Buttons!

Because it has never been done before.

This is a queer interlude.

Button Config

@inlude button(large alert);

// All the options in one (editable) map
$button-options: (
  style: flat glow outline,
  corners: round square pill,
  display: inline block inline-block,
  color: map-keys($colors),
  icon: map-keys($icons),
  size: map-keys($sizes),
  preset: map-keys($buttons),
);
// My styleguide
$buttons: (
  submit: glow round,
  call-to-action: glow square large block,
  secondary: submit text outline small,
  alert: submit stop skull,
  social: flat pill,
);

Back to Basics

Creating Maps

$map-magic: ();

Creating Maps

$map-magic: (
  hello: world,
  name: Eric Suzanne,
);

Merging Maps

map-merge($map1, $map2)

// Map One
$map-magic: (
  hello: world,
  name: Eric Suzanne,
);

// Map Two
$blendconf: (
  hello: Charlotte,
  audience: far away,
);

Merging Maps

Second value takes precedence.

$tonight: map-merge($map-magic, $blendconf);

// After
$tonight: (
  hello: Charlotte,
  name: Eric Suzanne,
  audience: far away,
);

Editing Values

Merge all the things. There is no map-set().

// Edit
$tonight: map-merge($tonight, (audience: "smart and attractive!"));

// Result
$tonight: (
  hello: Charlotte,
  name: Eric Suzanne,
  audience: "smart and attractive!",
);

Adding Pairs

Merge all the things. There is no map-set().

// Add
$tonight: map-merge($tonight, (month: September));

// Result
$tonight: (
  hello: Charlotte,
  name: Eric Suzanne,
  audience: far away,
  month: September,
);

Removing Pairs

map-remove($map, $key)

// Remove
$tonight: map-remove($tonight, month);

// Result
$tonight: (
  hello: Charlotte,
  name: Eric Suzanne,
  audience: "smart and attractive!",
);

Accessing Map Pairs

map-get($map, $key)

.talk::before {
  content: "Hello, " map-get($tonight, hello) "! ";
}

// Result
.talk::before {
  content: "Hello, Charlotte!",
}

Keys & Values

// map-has-key(map, key) => True/False
$has-hello: map-has-key($tonight, hello);

// map-keys(map) => List of Keys
$keys: map-keys($tonight);

// map-values(map) => List of Values
$values: map-values($tonight);

Reading Maps

.print-tonight {
  @each $key, $value in $tonight {
    #{$key}: $value;
  }
}

// Result
.print-tonight {
  hello: Charlotte;
  name: Eric Suzanne;
  audience: "smart and attractive!";
}

Map Arguments

$alert: (
  style: flat,
  size: large,
  color: red,
);

@include button($alert);

Variable Arguments

"..." expands each pair into a keyword argument

$alert: (
  style: flat,
  size: large,
  color: red,
);

@mixin $button($style, $color, $size) {/* clever mixin */}
@include button($alert...);

Anything Goes!

I dare you to find a good use case.

$OMG-STOP: (
  14 - floor(13/2): math, // map-get($OMG-STOP, 8)
  : (first: Eric, last: Suzanne),
  (one, two, three): 1 2 3,
  (tests: 12, pass: 0, fail: 12): "All your tests failed!",
);

Gimme 'em goram buttons, Ms. Suzanne

Now, please.

Button Components

The Yack-Shaving Approach™

  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Font Icons

[class^="icon-"], [class*=" icon-"] {
  font-family: 'magic';
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-variant: normal;
  text-transform: none;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
Icons from IcoMoon.

Sass Placeholder

%icon {
  font-family: 'magic';
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-variant: normal;
  text-transform: none;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.icon-skull:before {
  content: "\e18c";
}
.icon-warning:before {
  content: "\e243";
}
.icon-checkmark:before {
  content: "\e258";
}
.icon-instagram:before {
  content: "\e32e";
}
.icon-twitter:before {
  content: "\e32f";
}
.icon-dribbble:before {
  content: "\e341";
}
.icon-github:before {
  content: "\e34c";
}
.icon-soundcloud:before {
  content: "\e35d";
}
.icon-stackoverflow:before {
  content: "\e367";
}

Icon Map

$icons: (
  skull: "\e18c",
  warning: "\e243",
  checkmark: "\e258",
  instagram: "\e32e",
  twitter: "\e32f",
  dribbble: "\e341",
  github: "\e34c",
  soundcloud: "\e35d",
  stackoverflow: "\e367",
);

Data-Icon Loop

[data-icon]:before {
  @extend %icon;
  content: attr(data-icon);
}

@each $name, $glyph in $icons {
  [data-icon='#{$icon}']:before {
    content: map-get($icons, $name);
  }
}

Icon Function

@function icon($icon) {
  // Get from map, or trust me.
  $icon: map-get($icons, $icon) or $icon;
  @return $icon;
}

@each $name, $glyph in $icons {
  [data-icon='#{$icon}']:before {
    content: icon($name);
  }
}

Icon Mixin

@mixin icon($name) {
  &:before {
    @extend %icon;
    content: icon($name);
    @content;
  }
}

@each $name, $glyph in $icons {
  [data-icon='#{$icon}'] {
    @include icon($name);
  }
}

Data-Icon

  <span data-icon="checkmark"></span>

Sass-only Icon

.title { @include icon('checkmark'); }

Map Self-Reference

$icons: (
  skull: "\e18c",
  warning: "\e243",
  checkmark: "\e258",

  // Add self-referential names
  escher: skull,
  godel: escher,
  bach: godel,
  kevin bacon: bach,
);

Recursive Function

@function icon($icon) {
  $icon: map-get($icons, $icon) or $icon;

  // Add self-calling lookup
  @if map-has-key($icons, $icon) {
    $icon: icon($icon);
  }

  @return $icon;
}

Six Degrees of Icon

.title { @include icon(kevin bacon); }

Using @content

.title {
  @include icon(kevin bacon) {
    background: red;
    color: white;
    margin-right: .4em;
  }
}
  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Color Variables

$color-dark: #333;
$color-light: #eee;
$color-brand: hsl(330, 100%, 50%);
$color-accent: hsl(195, 100%, 55%);

$color-twitter: #00aced;
$color-instagram: #517fa4;
$color-stackoverflow: #F47920;
$color-soundcloud: #FF6600;
$color-dribbble: #EA4C89;
$color-github: #4183C4;

Colors Map

$colors: (
  dark: #333,
  light: #eee,
  brand: hsl(330, 100%, 50%),
  accent: hsl(195, 100%, 55%),

  twitter: #00aced,
  instagram: #517fa4,
  stackoverflow: #F47920,
  soundcloud: #FF6600,
  dribbble: #EA4C89,
  github: #4183C4,
);

Recursive Colors

$colors: (
  dark: #333,
  light: #eee,
  brand: hsl(330, 100%, 50%),
  accent: hsl(195, 100%, 55%),

  text: dark,
  action: accent,
);

Recursive Errors

$colors: (
  brand: hsl(330, 100%, 50%),

  // ERROR: map doesn't yet exist...
  accent: adjust-hue(brand, 180deg),
  accent: adjust-hue(map-get($colors, brand), 180deg),
);

Possible Solution?

$colors: (
  brand: hsl(330, 100%, 50%),

  // Define now
  accent: brand (adjust-hue: 180deg),
);

// Calculate later
a { color: color(accent); }
@function color($color) {
  // Parse arguments
  $color: map-get($colors, $color) or $color;
  $base: nth($color, 1);
  $adjust: if(length($color) > 1, nth($color, 2), ());

  // Recursive check
  $color: if(map-has-key($colors, $base), color($base), $base);

  // Adjustments [consider function-exists() warning]
  @each $function, $values in $adjust {
    $color: call($function, $color, $values...);
  }

  @return $color;
}
  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Modular Scale

$size-normal: 24px;
$ratio: 5/4;

$size-rhythm: modular-scale(1);
$size-h1: modular-scale(4);
$size-large: modular-scale(2);
$size-medium: $size-rhythm;
$size-small: modular-scale(-2);
$size-shim: .125em;

Scale Maps

$base-size: 24px;
$ratio: 5/4;

$sizes: (
  normal: modular-scale(0),
  rhythm: modular-scale(1),

  h1: modular-scale(4),
  large: modular-scale(2),
  medium: normal,
  small: modular-scale(-2),
  shim: .125em,
);

Named Ratios

$ratio-options: (
  octave            : 2,
  major-seventh     : 15/8,
  major-sixth       : 5/3,
  fifth             : 3/2,
  augmented-fourth  : 45/32,
  fourth            : 4/3,
  major-third       : 5/4,
  major-second      : 9/8,
);

$ratio: major-third;

Simpler Scale

$base-size: 24px;
$ratio: major-third;
$sizes: (
  normal: 0,
  rhythm: 1,

  h1: 4,
  large: 2,
  medium: normal,
  small: -2,
  shim: .125em,
);

Scale Retrieval

@function get-size($size) {
  $size: map-get($sizes, $size) or $size;
  @if map-has-key($sizes, $size) {
    $size: get-size($size);
  }
  @return $size;
}

@function get-ratio($ratio: $ratio) {
  $ratio: map-get($ratio-options, $ratio) or $ratio;
  @if map-has-key($ratio-options, $ratio) {
    $ratio: get-ratio($ratio);
  }
  @return $ratio;
}

Size Function

@function size(
  $size
) {
  $size: get-size($size);

  @if unitless($size) {
    $ratio: get-ratio($ratio);
    $size: round($base-size * pow($ratio, $size));
  }

  @return $size;
}
  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Display Options

Button Shorthand

button: icon size color display shape preset

Parse Shorthand

$button: parse(large flat pill checkmark inline-block);

// Result
$button: (
  size: large,
  style: flat,
  shape: pill,
  display: inline-block,
  icon: checkmark,
);

Options Map

$button-options: (
  // List of display keywords
  display: inline block inline-block,
);
@function parse($shorthand, $keywords){
  @if type-of($shorthand) == map {
    @return $shorthand;
  } @else {
    $return: ();
    @each $item in $shorthand {
      @if type-of($item) == map {
        $return: map-merge($return, $item);
      } @else {
        @each $setting, $options in $keywords {
          @if index($options, $item) {
            $return: map-merge($return, ($setting: $item));
          }
        }
      }
    }
    @return $return;
  }
}

Input => Output

@mixin button(
  $style
) {
  // parse
  $style: parse($style);

  // display-setting output
  display: map-get($style, display);
}
  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Button Shapes

Shape Options

$button-options: (
  shape: round square pill,
  display: inline block inline-block,
);

Shape Function

@function btn-shape(
  $input
) {
  $shape: map-get($input, shape);
  @if $shape == round {
    @return size(shim);
  } @else if $shape == pill {
    @return 50em;
  } @else {
    @return 0;
  }
}
@mixin button(
  $style
) {
  // parse
  $style: parse($style);

  // Output
  border-radius: btn-shape($input); // Shape logic
  display: map-get($style, display);
}
  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Button Styles

Style Options

$button-options: (
  style: flat glow outline,
  shape: round square pill,
  display: inline block inline-block,
);

Colors, Sizes & Icons

@function btn-parse(
  $input
) {
  // Add color, size, and icon keyword options
  $options: map-merge($button-options, (
    color: map-keys($colors),
    icon: map-keys($icons),
    size: map-keys($sizes),
  ));

  $input: parse($input, $options);

  @return $input;
}

Custom Functions

@mixin button($style) {
  $input: btn-parse($style);
  $output: (
    display: map-get($input, display),
    border-radius: btn-shape($input),
  );

  $style: map-get($input, style);
  @if function-exists('btn-#{$style}') {
    $output: map-merge($output, call('btn-#{$style}', $input));
  }

  @include render($output);
}

Style Functions

@function btn-flat(
  $input
) {
  $color: color(map-get($input, color));
  $output: (
    background: $color,
    color: contrast($color),
    border-radius: btn-radius($input),
  );

  @return $output;
}
@function btn-outline($input) {
  // ...outline logic Here...
  @return $output;
}

@function btn-glow($input) {
  // ...glow logic Here...
  @return $output;
}

Render Maps

@mixin render(
  $css
) {
  @each $key, $value in $css {
    #{$key}: $value;
  }
}

Render Maps

$output: (
  display: inline-block,
  border-radius: .25em,
);

.btn { @include render($output); }

// Result
.btn {
  display: inline-block;
  border-radius: .25em;
}

Nested Output

@function btn-icons($input) {
  $output: null;
  $icon: map-get($input, icon);
  @if $icon {
    $output: (
      extend: '%icon',
      content: icon($icon),
      margin-right: .4em,
    );
  }
  @return $output;
}

Render Nested

@mixin render(
  $css
) {
  @each $key, $value in $css {
    @if type-of($value) == map {
      // Recursive...
      #{$key} { @include render($value); }
    } @else if $key == 'extend' {
      @extend #{$value};
    } @else {
      #{$key}: $value;
    }
  }
}
@mixin button($style) {
  $input: btn-parse($style);

  $output: (
    display: map-get($input, display),
    border-radius: btn-shape($input),
    font-size: map-get($input, size),
    '&:before': btn-icons($input),
  );

  $style: map-get($input, style);
  @if function-exists('btn-#{$style}') {
    $output: map-merge($output, call('btn-#{$style}', $input));
  }

  @include render($output);
}
  1. Icons
  2. Colors
  3. Sizes
  4. Display
  5. Shapes
  6. Styles
  7. Defaults & Presets

Global Defaults

$button-defaults: (
  style: flat,
  shape: round,
  display: inline-block,
  color: action,
  icon: false,
  size: medium,
);

User Config

$button-config: (
  style: glow,
  shape: square,
);

Get Config

@function btn-get(
  $setting,
  $config: $button-config
) {
  @return map-get($config, $setting)
          or map-get($button-config, $setting)
          or map-get($btn-defaults, $setting);
}

Named Presets

Named Presets

$buttons: (
  submit: glow round,
  call-to-action: glow square large block,
  secondary: submit text outline small,
  alert: submit stop skull,
  social: flat pill,
);
@function btn-parse($input) {
  $options: map-merge($button-options, (
    color: map-keys($colors),
    icon: map-keys($icons),
    size: map-keys($sizes),
    preset: map-keys($buttons),
  ));

  $input: parse($input, $options);

  // Recursive presets...
  $preset: map-get($input, preset);
  @if map-has-key($buttons, $preset) {
    $preset: btn-parse(map-get($buttons, $preset));
    $input: map-merge($preset, $input);
  }

  @return $input;
}

OMG We're Done

@EricMSuzanne | @OddestBirds | @SassSusy

Social Buttons

Same names for colors & icons!

$social: twitter instagram stackoverflow soundcloud dribbble github;

@each $site in $social {
  .btn--#{$site} {
    @include button(social $site);
  }
}

BOOM