Sweating the Small Stuff: Recreating Subtle Design Details Using Sass

timhettler.github.io/sweating-small-stuff/

@timhettler

Open Standards Developer
at R/GA in NYC


timhettler.github.io/sweating-small-stuff/

I'm going to talk about this button
for the next 40(ish) minutes

Designers created this button
in Photoshop:

Normal State Active State

This is what the devs delivered:

Normal State Active State

Designers:
The Drop Shadow is at a 90° angle with a distance of 1px.

Developers:
So… what is the x- and y-offset?

Designers:
The Gradient Overlay uses the multiply blend mode.

Developers:
What the &%$^ is a blend mode?

Designers & developers
aren't speaking the same language

Traditional process

Lots of failure points in the traditional translation process

We can automate this translation

Sass is the rosetta stone between designers & developers

Let's rebuild our button using Sass

Part 1:
Abstraction

Button.Prototype
%button {
  @include box-sizing('border-box');
  @include user-select('none');
  -webkit-touch-callout: none;
  background-clip: padding-box;
  border: 0;
  border-radius: 0.2em;
  cursor: pointer;
  display: inline-block;
  font: 100%/2.6 "Trade Gothic LT Std", sans-serif;
  letter-spacing: -1px;
  margin: 0;
  padding: 0 1em;
  position: relative;
  text-align: center;
  text-decoration: none !important;
}
.button-orange {
  @extend %button;
  background-color: #fa5400;
}
.button-blue {
  @extend %button;
  background-color: #00a4e4;
}
.button-orange, .button-blue {
  …
}
.button-orange {
  background-color: #fa5400;
}
.button-blue {
  background-color: #00a4e4;
}

Placeholders allow you to write code that is easy to read in developlement,

without sacrificing speed & efficiency in production.

Determining Text Color
Dark Buttons Light Buttons

Compass function
to determine color lightness

@function contrast-color(
  $color,
  $dark: $contrasted-dark-default,
  $light: $contrasted-light-default,
  $threshold: $contrasted-lightness-threshold
) {
  @return if(lightness($color) < $threshold, $light, $dark)
}

compass-style.org/reference/compass/utilities/color/contrast/

Appropriate text-color gets set
programatically

