You might not need a
CSS preprocessor

Serg Hospodarets
@malyw

<blink>No CSS</blink>

1996 year - CSS invented
Content and presentation are separated

.page-header {
  /* shorthand */
  border-bottom: 1px solid #eee;
}
					

p {
  font-family: Arial; /* font */
  font-size: 14px; /* font */
  color: #333; /* color */
  margin: 10px 0; /* layout */
}
					

.btn-outline {
  color: #563d7c; /* color */
  border-color: #563d7c; /* color */
}
					

CSS problems

  • Absence of variables
  • Absence of mixins
  • Modules
  • Nested rules are not supported
  • Code duplication as result

Preprocessors time

Solved by preprocessors

Sass:

Variables and Operators (+, -, *, /, %)


$font-size: 10px;
$font-family: Helvetica, sans-serif;

body {
  font: $font-size $font-family;
}

.mark{
  font-size: 1.5 * $font-size;
}
					

Mixins


@mixin clearfix {
  &:after {
    display: block;
    content: '';
    clear: both;
  }
}

.sidebar{
  @include clearfix;
}

.main{
  @include clearfix;
}
					

.sidebar:after {
  display: block;
  content: '';
  clear: both;
}

.main:after {
  display: block;
  content: '';
  clear: both;
}
					

Nesting

HTML




					

SCSS


// menu
.nav {
  > li {
    > a:hover {
      background-color: red;
    }
    // submenu
    > ul {
      background-color: #fff;
      > li > a:hover {
        background-color: black;
      }
    }
  }
}
					

Modules

What we use today

Preprocessors problems

  • Additional setup is needed to make a compiler work
  • Any change require recompilation
  • Compilation takes time ⏰
  • Each has own syntax! πŸ’©

// Sass
$color: #f00;
$images: "../img";
@mixin clearfix {
  &:after {
    content: " ";
    display: block;
    clear: both;
  }
}
body {
  color: $color;
  background: url("#{img}/1.png");
  @include clearfix;
}
					

// Less
@color: #f00;
@images: "../img";
.clearfix() {
  &:after {
    content: " ";
    display: block;
    clear: both;
  }
}
body {
  color: @color;
  background: url("@{img}/1.png");
  .clearfix;
}
					
  • Source maps are required
  • Debug might be hard (or buggy)
We want
  • Variables
  • Mixins
  • Nesting
  • Modules
  • Selector helpers, color functions

We don't want
  • Additional setup
  • Compilation
  • Not standardized syntax
  • Hard debug

Does CSS have all this today?

Let's take a look

What we want?

Variables

First CSS variable currentColor


:root { color: red; }
i {border: 1px solid currentColor;}
					

:root { color: red; }
i {border: 1px solid red;}
					

Usual text: Lorem ipsum with link1 and link2

Disclaimer: I created images 1.png and 2.png but they are not meant to represent any specific company


