Sweating the Small Stuff: Recreating Subtle Design Details Using Sass

http://timhettler.github.io/cssconf-2013/

@timhettler

Open Standards Developer
at R/GA in NYC

I'm going to talk about this button
for the next 20 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

Preprocessors are the rosetta stone between designers & developers

Let's rebuild our button using
Sass + Compass

Building a base

Button Prototype Placeholder
%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;
}
Determining Text Color
Dark Buttons Light Buttons

Built-in Sass functions
to compare color lightness

@function get-button-color( $color ) {

  @return if( lightness( $color ) > 50, #333, #fff );

}

Text-color gets set programatically:

.dark-button {
  @extend %button;
  background-color: #333;
  color: get-button-color(#333);
}

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

.light-button {
  …
  background-color: #ffd204;
  color: #333;
}

Sass allows us to
abstract design logic

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:

  • Color
  • Opacity
  • Gradient
  • Angle
  • Scale

Angle

Vendor-prefixed:
0deg = East

Un-prefixed:
0deg = North

@function convert-angle($angle) {
    @if $angle == 0 {
        @return left;
    } @else if $angle == 45 {
        @return left bottom;
    } @else if $angle == 90 {
        @return bottom;
    } @else if $angle == 135 {
        @return right bottom;
    …
    } @else {
        @return $angle - 90deg;
    }
}

$css-direction: convert-angle( 90deg );  // bottom
$css-direction: convert-angle( 140deg ); // 50deg
Scale
@function stop-scale( $stop, $scale ) {
    $stop: percentage-to-decimal( $stop );
    $scale: percentage-to-decimal( $scale );

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

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

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

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

@for $i from 1 through length( $gradient-colors ) {

  $stop: join( nth( $gradient-colors, $i ), stop-scale( nth( $gradient-stops, $i ), $scale ), space );
  $color-stops: append($color-stops, $stop, comma);

}

@return linear-gradient($css-direction, $color-stops);
Works for simple gradients:
.simple-gradient {
  @include background-image(
    photoshop-gradient-overlay(
      100%, 90deg, 100%, ( #000, #fff ), ( 0%, 100% )
    )
  );
}
.simple-gradient {
  background-image: -webkit-linear-gradient(
    bottom, #000000 0%, #ffffff 100%
  );
  …
}
Or if your designer is on LSD:
.complex-gradient {
  @include background-image(
    photoshop-gradient-overlay(
      75%, 0deg, 50%, ( red, orange, yellow, green, blue, violet ), ( 0%, 20%, 40%, 60%, 80%, 100% )
    )
  );
}
.complex-gradient {
  background-image: -webkit-linear-gradient(
    left, 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, #fff ), ( 0%, 100% )
    )
  );

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

.button:active {
  -webkit-linear-gradient(
    top,
    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( $gradient-colors ) {

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

}

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

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

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

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

Optimizing Our Code

Lots of redundancy
.button {
  @extend %button;
  @include background-image(photoshop-gradient-overlay(#fa5400, 'overlay', 50%, 90deg));
  background-color: #fa5400;
  box-shadow: photoshop-drop-shadow(90deg, 1px, 0, 1px, rgba(#000, 0.25));
  color: get-button-color(#fa5400);
  text-shadow: photoshop-text-shadow(-90deg, 1px, 0, 0, rgba(get-shadow-color(#fa5400), 0.4));

  &: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(get-shadow-color(#fa5400), 0.4));
  }
}
Making our code modular
@mixin button($bg-color) {
  $text-color: get-button-color($bg-color);
  $shadow-color: get-shadow-color($bg-color);
  @extend %button;
  @include background-image(photoshop-gradient-overlay($bg-color, 'overlay', 50%, 90deg));
  background-color: $bg-color;
  box-shadow: photoshop-drop-shadow(90deg, 1px, 0, 1px, rgba(#000, 0.25));
  color: $text-color;
  text-shadow: photoshop-text-shadow(-90deg, 1px, 0, 0, rgba($shadow-color, 0.4));

  &:active {
    @include background-image(photoshop-gradient-overlay($bg-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