Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Modular CSS — Agent Conf '17 Edition

Modular CSS — Agent Conf '17 Edition

Slides from my talk at Agent Conf 2017

Andrey Okonetchnikov

January 21, 2017
Tweet

More Decks by Andrey Okonetchnikov

Other Decks in Programming

Transcript

  1. Modular CSS
    with @okonetchnikov

    View full-size slide

  2. Andrey Okonetchnikov
    @okonetchnikov

    View full-size slide

  3. ColorSnapper
    http://colorsnapper.com

    View full-size slide

  4. kaffemik
    Zollergasse 5,
    1070 Wien

    View full-size slide

  5. I ❤ Open Source
    github.com/okonet/react-dropzone
    github.com/okonet/lint-staged
    and many more…

    View full-size slide

  6. I design & develop
    User Interfaces

    View full-size slide

  7. Building scalable
    User Interfaces

    View full-size slide

  8. – http://www.csszengarden.com/
    “CSS allows complete and total control over the
    style of a hypertext document…”

    View full-size slide

  9. CSS was designed for documents,
    not for web-applications

    View full-size slide

  10. User Interface ∋ Components

    View full-size slide

  11. Libraries & Frameworks
    based on components

    View full-size slide

  12. What are
    UI Components?

    View full-size slide

  13. List component
    Button
    Button

    View full-size slide

  14. List Item Component
    Button

    View full-size slide

  15. Atomic components
    Button

    View full-size slide

  16. Let’s build a component!

    View full-size slide

  17. In React.js everything is a component!

    View full-size slide

  18. Button component
    Default
    Active
    Disabled

    View full-size slide

  19. Button.js
    1 const Button = ({ children }) => {
    2 return (
    3 4 type="button"
    5 >
    6 { children }
    7
    8 )
    9 }

    View full-size slide

  20. Button.js
    1 const Button = ({ children }) => {
    2 return (
    3 4 type="button"
    5 className="btn"
    6 >
    7
    8 { children }
    9
    10
    11 )
    12 }

    View full-size slide

  21. Button.js
    1 const Button = ({ children, disabled }) => {
    2 return (
    3 4 type="button"
    5 disabled={disabled}
    6 className="btn"
    7 >
    8
    9 { children }
    10
    11
    12 )
    13 }

    View full-size slide

  22. Button.js
    1 const Button = ({ children, disabled }) => {
    2 let classNames = 'btn'
    3 if (disabled) classNames += ' disabled'
    4 return (
    5 6 type="button"
    7 disabled={disabled}
    8 className={classNames}
    9 >
    10
    11 { children }
    12
    13
    14 )
    15 }

    View full-size slide

  23. Button.js
    1 const Button = ({ children, disabled, active }) => {
    2 let classNames = 'btn'
    3 if (disabled) classNames += ' disabled'
    4 if (active) classNames += ' active’
    5 return (
    6 7 type="button"
    8 disabled={disabled}
    9 className={classNames}
    10 >
    11
    12 { children }
    13
    14
    15 )
    16 }

    View full-size slide

  24. Let’s add some CSS
    1 .btn {
    2 /* styles for button */
    3 }
    4
    5 .active {
    6 /* styles for active button */
    7 background-color: blue;
    8 }
    9
    10 .label {
    11 /* styles for button label */
    12 }

    View full-size slide

  25. Button component
    Default
    Active
    Disabled
    Default
    Disabled
    Active

    View full-size slide

  26. Button component
    Active


    Click me!


    Active

    View full-size slide

  27. Star component


    View full-size slide

  28. Star.js
    1 const Star = ({ active }) => {
    2 let classNames = 'star'
    3 if (active) classNames += ' active'
    4 return (
    5
    6 )
    7 }

    View full-size slide

  29. Let’s add some CSS
    1 .star {
    2 /* styles for star */
    3 }
    4
    5 .active {
    6 /* styles for active star */
    7 background-color: orange;
    8 }
    9

    View full-size slide

  30. Star component



    View full-size slide

  31. f(props, state) => UI

    View full-size slide

  32. UI components should be
    pure, modular & isolated

    View full-size slide

  33. • Props
    • State
    • Styles
    Button

    View full-size slide

  34. • Props
    • State
    • Styles
    Button
    • Props
    • State
    • Styles
    • Props
    • State
    • Styles

    View full-size slide

  35. • Props
    • State
    • Styles
    Button
    • Props
    • State
    • Styles
    • Props
    • State
    • Styles

    View full-size slide

  36. • Props
    • State
    • Styles
    Button
    • Props
    • State
    • Styles
    • Props
    • State
    • Styles
    Global styles

    View full-size slide

  37. Global CSS
    1 .btn {
    2 /* styles for button */
    3 }
    4
    5 .active {
    6 /* styles for active button */
    7 background-color: blue;
    8 }
    9
    10 .label {
    11 /* styles for button label */
    12 }
    1 .star {
    2 /* styles for star */
    3 }
    4
    5 .active {
    6 /* styles for active star */
    7 background-color: orange;
    8 }
    9

    View full-size slide

  38. .content .albums .album .btn

    View full-size slide

  39. http://www.standardista.com/css3/css-specificity/

    View full-size slide

  40. Debugging CSS

    View full-size slide

  41. Where are my styles?

    View full-size slide

  42. Where is my CSS?
    1 const Button = ({ children }) => {
    2 return (
    3 4 type="button"
    5 className="btn"
    6 >
    7
    8 { children }
    9
    10
    11 )
    12 }

    View full-size slide

  43. Where is my CSS?
    Where is this class coming from?
    1 const Button = ({ children }) => {
    2 return (
    3 4 type="button"
    5 className="btn"
    6 >
    7
    8 { children }
    9
    10
    11 )
    12 }

    View full-size slide

  44. Let’s search
    quickly…

    View full-size slide

  45. CSS Monolith!

    View full-size slide

  46. Refactoring
    application.css

    View full-size slide

  47. Problems with CSS in Web Applications
    Disconnected from component’s code
    Implicit dependencies
    No styles isolation
    No composition & code sharing

    View full-size slide

  48. 1. Split your code

    View full-size slide

  49. Components > Files
    public
    ├── images
    │ └── icon.svg
    ├── javascripts
    │ ├── Button.js
    │ └── Dropdown.js
    └── stylesheets
    ├── Buttons.css
    └── Dropdown.css
    public
    ├── images
    │ └── icon.svg
    ├── javascripts
    │ └── application.js
    └── stylesheets
    └── application.css

    View full-size slide

  50. 1. Split & co-locate

    View full-size slide

  51. Separation of
    concerns?
    public
    ├── images
    │ └── icon.svg
    ├── javascripts
    │ ├── Button.js
    │ └── Dropdown.js
    └── stylesheets
    ├── Buttons.css
    └── Dropdown.css

    View full-size slide

  52. Separation of
    concerns technologies
    public
    ├── images
    │ └── icon.svg
    ├── javascripts
    │ ├── Button.js
    │ └── Dropdown.js
    └── stylesheets
    ├── Buttons.css
    └── Dropdown.css

    View full-size slide

  53. Separation of
    concerns
    components
    ├── Button
    │ ├── Button.css
    │ ├── Button.js
    │ ├── Button.spec.js
    │ ├── icon.svg
    │ └── index.js
    └── Dropdown
    public
    ├── images
    │ └── icon.svg
    ├── javascripts
    │ ├── Button.js
    │ └── Dropdown.js
    └── stylesheets
    ├── Buttons.css
    └── Dropdown.css

    View full-size slide

  54. 2. Dependencies

    View full-size slide

  55. CSS pre-processors?

    View full-size slide

  56. Dependencies in Sass
    1 /* Global styles */
    2 @import 'reset.css';
    3 @import 'typography.css';
    4 @import 'grid.css';
    5
    6 /* Components */
    7 @import 'button.css';

    View full-size slide

  57. Where is my CSS?
    Where is this class coming from?
    1 const Button = ({ children }) => {
    2 return (
    3 4 type="button"
    5 className="btn"
    6 >
    7
    8 { children }
    9
    10
    11 )
    12 }

    View full-size slide

  58. Explicit dependencies FTW!
    1 import './Button.css'
    2
    3 const Button = ({ children, disabled, active }) => {
    4 let classNames = 'btn'
    5 if (disabled) classNames += ' disabled'
    6 if (active) classNames += ' active'
    7 return (
    8 9 type="button"
    10 disabled={disabled}
    11 className={classNames}
    12 >
    13
    14 { children }
    15
    16
    17 )
    18 }
    19 export default Button

    View full-size slide

  59. 1. We can’t require CSS in JS

    View full-size slide

  60. 2. Browsers expect and <br/>

    View full-size slide

  61. 3. We want minimal number of HTTP requests

    View full-size slide

  62. Common build tools
    operate on file trees

    View full-size slide

  63. Dependency graph

    View full-size slide

  64. http://webpack.github.io/

    View full-size slide

  65. Allow requiring CSS with Webpack
    1 module.exports = {
    2 module: {
    3 loaders: [
    4 {
    5 test: /\.css$/,
    6 use: ["style-loader", "css-loader"]
    7 }
    8 ]
    9 }
    10 };

    View full-size slide

  66. Now we can import CSS in JS!
    1 import './Button.css'
    2
    3 const Button = ({ children, disabled, active }) => {
    4 let classNames = 'btn'
    5 if (disabled) classNames += ' disabled'
    6 if (active) classNames += ' active'
    7 return (
    8 9 type="button"
    10 disabled={disabled}
    11 className={classNames}
    12 >
    13
    14 { children }
    15
    16
    17 )
    18 }
    19 export default Button

    View full-size slide

  67. Tips & Tricks
    ✅ Split your code by components
    ✅ One directory per UI component
    ✅ One CSS file per UI component
    ✅ Explicit dependencies
    ✅ Co-locate JS, CSS and assets

    View full-size slide

  68. How are we doing so far?
    No styles isolation
    Composition & code sharing
    ✅ Disconnected from component’s code
    ✅ Implicit dependencies

    View full-size slide

  69. 3. Styles isolation

    View full-size slide

  70. Global CSS
    1 .btn {
    2 /* styles for button */
    3 }
    4
    5 .active {
    6 /* styles for active button */
    7 background-color: blue;
    8 }
    9
    10 .label {
    11 /* styles for button label */
    12 }
    1 .star {
    2 /* styles for star */
    3 }
    4
    5 .active {
    6 /* styles for active star */
    7 background-color: orange;
    8 }
    9

    View full-size slide

  71. BEM
    https://en.bem.info/

    View full-size slide

  72. B
    E
    M
    Block
    Element
    Modifier
    Button
    Button__label
    Button --active

    View full-size slide

  73. Without BEM


    Click me!


    View full-size slide

  74. With BEM


    Click me!


    View full-size slide

  75. Using BEM
    1 /* Button.css */
    2
    3 .Button { /* general rules */ }
    4 .Button --disabled { /* disabled rules */ }
    5 .Button --active { /* active rules */ }
    6 .Button__label { /* label rules */ }

    View full-size slide

  76. Using BEM
    1 import './Button.css'
    2
    3 const Button = ({ children, disabled, active }) => {
    4 let classNames = 'Button'
    5 if (disabled) classNames += ' Button --disabled'
    6 if (active) classNames += ' Button --active'
    7 return (
    8 9 type="button"
    10 disabled={disabled}
    11 className={classNames}
    12 >
    13
    14 { children }
    15
    16
    17 )
    18 }
    19 export default Button

    View full-size slide

  77. BEM
    ✅ Global namespace
    ✅ Styles isolation
    Not beginner friendly
    Very verbose
    Requires discipline
    Long class names

    View full-size slide

  78. @markdalgleish

    View full-size slide

  79. CSS-modules
    https://github.com/css-modules/css-modules

    View full-size slide

  80. How CSS-modules work

    View full-size slide

  81. Before: BEM-style
    1 /* Button.css */
    2
    3 .Button { /* general rules */ }
    4 .Button --disabled { /* disabled rules */ }
    5 .Button --active { /* active rules */ }
    6 .Button__label { /* label rules */ }

    View full-size slide

  82. After: CSS-modules
    1 /* Button.css */
    2
    3 .root { /* general rules */ }
    4 .disabled { /* disabled rules */ }
    5 .active { /* active rules */ }
    6 .label { /* label rules */ }

    View full-size slide

  83. The result
    1 styles: {
    2 root: "Button__root__abc5436",
    3 disabled: "Button__disabled__def6
    4 active: "Button__active__1638bcd"
    5 label: "Button__label__5dfg462"
    5 }
    1 /* Button.css */
    2
    3 .root { /* general rules */ }
    4 .disabled { /* disabled rules */ }
    5 .active { /* active rules */ }
    6 .label { /* label rules */ }

    View full-size slide

  84. Before: BEM-style
    1 import './Button.css'
    2
    3 const Button = ({ children, disabled, active }) => {
    4 let classNames = 'Button'
    5 if (disabled) classNames += ' Button --disabled'
    6 if (active) classNames += ' Button --active'
    7 return (
    8 9 type="button"
    10 disabled={disabled}
    11 className={classNames}
    12 >
    13
    14 { children }
    15
    16
    17 )
    18 }
    19 export default Button

    View full-size slide

  85. 1 import styles from './Button.css'
    2
    3 const Button = ({ children, disabled, active }) => {
    4 let className = {styles.root}
    5 if (disabled) className = {styles.disabled}
    6 if (active) className = {styles.active}
    7 return (
    8 9 type="button"
    10 disabled={disabled}
    11 className={className}
    12 >
    13
    14 { children }
    15
    16
    17 )
    18 }
    19 export default Button
    After: CSS-modules

    View full-size slide

  86. The result


    Click me!




    Click me!


    View full-size slide

  87. 1 import styles from './Button.css'
    2
    3 const Button = ({ children, disabled, active }) => {
    4 let className = {styles.root}
    5 if (disabled) className = {styles.disabled}
    6 if (active) className = {styles.active}
    7 return (
    8 9 type="button"
    10 disabled={disabled}
    11 className={className}
    12 >
    13
    14 { children }
    15
    16
    17 )
    18 }
    19 export default Button
    After: CSS-modules

    View full-size slide

  88. Enabling CSS-modules
    1 // webpack.config.js
    2
    3 module.exports = {
    4 module: {
    5 loaders: [
    6 {
    7 test: /\.css$/,
    8 loader: "css"
    9 }
    10 ]
    11 }
    12 };

    View full-size slide

  89. Enabling CSS-modules
    1 // webpack.config.js
    2
    3 module.exports = {
    4 module: {
    5 loaders: [
    6 {
    7 test: /\.css$/,
    8 loader: "css?modules"
    9 }
    10 ]
    11 }
    12 };

    View full-size slide

  90. BEM-like class names
    1 // webpack.config.js
    2
    3 module.exports = {
    4 module: {
    5 loaders: [
    6 {
    7 test: /\.css$/,
    8 loader: "css?modules
    9 &localIdentName=[name]__[local]__[hash:base64:5]"
    10
    11 }
    12 ]
    13 }
    14 };

    View full-size slide

  91. import styles from './Button.css'
    /*
    styles = {
    button: ‘Button__root__3fslE’,
    label: ‘Button__label__I8bKh’
    }
    */ name local hash
    BEM-like class names

    View full-size slide

  92. BEM for free!

    View full-size slide

  93. 4. Composition &
    code sharing

    View full-size slide

  94. Composition with Sass
    1 /* Button.css */
    2
    3 .Button { /* common rules */ }
    4 .Button --disabled { /* disabled rules */ }
    5 .Button --active { /* active rules */ }

    View full-size slide

  95. Composition with Sass
    1 .Button --common { /* common rules */ }
    2 .Button --disabled {
    3 @extends .Button --common;
    4 /* gray color, light background */
    5 }
    6 .Button --active {
    7 @extends .Button --common;
    8 /* white color, blue background */
    9 }

    View full-size slide

  96. Composition with Sass
    1 .Button --common, .Button --disabled, .Button --active {
    2 /* common rules */
    3 }
    4 .Button --disabled {
    5 /* gray color, light background */
    6 }
    7 .Button --active {
    8 /* white color, blue background */
    9 }

    View full-size slide

  97. Composition with CSS-modules
    1 /* Button.css */
    2
    3 .root { /* general rules */ }
    4 .disabled { /* disabled rules */ }
    5 .active { /* active rules */ }

    View full-size slide

  98. Composition with CSS-modules
    1 /* Button.css */
    2
    3 .root { /* general rules */ }
    4 .disabled {
    5 composes: root;
    6 /* disabled rules */
    7 }
    8 .active {
    9 composes: root;
    10 /* active rules */
    11 }

    View full-size slide

  99. Composition with CSS-modules
    1 styles: {
    2 root: "Button__root__abc5436",
    3 disabled: "Button__root__abc5436 Button__disabled__def6547",
    4 active: "Button__root__abc5436 Button__active__1638bcd"
    5 }

    View full-size slide

  100. Use only one class name!
    1 /* Don't do this */
    2 `class=${[styles.normal, styles['active']].join(" ")}`
    3
    4 /* Using a single name makes a big difference */
    5 `class=${styles['active']}`
    6
    7 /* camelCase makes it even better */
    8 `class=${styles.isActive}`

    View full-size slide

  101. The result


    Click me!


    Button_active_Hf415”>

    Click me!


    View full-size slide

  102. Compose from other files!
    1 /* colors.css */
    2 .active {
    3 background-color: blue;
    4 }
    5 .secondary {
    6 background-color: #999;
    7 }
    1 /* Button.css */
    2 .root { /* general rules */ }
    3 .active {
    4 composes: root;
    5 composes: active from ' ../colors.css';
    6 /* active rules */
    7 }

    View full-size slide

  103. Or use variables!
    /* variables.css */
    @value small: (max-width: 599px);
    /* component.css */
    @value small from "./variables.css";
    .pageContent {
    background: green;
    @media small {
    background: red;
    }
    }

    View full-size slide

  104. 5. MinificationBonus

    View full-size slide

  105. Minification
    1 // webpack.config.js
    2
    3 module.exports = {
    4 module: {
    5 loaders: [
    6 {
    7 test: /\.css$/,
    8 loader: "css?modules"
    9 }
    10 ]
    11 }
    12 };

    View full-size slide

  106. Minification


    Click me!




    Click me!


    View full-size slide

  107. Solved with CSS-modules
    ✅ No styles isolation
    ✅ Constants sharing
    ✅ Minification
    ✅ Disconnected from component’s code
    ✅ Implicit dependencies

    View full-size slide

  108. CSS-modules overview

    View full-size slide

  109. Local by default but allow exceptions
    .local-class {
    color: red;
    }
    :global(.prefix-modal-open) .local-class {
    color: green;
    }

    View full-size slide

  110. Composition
    .common {
    /* all the common styles you want */
    }
    .normal {
    composes: common;
    /* anything that only applies to Normal */
    }
    .disabled {
    composes: common;
    /* anything that only applies to Disabled */
    }

    View full-size slide

  111. Explicit dependencies
    .otherClassName {
    composes: className from "./style.css";
    }

    View full-size slide

  112. Import variables
    /* variables.css */
    @value small: (max-width: 599px);
    /* component.css */
    @value small from "./breakpoints.scss";
    .pageContent {
    background: green;
    @media small {
    background: red;
    }
    }

    View full-size slide

  113. Use with pre-processors like Sass or Less
    :global {
    .this-is-global {
    color: yellow;
    }
    .potential-collision-city {
    color: crap;
    }
    }

    View full-size slide

  114. Works on server, too!
    https://github.com/css-modules/postcss-modules
    https://github.com/css-modules/cssm-rails

    View full-size slide

  115. PostCSS
    http://postcss.org

    View full-size slide

  116. Like Babel, but for CSS

    View full-size slide

  117. How PostCSS works

    View full-size slide

  118. What’s possible?

    View full-size slide

  119. Autoprefixer
    :full-screen {
    }
    :-webkit-:full-screen {
    }
    :-moz-:full-screen {
    }
    :full-screen {
    }

    View full-size slide

  120. Sass/less syntax

    View full-size slide

  121. Browser hacks

    View full-size slide

  122. Local CSS
    .name {
    color: gray;
    }
    .Logo__name__SVK0g {
    color: gray;
    }

    View full-size slide

  123. CSS next
    :root {
    --red: #d33;
    }
    a {
    &:hover {
    color: color(var( --red) a(54%));
    }
    }
    a:hover {
    color: #dd3333;
    color: rgba(221, 51, 51, 0.54);
    }

    View full-size slide

  124. Stylelint
    a {
    color: #4f;
    }

    View full-size slide

  125. PostCSS overview
    1. More powerful and faster than pre-processors thanks AST
    2. Lots of plugins and easy to write your own
    3. Tools! stylelint or automatic properties sorting
    4. IDE Support (WebStorm, Atom, Sublime)
    5. Shared constants between CSS and JS
    6. Works not only with SPAs or React
    7. It’s just CSS*

    View full-size slide

  126. http://postcss.org

    View full-size slide

  127. How to setup

    View full-size slide

  128. Example setup with Webpack
    module.exports = {
    module: {
    loaders: [
    {
    test: /\.css$/,
    use: ['style-loader', 'css-lader', 'postcss-loader']
    }
    ]
    },
    postcss: function() {
    return [
    require('autoprefixer')({ browsers: ['last 2 version'] })
    ];
    }
    };

    View full-size slide

  129. http://postcss.parts/
    by @mxstbr

    View full-size slide

  130. Lint CSS as pre-commit hook
    1 {
    2 "lint-staged": {
    3 "*.css": "stylelint"
    4 },
    5 "pre-commit": "lint-staged",
    6 "stylelint": {
    7 "extends": "stylelint-config-standard"
    8 }
    9 }
    https://github.com/okonet/lint-staged

    View full-size slide

  131. Do I need it?

    View full-size slide

  132. Do I need it?
    ✅ You work in a team
    ✅ You plan to update your CSS in the future
    ✅ You use third-party CSS and don’t want to break things
    ✅ You care about code readability and reusability
    ✅ You hate manual work

    View full-size slide

  133. What about CSS-in-JS?

    View full-size slide

  134. Worth checking out
    by @mxstbr and
    @glennmaddern
    by @oleg008

    View full-size slide

  135. – Glen Maddern
    https://github.com/css-modules/css-modules/issues/187#issuecomment-257752790
    “[…] I wouldn’t suggest using any CSS-in-JS tool over CSS
    Modules for decent-sized projects in the near term.”

    View full-size slide

  136. Final thoughts

    View full-size slide

  137. It’s not about writing code,
    it’s about reading it.

    View full-size slide

  138. Build isolated components.

    View full-size slide

  139. Separate concerns,
    not technologies.

    View full-size slide

  140. Treat CSS seriously.
    It’s here to stay.

    View full-size slide

  141. Choose the right tool
    for the job.

    View full-size slide

  142. Stay open-minded &
    keep experimenting!

    View full-size slide

  143. Andrey
    Okonetchnikov
    @okonetchnikov
    http://okonet.ru
    https://github.com/okonet
    Freelance UI-Developer

    View full-size slide