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

69bb6b30cd7b682ba5d5a1f352e6862a?s=128

Andrey Okonetchnikov

January 21, 2017
Tweet

Transcript

  1. Modular CSS with @okonetchnikov

  2. Andrey Okonetchnikov @okonetchnikov

  3. None
  4. ColorSnapper http://colorsnapper.com

  5. kaffemik Zollergasse 5, 1070 Wien

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

  7. I design & develop User Interfaces

  8. Building scalable User Interfaces

  9. None
  10. None
  11. None
  12. None
  13. – http://www.csszengarden.com/ “CSS allows complete and total control over the

    style of a hypertext document…”
  14. CSS was designed for documents, not for web-applications

  15. None
  16. None
  17. None
  18. User Interface ∋ Components

  19. Libraries & Frameworks based on components

  20. What are UI Components?

  21. List component Button Button

  22. List Item Component Button

  23. Atomic components Button

  24. Let’s build a component!

  25. In React.js everything is a component!

  26. Button component Default Active Disabled

  27. Button.js 1 const Button = ({ children }) => {

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

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

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

    { 2 let classNames = 'btn' 3 if (disabled) classNames += ' disabled' 4 return ( 5 <button 6 type="button" 7 disabled={disabled} 8 className={classNames} 9 > 10 <span className="label"> 11 { children } 12 </span> 13 </button> 14 ) 15 }
  31. 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 <button 7 type="button" 8 disabled={disabled} 9 className={classNames} 10 > 11 <span className="label"> 12 { children } 13 </span> 14 </button> 15 ) 16 }
  32. 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 }
  33. Button component Default Active Disabled <Button>Default </Button> <Button disabled>Disabled </Button>

    <Button active>Active </Button>
  34. Button component <Button active>Active </Button> <button class="btn active"> <span class="label">

    Click me! </span> </button> Active
  35. Star component <Star /> <Star active />

  36. Star.js 1 const Star = ({ active }) => {

    2 let classNames = 'star' 3 if (active) classNames += ' active' 4 return ( 5 <span className={classNames} /> 6 ) 7 }
  37. 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
  38. Star component <span class="star active"> </span> <Star active />

  39. f(props, state) => UI

  40. UI components should be pure, modular & isolated

  41. • Props • State • Styles Button

  42. • Props • State • Styles Button • Props •

    State • Styles • Props • State • Styles
  43. • Props • State • Styles Button • Props •

    State • Styles • Props • State • Styles
  44. and yet…

  45. • Props • State • Styles Button • Props •

    State • Styles • Props • State • Styles Global styles
  46. None
  47. 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
  48. .btn.active

  49. .content .albums .album .btn

  50. None
  51. http://www.standardista.com/css3/css-specificity/

  52. Debugging CSS

  53. .btn.active

  54. Where are my styles?

  55. Where is my CSS? 1 const Button = ({ children

    }) => { 2 return ( 3 <button 4 type="button" 5 className="btn" 6 > 7 <span className="label"> 8 { children } 9 </span> 10 </button> 11 ) 12 }
  56. Where is my CSS? Where is this class coming from?

    1 const Button = ({ children }) => { 2 return ( 3 <button 4 type="button" 5 className="btn" 6 > 7 <span className="label"> 8 { children } 9 </span> 10 </button> 11 ) 12 }
  57. Let’s search quickly…

  58. CSS Monolith!

  59. Refactoring application.css

  60. Problems with CSS in Web Applications Disconnected from component’s code

    Implicit dependencies No styles isolation No composition & code sharing
  61. 1. Split your code

  62. 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
  63. 1. Split & co-locate

  64. Separation of concerns? public ├── images │ └── icon.svg ├──

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

    ├── javascripts │ ├── Button.js │ └── Dropdown.js └── stylesheets ├── Buttons.css └── Dropdown.css
  66. 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
  67. 2. Dependencies

  68. CSS pre-processors?

  69. 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';
  70. Where is my CSS? Where is this class coming from?

    1 const Button = ({ children }) => { 2 return ( 3 <button 4 type="button" 5 className="btn" 6 > 7 <span className="label"> 8 { children } 9 </span> 10 </button> 11 ) 12 }
  71. None
  72. 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 <button 9 type="button" 10 disabled={disabled} 11 className={classNames} 12 > 13 <span className="label"> 14 { children } 15 </span> 16 </button> 17 ) 18 } 19 export default Button
  73. None
  74. 1. We can’t require CSS in JS

  75. 2. Browsers expect <link> and <script>

  76. 3. We want minimal number of HTTP requests

  77. Common build tools operate on file trees

  78. Dependency graph

  79. http://webpack.github.io/

  80. 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 };
  81. 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 <button 9 type="button" 10 disabled={disabled} 11 className={classNames} 12 > 13 <span className="label"> 14 { children } 15 </span> 16 </button> 17 ) 18 } 19 export default Button
  82. None
  83. 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
  84. How are we doing so far? No styles isolation Composition

    & code sharing ✅ Disconnected from component’s code ✅ Implicit dependencies
  85. 3. Styles isolation

  86. 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
  87. BEM https://en.bem.info/

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

  89. Without BEM <button class="button active"> <span class="label"> Click me! </span>

    </button>
  90. With BEM <button class="Button Button --active"> <span class="Button__label"> Click me!

    </span> </button>
  91. 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 */ }
  92. 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 <button 9 type="button" 10 disabled={disabled} 11 className={classNames} 12 > 13 <span className="Button__label"> 14 { children } 15 </span> 16 </button> 17 ) 18 } 19 export default Button
  93. BEM ✅ Global namespace ✅ Styles isolation Not beginner friendly

    Very verbose Requires discipline Long class names
  94. @markdalgleish

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

  96. None
  97. How CSS-modules work

  98. 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 */ }
  99. After: CSS-modules 1 /* Button.css */ 2 3 .root {

    /* general rules */ } 4 .disabled { /* disabled rules */ } 5 .active { /* active rules */ } 6 .label { /* label rules */ }
  100. 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 */ }
  101. 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 <button 9 type="button" 10 disabled={disabled} 11 className={classNames} 12 > 13 <span className="Button__label"> 14 { children } 15 </span> 16 </button> 17 ) 18 } 19 export default Button
  102. 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 <button 9 type="button" 10 disabled={disabled} 11 className={className} 12 > 13 <span className={styles.label}> 14 { children } 15 </span> 16 </button> 17 ) 18 } 19 export default Button After: CSS-modules
  103. The result <button class="Button"> <span class="Button__label"> Click me! </span> </button>

    <button class="Button_root_3fslE"> <span class="Button_label_I8bKh"> Click me! </span> </button>
  104. 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 <button 9 type="button" 10 disabled={disabled} 11 className={className} 12 > 13 <span className={styles.label}> 14 { children } 15 </span> 16 </button> 17 ) 18 } 19 export default Button After: CSS-modules
  105. 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 };
  106. 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 };
  107. 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 };
  108. import styles from './Button.css' /* styles = { button: ‘Button__root__3fslE’,

    label: ‘Button__label__I8bKh’ } */ name local hash BEM-like class names
  109. BEM for free! ❤

  110. 4. Composition & code sharing

  111. Composition with Sass 1 /* Button.css */ 2 3 .Button

    { /* common rules */ } 4 .Button --disabled { /* disabled rules */ } 5 .Button --active { /* active rules */ }
  112. 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 }
  113. 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 }
  114. Composition with CSS-modules 1 /* Button.css */ 2 3 .root

    { /* general rules */ } 4 .disabled { /* disabled rules */ } 5 .active { /* active rules */ }
  115. 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 }
  116. 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 }
  117. 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}`
  118. The result <button class="Button Button --active"> <span class="Button__label"> Click me!

    </span> </button> <button class=“Button_root_3fslE Button_active_Hf415”> <span class="Button_label_I8bKh"> Click me! </span> </button>
  119. 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 }
  120. Or use variables! /* variables.css */ @value small: (max-width: 599px);

    /* component.css */ @value small from "./variables.css"; .pageContent { background: green; @media small { background: red; } }
  121. 5. MinificationBonus

  122. 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 };
  123. Minification <button class="Button"> <span class="Button__label"> Click me! </span> </button> <button

    class="1s6s23ca312"> <span class="sdp9423cadg"> Click me! </span> </button>
  124. Solved with CSS-modules ✅ No styles isolation ✅ Constants sharing

    ✅ Minification ✅ Disconnected from component’s code ✅ Implicit dependencies
  125. CSS-modules overview

  126. Local by default but allow exceptions .local-class { color: red;

    } :global(.prefix-modal-open) .local-class { color: green; }
  127. 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 */ }
  128. Explicit dependencies .otherClassName { composes: className from "./style.css"; }

  129. Import variables /* variables.css */ @value small: (max-width: 599px); /*

    component.css */ @value small from "./breakpoints.scss"; .pageContent { background: green; @media small { background: red; } }
  130. Use with pre-processors like Sass or Less :global { .this-is-global

    { color: yellow; } .potential-collision-city { color: crap; } }
  131. Works on server, too! https://github.com/css-modules/postcss-modules https://github.com/css-modules/cssm-rails

  132. None
  133. FTW!

  134. PostCSS http://postcss.org

  135. Like Babel, but for CSS

  136. How PostCSS works

  137. What’s possible?

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

    :full-screen { }
  139. Sass/less syntax

  140. Browser hacks

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

    gray; }
  142. CSS next :root { --red: #d33; } a { &:hover

    { color: color(var( --red) a(54%)); } } a:hover { color: #dd3333; color: rgba(221, 51, 51, 0.54); }
  143. Stylelint a { color: #4f; }

  144. 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*
  145. http://postcss.org

  146. How to setup

  147. 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'] }) ]; } };
  148. http://postcss.parts/ by @mxstbr

  149. 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
  150. Do I need it?

  151. 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
  152. What about CSS-in-JS?

  153. None
  154. Worth checking out by @mxstbr and @glennmaddern by @oleg008

  155. – 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.”
  156. Final thoughts

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

  158. Build isolated components.

  159. Separate concerns, not technologies.

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

  161. Choose the right tool for the job.

  162. Stay open-minded & keep experimenting!

  163. Thank you!

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