Article
Creating CSS Frameworks
A small Sass tool to spinning up Atomic CSS quickly: grids, breakpoints, flexbox helpers, gradients, and atomic utility classes.
Dec 2015 • 1718 words
I published a small gist today. It has been sitting around in some form for about a year, copied between projects, renamed, trimmed, broken, repaired, and slowly turned into a tool I use encode design principles and get projects up and running quickly. It's a little & lightweight starting point for layout, spacing, flexbox, gradients, and utility classes for most UI work.
Most projects have previously had some gigantic library like bootstrap, which meant spending time unpicking hyper-specific CSS. Even when using typeahead, I often find myself wishing I had just built my own version. So, over time I've started from scratch, and gradually rebuilt the same helpers over and over again. Grids. Breakpoints. A few display helpers. A color system. The reset. Checking caniuse for browser fallbacks. Deciding on way to spit out classes without writing the same thing over and over again. It's busywork, and I don't like doing work for the sake of it.
I've worked towards a hyper-atomic structure because 1: it mirrors (good! structured!) interface design principles, and because small predictable classes are useful when you are trying to iterate quickly. OOCSS always made me mildly uncomfortable and while there is a lot of sense in it, I have never loved the repetition and tight-coupling to semantic structure. It's like each class group becomes a graveyard of shortcuts and overrides that people are too worried to modify. Suffice to say, naming is expensive, and I would rather have .col-6, .u768-col-12 as part of a generated set of utilities, than have to decide what the object is supposed to be called in CSS, and the smartest way to establish levels of specificity. It's insanely wasteful and invites debate that nobody really needs.
This obviously starts to look like shorthand inline styles, which we all hate, and I do not think pretending otherwise is very useful. A class like .mt-20 or .col-6 is functionally identical writing a value directly into the element because it is still a styling decision living in the markup. But by defining levels to things (again, going back to encoding design structure in CSS), it's easier to iterate over. For example, .bg-primary .bg-secondary, .text-l, .text-ll, .text-sss all give a fast way to alter a type system if needed. At least the class name is recognisable enough that I don't have to jump between that & CSS to figure out where colour, size and everything else is declared. I can just look at the element and know what is going on.
Coupling markup with style has been treated as a faux-pas for as long as I have been writing HTML and CSS, and I get why, but that approach coupled markup with content. And guess what? I don't think we should do that, especially with easy to use build pipelines and frameworks. Now we can build sets of generalised structures that can be reused helps to mirror design decisions while allowing content & purpose to change.
Anyway, that is why the grid is plain, with configurable columns, gutters, max-width, and breakpoints. Nothing clever. Breakpoint names are ugly but useful. .u768-col-6 tells me exactly what it does. The Sass loops do the boring work, which is exactly where Sass feels most useful to me. I've seen lots of elaborate abstraction and crazy nesting rules, but things like that quickly become brittle. We should only really care about touching CSS for the core, atomic principles, so we need to write barely any CSS anywhere else. Flexbox mixins follow the same principle, with the common cases covered like direction, wrap, order, grow, shrink, basis, justify, align. There is more, and hopefully it's easy to understand.
I've toyed with the idea of packaging this up, but my worry is that it might easily become a dumping ground in an attempt to capture every use case. I don't want to create helpers for every process or workflow, because the the thing that was supposed to make things faster could easily become another framework with too many opinions that, again, need to be unpicked. I don't want that. The point is to give myself a compact base that lets me start codify UI principles. So that is all this is: a little CSS machine to encode design principles. A grid, some responsive helpers, a few mixins, and a heavy opinion that CSS should be utility-driven. Enjoy :)
$screen-size-names: u1240, u1030, u768, u481;
$screen-sizes: 1240px, 1030px, 768px, 481px;
$grid-max-width: 1192px;
$grid-columns: 12;
$grid-gutter-width: 2%;
$grid-column-width: (100% / ($grid-columns));
*, *:before, *:after {
box-sizing: border-box;
}
.row {
display: block;
margin: 0 auto;
max-width: $grid-max-width;
}
[class*="col-"] {
float: left;
margin: 0 ($grid-gutter-width / 2);
position: relative;
}
@for $i from 1 through $grid-columns {
.col-#{$i} {
width: ($i * $grid-column-width) - $grid-gutter-width;
}
}
@for $i from 1 through length($screen-size-names) {
@media screen and (max-width: (nth($screen-sizes, $i))) {
.#{nth($screen-size-names, $i)} {
@for $i from 1 through $grid-columns {
&-col-#{$i} {
width: ($i * $grid-column-width) - $grid-gutter-width;
}
}
}
}
}
// Mixins for:
// • Iterating classes
// • Flexbox, including browser prefixes
// • Gradients, including browser prefixes
// Created by Carl Fairclough
// carlfairclough.me
@mixin iterate-classes($name, $property, $values, $unit: null) {
@each $value in $values {
$val: $value / 10;
.#{$name}-#{$val * 10} {
#{$property}: $value + $unit;
}
}
}
@mixin flexbox {
display: -webkit-box;
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
}
%flexbox {
@include flexbox;
}
@mixin inline-flex {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
}
%inline-flex {
@include inline-flex;
}
// Flexbox Direction
// Values: row | row-reverse | column | column-reverse
// Default: row
@mixin flex-direction($value: row) {
@if $value == row-reverse {
-webkit-box-direction: reverse;
-webkit-box-orient: horizontal;
} @else if $value == column {
-webkit-box-direction: normal;
-webkit-box-orient: vertical;
} @else if $value == column-reverse {
-webkit-box-direction: reverse;
-webkit-box-orient: vertical;
} @else {
-webkit-box-direction: normal;
-webkit-box-orient: horizontal;
}
-webkit-flex-direction: $value;
-moz-flex-direction: $value;
-ms-flex-direction: $value;
flex-direction: $value;
}
// Shorter version:
@mixin flex-dir($args...) {
@include flex-direction($args...);
}
// Flexbox Wrap
// Values: nowrap | wrap | wrap-reverse
// Default: nowrap
@mixin flex-wrap($value: nowrap) {
// No Webkit Box fallback.
-webkit-flex-wrap: $value;
-moz-flex-wrap: $value;
@if $value == nowrap {
-ms-flex-wrap: none;
} @else {
-ms-flex-wrap: $value;
}
flex-wrap: $value;
}
// Flexbox Flow shorthand
// Values: <flex-direction> | <flex-wrap>
// Default: row nowrap
@mixin flex-flow($values: (row nowrap)) {
// No Webkit Box fallback.
-webkit-flex-flow: $values;
-moz-flex-flow: $values;
-ms-flex-flow: $values;
flex-flow: $values;
}
// Flexbox Order
// Default: 0
@mixin order($int: 0) {
-webkit-box-ordinal-group: $int + 1;
-webkit-order: $int;
-moz-order: $int;
-ms-flex-order: $int;
order: $int;
}
// Flexbox Grow
// Default: 0
@mixin flex-grow($int: 0) {
-webkit-box-flex: $int;
-webkit-flex-grow: $int;
-moz-flex-grow: $int;
-ms-flex-positive: $int;
flex-grow: $int;
}
// Flexbox Shrink
// Default: 1
@mixin flex-shrink($int: 1) {
-webkit-flex-shrink: $int;
-moz-flex-shrink: $int;
-ms-flex-negative: $int;
flex-shrink: $int;
}
// Flexbox Basis
// Values: Like width
// Default: auto
@mixin flex-basis($value: auto) {
-webkit-flex-basis: $value;
-moz-flex-basis: $value;
-ms-flex-preferred-size: $value;
flex-basis: $value;
}
// Flexbox Flex shorthand
// Values: none | <flex-grow> <flex-shrink> || <flex-basis>
// Default: See individual properties, usually 1 1 0.
@mixin flex($fg: 1, $fs: null, $fb: null) {
// Set a variable to be used by box-flex properties.
$fg-boxflex: $fg;
// Box-Flex only supports a flex-grow value, so grab the
// first item in the list and just return that.
@if type-of($fg) == "list" {
$fg-boxflex: nth($fg, 1);
}
-webkit-box-flex: $fg-boxflex;
-webkit-flex: $fg $fs $fb;
-moz-box-flex: $fg-boxflex;
-moz-flex: $fg $fs $fb;
-ms-flex: $fg $fs $fb;
flex: $fg $fs $fb;
}
// Flexbox Justify Content
// Values: flex-start | flex-end | center | space-between | space-around
// Default: flex-start
@mixin justify-content($value: flex-start) {
@if $value == flex-start {
-webkit-box-pack: start;
-ms-flex-pack: start;
} @else if $value == flex-end {
-webkit-box-pack: end;
-ms-flex-pack: end;
} @else if $value == space-between {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
} @else if $value == space-around {
-ms-flex-pack: distribute;
} @else {
-webkit-box-pack: $value;
-ms-flex-pack: $value;
}
-webkit-justify-content: $value;
-moz-justify-content: $value;
justify-content: $value;
}
// Shorter version:
@mixin flex-just($args...) {
@include justify-content($args...);
}
// Flexbox Align Items
// Values: flex-start | flex-end | center | baseline | stretch
// Default: stretch
@mixin align-items($value: stretch) {
@if $value == flex-start {
-webkit-box-align: start;
-ms-flex-align: start;
} @else if $value == flex-end {
-webkit-box-align: end;
-ms-flex-align: end;
} @else {
-webkit-box-align: $value;
-ms-flex-align: $value;
}
-webkit-align-items: $value;
-moz-align-items: $value;
align-items: $value;
}
// Flexbox Align Self
// Values: auto | flex-start | flex-end | center | baseline | stretch
// Default: auto
@mixin align-self($value: auto) {
// No Webkit Box Fallback.
-webkit-align-self: $value;
-moz-align-self: $value;
@if $value == flex-start {
-ms-flex-item-align: start;
} @else if $value == flex-end {
-ms-flex-item-align: end;
} @else {
-ms-flex-item-align: $value;
}
align-self: $value;
}
// Flexbox Align Content
// Values: flex-start | flex-end | center | space-between | space-around | stretch
// Default: stretch
@mixin align-content($value: stretch) {
// No Webkit Box Fallback.
-webkit-align-content: $value;
-moz-align-content: $value;
@if $value == flex-start {
-ms-flex-line-pack: start;
} @else if $value == flex-end {
-ms-flex-line-pack: end;
} @else {
-ms-flex-line-pack: $value;
}
align-content: $value;
}
// GRADIENTS
@mixin linear-gradient($direction, $color-stops...) {
// Direction has been omitted and happens to be a color-stop.
@if is-direction($direction) == false {
$color-stops: $direction, $color-stops;
$direction: 180deg;
}
background: nth(nth($color-stops, 1), 1);
background: -webkit-linear-gradient(legacy-direction($direction), $color-stops);
background: linear-gradient($direction, $color-stops);
}