p {color: #333;}

p.disclaimer {color: red;}

/* Set links to reuse currentcolor
    for color and border-color */
p a {
  text-decoration: none;
  color: currentcolor;
  border-bottom:
    1px dashed currentcolor;
}
					

CSS custom properties
(a.k.a. CSS variables)

Good news!

Syntax


/* declaration */
--VAR_NAME: <declaration-value>;
/* usage */
var(--VAR_NAME)
					



/* root element selector (global scope), e.g. <html> */
:root {
  /* CSS variables declarations */
  --main-color: #ff00ff;
  --main-bg: rgb(200, 255, 255);
}

body {
  /* use the variable */
  color: var(--main-color);
}
					

Didn't expect the "--"? There was a reason.

Variable examples


:root{
    --main-color: #4d4e53;
    --main-bg: rgb(255, 255, 255);
    --logo-border-color: rebeccapurple;

    --header-height: 68px;
    --content-padding: 10px 20px;

    --base-line-height: 1.428571429;
    --transition-duration: .35s;
    --external-link: "external link";
    --margin-top: calc(2vh + 20px);
}
					

And even..


:root{
    --foo: if(x > 5) this.width = 10;
}
					

Variable Defaults

If the variable has already been assigned to
- it won’t be re-assigned


p {
 --p-margin: 5px;
 margin: var(--p-margin, 0 0 10px); /* 5px */
}
					

If it doesn’t have a value yet
- it will be given one.


p {
 margin: var(--p-margin, 0 0 10px);/* 0 0 10px */
}
					

Reassign vars from others


.block {
  --block-text: 'This is my block';
  --block-highlight-text: var(--block-text)' with highlight';
}

.block__highlight:before {
  content: var(--block-highlight-text);
    /*This is my block with highlight*/
}
					

Reset/inherit values

As for any other CSS property, you can apply "initial" and "inherit" values


.with-reset {
  --bgcolor: initial;/* RESETS THE VALUE for the scope */

  --color: green;/* CHANGES THE VALUE */

  --border: inherit;/* INHERITS THE VALUE for the scope */
}
					
Usage example: emulating non existing CSS rule

Scopes


My block is
awesome

Follow usual CSS cascade rules:


:root{
    --global-var: 1em;
    /* --global-var is available globally */
}
.block {
    --block-var: 1.5em;
    /* --global-var and --block-var are available */
}

.block__highlight {
    --block-highlight-var: 2rem;
    /* --global-var , --block-var and --block-highlight-var */

    font-size: var(--block-highlight-font-size);
}
					
CSS / preprocessors scopes are different

/* SCSS: scope depends on
 the selectors
 structure in the code
 "{}" define scope */
$font-size: 20px;

.block{
  $font-size: 42px;
  /* font-size: $font-size; */
}

.block__highlight{
  font-size: $font-size;
}
					

/* CSS: scope depends
 on the current HTML DOM structure
*/
:root{
  --font-size: 20px;
}

.block{
  --font-size: 42px;
}

.block__highlight{
  font-size: var(--font-size);
}
					
Scope examples

/* Global scope (usually <html/>) */
:root {
    --bg: #f00;
}

/* var is reassigned when media query is applied */
@media screen and (min-width: 800px) {
  :root { --bg: #f00; }
}

/* and on body hover is reassigned for <body/> */
/* (but will be the same for <html/> as it's a parent scope) */
body:hover {
  --bg: #ff0;
}
					

Variables are aliveπŸ–₯


:root {
    --screen-category: 'desktop';
}

@media screen and (max-width: 1024px) {
    :root { --screen-category: 'tablet'; }
}

@media screen and (max-width: 640px) {
    :root { --screen-category: 'phone'; }
}

body:after {
    content: '--screen-category : 'var(--screen-category);
}
					

Demo

Operators and calculations


:root {
  --block-font-size: 1rem;
}

.block__highlight {
  /* DOESN'T WORK */
  font-size: var(--block-font-size)*1.5;
}
					

CSS   calc(🀘)   to the rescue (for values)!


:root {
  --block-font-size: 1rem;
}

.block__highlight {
  /* WORKS */
  font-size: calc(var(--block-font-size)*1.5);
}
					

Demo πŸ–₯

Generate colors from CSS custom properties (R,G,B)

CSS to JS: without

To pass variables from CSS to JS we used to use workarounds or hacks to write JSON in CSS


.breakpoints-data {
    font-family: '{"phone":"480px","tablet":"800px"}';
}
					

CSS to JS: with


.breakpoints-data {
  --phone: 480px;
  --tablet: 800px;
}
					

JS


const breakpointsData = document.querySelector('.breakpoints-data');

// GET
const phone = getComputedStyle(breakpointsData)
    .getPropertyValue('--phone');

// SET
breakpointsData.style
    .setProperty('--phone', 'custom');
					

Demo πŸ–₯

Rotate page elements using CSS variables

Check if supported

CSS

@supports ( (--a: 0)) {
    /* supported */
}

@supports ( not (--a: 0)) {
    /* not supported */
}
					
JS

const isSupported = window.CSS && window.CSS.supports &&
    window.CSS.supports('--a', 0);

/* e.g. load a CSS file generated by a preprocessor */
if(!isSupported){
    removeCss('css-custom-properties.css')
    loadCss('without-css-custom-properties.css');
}
					

Why else CSS Custom Props are better?

Demo πŸ–₯

Color schema switcher based on CSS custom property values


Cannot be done by a preprocessor without generating additional code

What do we want?

Mixins

CSS Custom Sets of Properties and @apply rule
(a.k.a. CSS mixins)

Custom Property:


:root{
    /* --property: value; */

    --VAR: <declaration-value>;
}
					

Custom properties can hold more than just values- they can also be used to hold sets of declarations:


:root{
    /*
        --property: {
            property1: value1;
            property...: value...;
        }
    */

    --MIXIN: {
        /* style declaration 1 */
        /* style declaration ... */
    };
}
					

Syntax


:root {
    --pink-schema: {
        color: #6A8759;
        background-color: #F64778;
    }
}

body{
  @apply --pink-schema;
}
					

@apply rule takes these sets of declarations and inlines them in another style rule

Reasons to use

  • Put reusable bunches of styles into separate entities
  • Avoid code duplication
  • Apply changes in a central place

Behavior

Everything from CSS variables (scopes, usage from JS etc.) is applicable for Custom Sets of Properties

Examples


:root {
  --clearfix: {
    display: table;
    clear: both;
    content: '';
  };
}

.clearfix:after{
  @apply --clearfix;
}
					

:root {
  --overflow-ellipsis: {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  };
}

.overflow-box{
  @apply --overflow-ellipsis;
}
					

Demo πŸ–₯

CSS Triangle Mixin

CSS Triangle Mixin



:root {
  --triangle-to-bottom-size: 50px;
  --triangle-to-bottom-color: #007bff;

  --triangle-to-bottom: {
    width: 0;
    height: 0;
    border-style: solid;
    border-width: var(--triangle-to-bottom-size)
                    var(--triangle-to-bottom-size)
                    0 var(--triangle-to-bottom-size);
    border-color: var(--triangle-to-bottom-color)
                    transparent transparent transparent;
  };
}
					

.triangle-to-bottom {
  @apply --triangle-to-bottom;
}
					

Problem- variables cannot be passed


:root {
    --triangle-to-bottom-size: 50px;

    --triangle-to-bottom: {
        /* STYLES */
        border-bottom-width: var(--triangle-to-bottom-size);
    };
}

.triangle-to-bottom {
    --triangle-to-bottom-size: 8px;

    @apply --triangle-to-bottom;
    /*  but still 50px size is applied for border-bottom-width */
}
					

Discussion to change this behavior is in progress

Mixins for CSS vendor prefixes?

Lea Verou's idea:


* {/* has zero specificity */
  /* prevents the property inheritance from outer scopes */
  --clip-path: initial;

  -webkit-clip-path: var(--clip-path);
  clip-path: var(--clip-path);
}
					

Every time you need clip-path:


header {/* any selector like this overrides the "*" */
  /* assign the prop value for the scope */
  --clip-path: polygon(0% 0%, 100% 0%, 100% 100%%, 0% 100%);
}
					

What we need?

Nesting

What we want to avoid?

  • CSS code duplication in selectors
  • Bad readability of the code

table.colortable td {
  text-align:center;
}
table.colortable td.upper {
  text-transform:uppercase;
}
table.colortable td:first-child,
table.colortable td:first-child+td {
  border:1px solid #000;
}
					

Tab Atkins' CSS Nesting spec proposal

Syntax is close to preprocessors


/* Dropdown menu on hover */
ul {
  /* direct nesting (& MUST be the first part of selector)*/
  & > li {
    color: #000;

    & > ul { display: none; }

    &:hover {
      color: #f00;

      & > ul { display: block; }
    }
  }
}

The Nesting At-Rule: '@nest' for complex cases

@nest < selector (MUST CONTAIN a nesting selector '&') >


.foo {
  color: black;


  @nest body.loading & {
    opacity: 0.5;
  }

  @nest :not(&) {
    color: white;
  }
}
					

.foo {
  color: black;
}

body.loading .foo {
  opacity: 0.5;
}

:not(.foo) {
  color: white;

}
					

Migration is easy

  • Nesting selector syntax is very close to Sass
  • Complex nesting can be done using @nest at-rule
  • media expression etc. are nested:

a {
    @media (min-width: 30em) {
        color: yellow;
    }
}
					

@media (min-width: 30em) {
    a {
        color: yellow
    }
}
					

What we wish?

Modules

The @import CSS at-rule is used to import style rules from other style sheets.

It is available in all browsers since IE 5.5!

  • Why we didn't use this before?

Bugs in old browsers with the order of inclusion

Before requests didn't go in parallel

For HTTP1x good practice is file concatenation.
With coming of HTTP/2 rules will be cnanged.

Advantages for free:

Conditional loading


/* Formal syntax */
@import [ <string> | <url> ] [<media-query-list>]?;
					
You can easily apply media queries for different stylesheets.

@import url("print.css") print;
@import "mobile.css" (max-width: 728px);
					

The linked resources are loaded only when condition is met.

We have

  • Variables, Mixins
  • Nesting and Modules

What else we want?

  • Selector helpers for complex cases
  • Color functions

:matches pseudo-class


                /* SYNTAX */
:matches( selector[, selector]* )
					

A functional pseudo-class taking a selector list as its argument.


.nav:matches(.side,.top) .links:matches(:hover, :focus) {
  color: #BADA55;
}

/* Same thing as this... */
.nav.side .links:hover,
.nav.top  .links:hover,
.nav.side .links:focus,
.nav.top  .links:focus {
  color: #BADA55;
}
					

@custom-selector


                /* SYNTAX */
@custom-selector: <custom-selector> <selector-list>;
					

Example:


@custom-selector :--text-inputs input[type="text"],
input[type="password"];

:--text-inputs.disabled,
:--text-inputs[disabled] {
    opacity: 0.5
}
					

Same as:


input[type="text"].disabled,
input[type="password"].disabled,
input[type="text"][disabled],
input[type="password"][disabled] {
    opacity: 0.5
}
					

Color functions


/* SYNTAX */
color( <color> <color-adjuster>* )
					
  • some adjusters have shortcuts
  • adjusters can be pipped

color(
  red             /* from red */
  blackness(+25%) /* to 25% more black than red */
  blackness(+25%) /* to 50% more black than red */
  blackness(-50%) /* to red again */
  hue(+ 30deg)    /* to orange */
  hue(- 30deg)    /* to red again */
);
					

Better media queries!

Problem

  • The syntax is too long and they cannot be reused easily

Need

  • A simple way to set breakpoints and reuse them

Custom media queries (MQ Level 5)


/* SYNTAX */
@custom-media --NAME <media-query-list>;
					

How to use:


@custom-media --tablet (min-width: 800px) and (max-width: 1024px);

@media (--tablet){
    .custom-media-queries{
        background-color: red;
    }
}
					

Media queries ranges (MQ Level 4)

Instead of:

@media (min-width: 800px) and (max-width: 1024px) {
    .media-queries-range{
        background-color: red;
    }
}
					
Use:

@media (width >= 800px) and (width <= 1024px) {
    .media-queries-range{ background-color: red; }
}
					

With custom media queries:


@custom-media --tablet (width >= 800px) and (width <= 1024px);

@media (--tablet){
    /* STYLES */
}
					

Can we Use it now?

Current situation

  • CSS variables are supported in all the modern browsers except EDGE
  • CSS mixins work in Chrome Canary
  • Some of other specs are integrated in various browsers, sometimes it's in beta/dev versions

Should we wait for another couple years?

  • PostCSS based
  • Includes other PostCSS plugins (vars, mixins etc.)
  • If you already use Autoprefixer- change is straign forward

Build setup:


//...
postcss: {
  options: {
    processors: [
      require('autoprefixer')({
       browsers: ['last 2 versions']
      })
    ]
  },
//...
					

//...
postcss: {
  options: {
    processors: [
      require('postcss-cssnext')({
       browsers: ['last 2 versions']
      })
    ]
  },
//...
					

How to migrate

1) Preprocessor πŸ€–

  • Add PostCSS and cssnext

2) Preprocessor+PostCSS+ability to use new CSS features πŸ™‚

  • Change vars, mixins, media queries, colors and selectors to the CSS ones
  • Disable a preprocessor

3) PostCSS +all the CSS additions power (with limitations) πŸ˜€

  • Disable cssnext and PostCss when the browsers support everything

4) You have pure CSS with everything 😎

You still can combine preprocessor and all the mentioned CSS additions (or process them via PostCSS) to use the strongest parts of the both.

Any live sites which use all these new CSS features?

blog.hospodarets.com

Conclusions

  • Today we have plain mostly everything we need from preprocessors in pure CSS
    • variables, mixins
    • modules, nesting
    • complex selectors and color functions
  • We have polyfills to make it work until it's supported in all browsers
  • We have the real examples of migrated applications

How can I help / stay tuned?

Spec, drafts, proposals


Other

Thank you!

@malyw Avatar