Sweating the Small Stuff: Recreating Subtle Design Details Using Sass
http://timhettler.github.io/cssconf-2013/
http://timhettler.github.io/cssconf-2013/
@timhettler
Open Standards Developerat 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
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
%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;
}
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
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);
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
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 ) );
}
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);
}
linear-gradient()
Linear Gradient CSS Syntax
[angle | keyword], color-stop (, color-stop)
linear-gradient( top, #000 0%, #fff 100% )
Photoshop Gradient Overlay:
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
@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%
…
$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);
.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%
);
…
}
.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%
);
…
}
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%
);
}
@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
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%
);
}
.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));
}
}
@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));
}
}
.button-orange {
@include button(#fa5400);
}
.button-hotpink {
@include button(hotpink);
}
.button-bada55 {
@include button(#bada55);
}