.dark-button {
  @extend %button;
  @include contrasted(#983168, #333, #fff);
}

.light-button {
  @extend %button;
  @include contrasted(#ffd204, #333, #fff);
}
.dark-button {
  …
  background-color: #983168;
  color: #fff;
}

.light-button {
  …
  background-color: #ffd204;
  color: #333;
}
Default Contrast Settings
#DC404A #87e300 #00A4E4
50% Lightness Threshold
hsl(356, 69%, 56%) hsl(84, 100%, 45%) hsl(197, 100%, 45%)
The eye is more sensitive
to changes
in theorange-blue range than
in the purple-green range
The YIQ color space
@function yiq-contrast-color(
  $color,
  $dark: #000,
  $light: #fff
) {
  $red: red($color);
  $green: green($color);
  $blue: blue($color);

  $yiq: ( ( $red * 299 ) + ( $green * 587 ) + ( $blue * 114 ) ) / 1000;

  @return if($yiq >= 128, $dark, $light);
}

GITHUB.COM/TIMHETTLER/COMPASS-YIQ-COLOR-CONTRAST

YIQ COLOR CONTRAST
111.784 173.614 122.26

SASS turns a designer's
"gut feeling" into a
repeatable pattern

Managing State

Dark Buttons Lighten

Normal State Hover State

Light Buttons Darken

Normal State Hover State
Sass Color Functions
.dark-button {
  …
  @include yiq-contrasted(#fa5400);

  &:hover {
    background-color: adjust-color(#fa5400, $lightness: 5);
  }
}

.light-button {
  …
  @include yiq-contrasted(#87e300);

  &:hover {
    background-color: adjust-color(#87e300, $lightness: -5);
  }
}
.dark-button {
  …
  background-color: #fa5400;
  color: #fff;
}

.dark-button:hover {
  background-color: #ff6315;
}

.light-button {
  …
  background-color: #87e300;
  color: #333333;
}

.light-button:hover {
  background-color: #78ca00;
}

Using SASS,
Developers &
Designers
can speak the same language

Part 2:
Photoshop to CSS

Drop Shadow to
box-shadow()

Box-Shadow CSS Syntax

offset-x offset-y (blur-radius) (spread-radius) (color) (inset)

box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.25);

Photoshop Drop Shadow
  • Color
  • Opacity
  • Angle
  • Distance
  • Spread
  • Size

Calculating the missing sides of our triangle: SOHCAHTOA

$x-offset: round( cos( $angle ) * $distance )
$y-offset: round( sin( $angle ) * $distance )
@function photoshop-shadow( $angle: 120, $distance: 0, $spread: 0, $size: 0, $color: #000, $inner: false ) {

  $x-offset: round( cos( $angle ) * $distance );
  $y-offset: round( sin( $angle ) * $distance );
  $css-spread: $size * ( $spread/100 );
  $blur: $size - $css-spread;
  $inset: if( $inner != false, 'inset', '' );

  @return ( $x-offset $y-offset $blur $css-spread $color unquote($inset) );
}

github.com/heygrady/compass-photoshop-drop-shadow

Adding box-shadow() to our button
.button {
  …
  box-shadow: photoshop-drop-shadow(
    90deg, 1px, 0, 1px, rgba( #000, 0.25 )
  );

  &:active {
    box-shadow: photoshop-inner-shadow(
      90deg, 1px, 0, 4px, rgba( #000, 0.35 )
    );
  }
}
.button {
  …
  box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.25);
}

.button:active {
  box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.35) inset;
}
text-shadow()
@function photoshop-text-shadow( $angle: 120deg, $distance: 0, $spread: 0, $size: 0, $color: #000 ) {

  @if $spread > 0 {
    @warn "spread has no effect for text shadows";
  }

  $shadow: photoshop-shadow( $angle, $distance, $spread, $size, $color );

  @return ( nth( $shadow, 1 ) nth( $shadow, 2 ) nth( $shadow, 3 ) nth( $shadow, 5 ) );
}
Adding text-shadow() to our button
.button {
  …
  text-shadow: photoshop-text-shadow(
    -90deg, 1px, 0, 0, rgba( #000, 0.4 )
  );

  &:active {
    text-shadow: photoshop-text-shadow(
      90deg, 1px, 0, 0, rgba( #000, 0.4 )
    );
  }
}
.button {
  …
  text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.4);
}

.button:active {
  text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.4);
}

Gradient Overlay to linear-gradient()

Linear Gradient CSS Syntax

[angle | keyword], color-stop (, color-stop)

linear-gradient( top, #000 0%, #fff 100% )

Photoshop Gradient Overlay

  • Opacity
  • Gradient
  • Angle
  • Scale

Angle

Photoshop:
0deg = East

CSS:
0deg = North

$css-angle: $angle - 90deg;
Scale
@function stop-scale( $stop, $scale ) {
    $stop: percentage-to-decimal( $stop );
    $scale: percentage-to-decimal( $scale );

    @return $scale * $stop - ( 0.5 * ( $scale - 1 ) );
}

//Examples
$stop1: stop-scale( 0%, 100% );   // 0%
$stop2: stop-scale( 50%, 100% );  // 50%
$stop3: stop-scale( 100%, 100% ); // 100%

$stop1: stop-scale( 0%, 150% );   // -25%
$stop2: stop-scale( 50%, 150% );  // 50%
$stop3: stop-scale( 100%, 150% ); // 125%

$stop1: stop-scale( 0%, 50% );    // 25%
$stop2: stop-scale( 50%, 50% );   // 50%
$stop3: stop-scale( 100%, 50% );  // 75%
Creating the CSS Syntax
…
$color-stops: ( #000 0%, #fff 100%);
$gradient-scale: 100%;
$css-color-stops: ();

@for $i from 1 through length( $color-stops ) {

  $color: nth( nth( $color-stops, $i ), 1 );
  $stop: stop-scale( nth( nth( $color-stops, $i ), 2 ), $gradient-scale );
  $color-stop: join( $color, $stop, space );

  $css-color-stops: append($color-stops, $color-stop, comma);

}

@return linear-gradient($css-direction, $css-color-stops);
Works for simple gradients
.simple-gradient {
  @include background-image(
    photoshop-gradient-overlay(
      100%, 90deg, 100%, ( #000 0% , #fff 100% )
    )
  );
}
.simple-gradient {
  background-image: linear-gradient(0deg, #000 0%, #FFF 100%);
  …
}
Or if your designer is on LSD
.complex-gradient {
  @include background-image(
    photoshop-gradient-overlay(
      75%, 0deg, 50%, ( red 0%, orange 20%, yellow 40%, green 60%, blue 80%, violet 100% )
    )
  );
}
.complex-gradient {
  background-image: linear-gradient(30deg, rgba(255, 0, 0, 0.75) 25%, rgba(255, 165, 0, 0.75) 35%, rgba(255, 255, 0, 0.75) 45%, rgba(0, 128, 0, 0.75) 55.0%, rgba(0, 0, 255, 0.75) 65%, rgba(238, 130, 238, 0.75) 75%);
  …
}
Adding linear-gradient() to our button
.button {
  …
  @include background-image(
    photoshop-gradient-overlay(
      50%, 90deg, 100%, ( #000 0% , #fff 100% )
    )
  );

  &:hover {
    @include background-image(
      photoshop-gradient-overlay(
        50%, 90deg, 100%, ( #000 0% , #fff 100% )
      )
    );
  }
}
.button {
  …
  linear-gradient(
    0deg,
    rgba(0, 0, 0, 0.5) 0%,
    rgba(255, 255, 255, 0.5) 100%
  );
}

.button:active {
  linear-gradient(
    -180deg,
    rgba(0, 0, 0, 0.5) 0%,
    rgba(255, 255, 255, 0.5) 100%
  );
}
Blend Modes
@function blend--normal($bg, $fg) {
  @return $fg;
}

@function blend--multiply($bg, $fg) {
  @return $fg * $bg / 255;
}

@function blend--overlay($bg, $fg) {
  @if ($bg < 128) {
    @return 2 * $fg * $bg / 255;
  } @else {
    @return 255 - 2 * (255 - $fg) * (255 - $bg) / 255);
  }
}

SASS Blend Mode Limitations

Must know foreground- and background-color

Update photoshop-gradient-overlay()
…
$background-color: #fa5400;

@for $i from 1 through length($color-stops) {

  $blended-color: photoshop-blend('overlay', $background-color, nth( nth( $color-stops, $i ), 1 ), $opacity);
  $stop: join( $blended-color, stop-scale( nth( nth( $color-stops, $i ), 2 ), $scale ), space );
  $css-color-stops: append($css-color-stops, $stop, comma);

}

@return linear-gradient($css-angle, $css-color-stops);

github.com/timhettler/compass-photoshop-gradient-overlay

.button {
  …
  linear-gradient(
    0deg, rgba(245, 0, 0, 0.5) 0%, rgba(255, 168, 0, 0.5) 100%
  );
}

.button:active {
  …
  linear-gradient(
    -180deg, rgba(245, 0, 0, 0.5) 0%, rgba(255, 168, 0, 0.5) 100%
  );
}

Part 3:
Optimization

Lots of redundancy
.button {
  @extend %button;
  @include background-image(photoshop-gradient-overlay(#fa5400, 'overlay', 50%, 90deg));
  @include yiq-contrasted(#fa5400);
  box-shadow: photoshop-drop-shadow(90deg, 1px, 0, 1px, rgba(#000, 0.25));
  text-shadow: photoshop-text-shadow(-90deg, 1px, 0, 0, rgba(yiq-contrast-color(#fa5400, #fff, #000), 0.4));
  &:hover {
    @if (yiq-contrast-color($background-color, #000, #fff) == #fff) {
      @include background-image(photoshop-gradient-overlay(lighten(#fa5400, 5), 'overlay', 50%));
      background-color: adjust-color(#fa5400, $lightness: 5);
    } @else {
      @include background-image(photoshop-gradient-overlay(darken(#fa5400, 5), 'overlay', 50%));
      background-color: adjust-color(#fa5400, $lightness: -5);
    }
  }
  &:active {
    @include background-image(photoshop-gradient-overlay(#fa5400, 'overlay', 50%, -90deg));
    box-shadow: photoshop-inner-shadow(90deg, 1px, 0, 4px, rgba(#000, 0.35));
    text-shadow: photoshop-text-shadow(90deg, 1px, 0, 0, rgba(yiq-contrast-color(#fa5400, #fff, #000), 0.4));
  }
}
Making our code modular
@mixin button($background-color) {
  $color-type: if(yiq-contrast-color($background-color, #000, #fff) == #000, "light", "dark");
  $shadow-color: yiq-contrast-color($background-color, #fff, #000);
  @extend %button;
  @include background-image(photoshop-gradient-overlay($background-color, 'overlay', 50%, 90deg));
  @include yiq-contrasted($background-color);
  box-shadow: photoshop-drop-shadow(90deg, 1px, 0, 1px, rgba(#000, 0.25));
  text-shadow: photoshop-text-shadow(-90deg, 1px, 0, 0, rgba($shadow-color, 0.4));
  &:hover {
    $color-adjust: if($color-type == "dark", 5, -5);
    $hover-color: adjust-color($background-color, $lightness: $color-adjust);
    @include background-image(photoshop-gradient-overlay($hover-color, 'overlay', 50%));
    background-color: $hover-color;
  }
  &:active {
    @include background-image(photoshop-gradient-overlay($background-color, 'overlay', 50%, -90deg));
    box-shadow: photoshop-inner-shadow(90deg, 1px, 0, 4px, rgba(#000, 0.35));
    text-shadow: photoshop-text-shadow(90deg, 1px, 0, 0, rgba($shadow-color, 0.4));
  }
}
We can style a button with 1 line of code
.button--orange {
  @include button(#fa5400);
}
So creating new buttons is trivial
.button--hotpink {
  @include button(hotpink);
}
Which is pretty #bada55
.button--bada55 {
  @include button(#bada55);
}
@timhettler

timhettler.github.io/sweating-small-stuff