A place to cache linked articles (think custom and personal wayback machine)
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

5 роки тому
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. title: Extending Styles
  2. url: http://philipwalton.com/articles/extending-styles/
  3. hash_url: 2f185d06dc6c4f9dff67e11d93cbb7a7
  4. <p>Last week <a href="https://twitter.com/simurai">@simurai</a> wrote <a href="http://simurai.com/blog/2015/05/11/nesting-components/">a great article</a> discussing the various strategies for contextual styling in CSS. If you haven’t read his article yet, you should—it will give you better context for this read, and you’ll probably learn something you didn’t know.</p><p>The problem? What is the best way to approach altering the look of a component when it’s a descendant of another component?</p><p>The example he uses is a button that should render differently when it’s inside the header. In the article @simurai outlines a number of the more common approaches, assesses the pros and cons of each, and then states that he’s not sure there’s a clear winner. He closes by opening it up to the community for feedback in the hopes that a consensus can be reached.</p><p>While I share his desire to nail down the best strategy (and I do have an opinion on the subject), I think it’s actually more valuable to discuss <em>how</em> one might approach answering this question rather than <em>what</em> the actual answer may be. If you understand the how and the why, you’ll be more equipped to answer similar questions in the future.</p><h2 id="criteria-for-choosing">Criteria for choosing</h2><p>The point of extending styles is to reuse code. If you’ve defined some base-level styles, you want to be able to use those styles again without having to rewrite them. And if you need to change those base-level styles, you want those changes to propagate throughout.</p><p>Simply reusing code is easy. But reusing code in a way that is predictable, maintainable, and scalable is hard. Fortunately, computer scientists have been studying these problems for decades, and a lot of the principles of good software design apply to CSS as well.</p><h3 id="adherence-to-software-design-principles">Adherence to software design principles</h3><p>All of the options @simurai lists in his article are examples of either modifying a style declaration or extending it. When presented with these two choices, we can heed the advice offered by the <a href="http://en.wikipedia.org/wiki/Open/closed_principle">open/close principle</a> of software development. It states:</p><blockquote><p>software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification</p></blockquote><p>To understand what this means in the context of CSS components, it’s important to define the terms <em>extension</em> and <em>modification</em>.</p><p>Modifying a component means you change its style definition—its properties and values. Extending a component, by contrast, means you take an existing component and build on top of it. You do not change the definition of the existing component; instead, you create a new component that includes the original styles and adds new styles (or overrides) on top of them.</p><p>There are two primary reasons why components should be extended rather than modified. First of all, when you modify a component you break its contract and the expectations of developers familiar with that component. You also run the risk of breaking your existing design. For small sites this risk is probably minimal, but for large sites with lots of components, you may not always know the full extent of how all your styles are used.</p><p>A second reason to prefer extension over modification is when you modify a component, you limit your options going forward. You can no longer use that component in its pre-modified form.</p><h3 id="compatibility-with-future-technologies">Compatibility with future technologies</h3><p>Another important criteria for weighing our options and choosing our best-practices is how those practices will align with future technologies. Writing modular CSS today is challenging because the web platform doesn’t support a lot of the feature we’ve come to enjoy in other environments that promote modular development. But this will not always be the case.</p><p>As the web evolves, it’s going to become easier and easier to write CSS without having to worry about all the complications and <a href="/articles/side-effects-in-css/">side effects</a> that come from all rules existing in the global scope. So we need to make sure our choices today don’t force our hand and lock us in to outdated technology tomorrow.</p><p>Web Components give us real solutions to almost all the problems that make writing modular CSS hard. And now that all major browser vendors have <a href="https://www.w3.org/wiki/Webapps/WebComponentsApril2015Meeting">reached some consensus</a> on the contentious parts of the specification and agreed to move forward with implementation, we as web developers need to start thinking about how our current methodologies will fit into that future.</p><p>With these things in mind, let’s consider the current options.</p><h2 id="option-1-ndash-descendant-combinator">Option 1 – descendant combinator</h2><p>Option 1 is a textbook example of component modification—what the open/closed principle says <em>not</em> to do.</p><pre class="hljs"><code class="css"><span class="hljs-class">.Header</span> <span class="hljs-class">.Button</span> <span class="hljs-rules">{
  5. <span class="hljs-rule"><span class="hljs-attribute">font-size</span>:<span class="hljs-value"> .<span class="hljs-number">75em</span></span></span>;
  6. }</span></code></pre><p>In this example the <code>.Button</code> component is defined somewhere else in the stylesheet, and then it’s redefined (modified) here for all cases where <code>.Button</code> appears as a descendant of <code>.Header</code>.</p><p>As I mentioned above, this practice can be really problematic. It makes the <code>.Button</code> component less predictable because it can now render differently depending on where it lives in the HTML. Someone on the team who has used <code>.Button</code> in the past might want to use it again but be unaware that its definition has been changed outside of its source file.</p><p>Moreover, this option is short-sighted. It solves the problem at hand, yet it limits your options for using the <code>.Button</code> component in the future. What if a new feature is added that requires additional buttons in the header, and those new buttons need to look like <code>.Button</code> did before it was modified? Since this approach changes the definition of <code>.Button</code>, its pre-modified styles can no longer be used inside <code>.Header</code>, and refactoring will have to happen, increasing the risk of bugs.</p><h2 id="option-2-ndash-variations">Option 2 – variations</h2><p>In BEM this option is called a “modifier” (the “M” in BEM), and in SMACSS it’s called “subclassing”. Note that despite being called a modifier in BEM, it’s not a modification in the sense that the open/close principle warns against.</p><pre class="hljs"><code class="css"><span class="hljs-class">.Button--small</span> <span class="hljs-rules">{
  7. <span class="hljs-rule"><span class="hljs-attribute">font-size</span>:<span class="hljs-value"> .<span class="hljs-number">75em</span></span></span>;
  8. }</span></code></pre><pre class="hljs"><code class="html"><span class="hljs-tag">&lt;<span class="hljs-title">header</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"Header"</span>&gt;</span>
  9. <span class="hljs-tag">&lt;<span class="hljs-title">button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"Button Button--small"</span>&gt;</span>Download<span class="hljs-tag">&lt;/<span class="hljs-title">button</span>&gt;</span>
  10. <span class="hljs-tag">&lt;/<span class="hljs-title">header</span>&gt;</span></code></pre><p>When using this option, you don’t change the original style definition, so you’re still able to use the original <code>.Button</code> component inside of <code>.Header</code>.</p><h2 id="option-3-ndash-adopted-child">Option 3 – adopted child</h2><p>With the adopted child option (or <a href="https://en.bem.info/forum/issues/4/">mixes</a> as it’s called in BEM) you style an element with two classes from two different components.</p><p>While I’ve certainly used this pattern in my own code from time to time, it’s always made me a little uneasy. The problem with this approach is if two or more classes are applied to the same element, and they contain some of the same property declarations, the more specific selector will win. Sometimes this works out exactly how you want, but sometimes it doesn’t, and you have to resort to specificity hacks (as you can see in the provided example).</p><p>In <em>header.css</em>:</p><pre class="hljs"><code class="css"><span class="hljs-comment">/*
  11. * Increased specificity needed so this class will win
  12. * when used on elements with the class "Button".
  13. */</span>
  14. <span class="hljs-class">.Header</span> <span class="hljs-class">.Header-item</span> <span class="hljs-rules">{
  15. <span class="hljs-rule"><span class="hljs-attribute">font-size</span>:<span class="hljs-value"> .<span class="hljs-number">75em</span></span></span>;
  16. }</span></code></pre><p>And in <em>button.css</em>:</p><pre class="hljs"><code class="css"><span class="hljs-class">.Button</span> <span class="hljs-rules">{
  17. <span class="hljs-rule"><span class="hljs-attribute">font-size</span>:<span class="hljs-value"> <span class="hljs-number">1em</span></span></span>;
  18. }</span></code></pre><p>While sometimes a comment like the one in <em>header.css</em> above does the trick, it’s definitely not a fool-proof solution.</p><p>Whenever you put more than one class on an element, those classes combine to form the final, rendered state. With modifiers this is not really a problem because the two classes are defined in the same file, so cascade preference can be easily managed by source order.</p><p>On the other hand, when adding two classes to an element and those classes are defined in <em>different</em> files, that’s where you run into issues. Most of the time there is a “base” class and one or more “extending” classes, and in those cases I think it makes more sense to make the relationship explicit and the dependencies clear. More about that in option 4.</p><h2 id="option-4-ndash-extend">Option 4 – @extend</h2><p>Most CSS preprocessors today support some method of extending existing styles. In fact, this may soon be supported natively in CSS if the <a href="https://tabatkins.github.io/specs/css-extend-rule/">extend rule proposal</a> is approved.</p><p>And most preprocessors also support declaring dependencies through <code>import</code> or <code>include</code> statements, which helps ensure your styles cascade properly by forcing the correct source order at build time.</p><pre class="hljs"><code class="scss"><span class="hljs-at_rule">@<span class="hljs-keyword">import</span> <span class="hljs-string">'./button.css'</span>;</span>
  19. <span class="hljs-class">.PromoButton</span> {
  20. <span class="hljs-at_rule">@<span class="hljs-keyword">extend</span><span class="hljs-preprocessor"> .Button</span>;</span>
  21. <span class="hljs-comment">/* Additional styles... */</span>
  22. }</code></pre><pre class="hljs"><code class="html"><span class="hljs-tag">&lt;<span class="hljs-title">header</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"Header"</span>&gt;</span>
  23. <span class="hljs-tag">&lt;<span class="hljs-title">button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"PromoButton"</span>&gt;</span>Download<span class="hljs-tag">&lt;/<span class="hljs-title">button</span>&gt;</span>
  24. <span class="hljs-tag">&lt;/<span class="hljs-title">header</span>&gt;</span></code></pre><p>What’s nice about this approach is it’s clear to other developers that <code>.PromoButton</code> includes styles from <code>.Button</code>, and it’s clear to the preprocessor (or build system) that <em>button.css</em> needs to be included before <em>promo-button.css</em> when the final stylesheet is created.</p><p>If you were using the mixes approach above and including two or more classes on a single HTML element, <code>@extend</code> can be a very handy way to construct a new component from those parts while simultaneously ensuring the source order is correct. In the following example, all styles will appear in the order they are imported.<sup><a href="#footnote-1">[1]</a></sup></p><pre class="hljs"><code class="scss"><span class="hljs-at_rule">@<span class="hljs-keyword">import</span> <span class="hljs-string">'./button.css'</span>;</span>
  25. <span class="hljs-at_rule">@<span class="hljs-keyword">import</span> <span class="hljs-string">'./header.css'</span>;</span>
  26. <span class="hljs-class">.PromoButton</span> {
  27. <span class="hljs-at_rule">@<span class="hljs-keyword">extend</span><span class="hljs-preprocessor"> .Button</span>;</span>
  28. <span class="hljs-at_rule">@<span class="hljs-keyword">extend</span><span class="hljs-preprocessor"> .Header-item</span>;</span>
  29. <span class="hljs-comment">/* Optional additional styles... */</span>
  30. }</code></pre><h2 id="web-component-considerations">Web Component considerations</h2><p>The primary way a future shift to Web Components will affect this discussion is that styling elements will no longer simply be a function of adding classes to elements or selectors to your stylesheets.</p><p>With Web Components (specifically Shadow DOM), the only styles that can affect the inner-workings of an element are the styles that the component author has packaged within that element. Likewise, the only way a parent context is allowed to affect the style of an element is if the component author has explicitly OK’d it.<sup><a href="#footnote-2">[2]</a></sup></p><p>This means that if you use options 1 or 3 now, it will be quite a bit harder to transition your code to use Web Components. Option 1 will never be able to work with third-party components (since they can’t predict your HTML structure in advance), and adding a list of classes to a custom element (option 3) will only affect that particular element. It will <em>not</em> affect its descendants.</p><p>Options 2 and 4 are much more Web Component-friendly because they more closely resemble a single-component model. Web Components encapsulate styles and functionality internally, and they expose that to developers as a single HTML element. This means that components are always a single thing, even if under the hood they’re the result of a bunch of smaller things put together.</p><p>Consider the following HTML. There’s a button component that should be displayed as block and take up the full width of its container. It should also use the typeface of the company’s logo:</p><pre class="hljs"><code class="html"><span class="hljs-tag">&lt;<span class="hljs-title">button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"Button FullWidthBlock LogoType"</span>&gt;</span>Download<span class="hljs-tag">&lt;/<span class="hljs-title">button</span>&gt;</span></code></pre><p>Converting this to a Web Component in the following way (similar to option 3) will not work:</p><pre class="hljs"><code class="html"><span class="hljs-tag">&lt;<span class="hljs-title">promo-button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"FullWidthBlock LogoType"</span>&gt;</span>Download<span class="hljs-tag">&lt;/<span class="hljs-title">promo-button</span>&gt;</span></code></pre><p>Instead, you’d have to add these styles to the shadow root, as part of the component’s internal (private) implementation:</p><pre class="hljs"><code class="html"><span class="hljs-comment">&lt;!-- Pseudo Code --&gt;</span>
  31. <span class="hljs-tag">&lt;<span class="hljs-title">promo-button</span>&gt;</span>
  32. #shadow-root
  33. <span class="hljs-tag">&lt;<span class="hljs-title">style</span>&gt;</span><span class="css">
  34. <span class="hljs-at_rule">@<span class="hljs-keyword">import</span> <span class="hljs-string">'./button.css'</span></span>;
  35. <span class="hljs-at_rule">@<span class="hljs-keyword">import</span> <span class="hljs-string">'./full-width-block.css'</span></span>;
  36. <span class="hljs-at_rule">@<span class="hljs-keyword">import</span> <span class="hljs-string">'./logo-type.css'</span></span>;
  37. </span><span class="hljs-tag">&lt;/<span class="hljs-title">style</span>&gt;</span>
  38. <span class="hljs-tag">&lt;<span class="hljs-title">button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"Button FullWidth LogoType"</span>&gt;</span>
  39. <span class="hljs-tag">&lt;<span class="hljs-title">content</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">content</span>&gt;</span>
  40. <span class="hljs-tag">&lt;/<span class="hljs-title">button</span>&gt;</span>
  41. /#shadow-root
  42. <span class="hljs-tag">&lt;/<span class="hljs-title">promo-button</span>&gt;</span></code></pre><p>This may seem like more work, but it will end up being much more robust and predictable. This component will always look exactly how you want, regardless of where it appears in the HTML and what other styles exist on the page.</p><p>This is very similar using <code>@extend</code> as shown in option 4. If you use this pattern in your code today, it will be very easy to transition your CSS components to Web Components in the future.</p><p>Likewise, option 2 (variations) also fits nicely into the Web Component model. However, instead of modifier classes, we’ll likely define element attributes that represent the different variations of our components.</p><pre class="hljs"><code class="html"><span class="hljs-comment">&lt;!-- Using a BEM modifier --&gt;</span>
  43. <span class="hljs-tag">&lt;<span class="hljs-title">button</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"Button Button--small"</span>&gt;</span>Download<span class="hljs-tag">&lt;/<span class="hljs-title">button</span>&gt;</span>
  44. <span class="hljs-comment">&lt;!-- Using a Web Component with an attribute for variation --&gt;</span>
  45. <span class="hljs-tag">&lt;<span class="hljs-title">my-button</span> <span class="hljs-attribute">small</span>&gt;</span>Download<span class="hljs-tag">&lt;/<span class="hljs-title">my-button</span>&gt;</span></code></pre><p>Attributes become part of the public API for styling components, and only the approved attributes will affect their look. Attributes without a corresponding internal style rule will simply do nothing.</p><h2 id="conclusions">Conclusions</h2><p>Given all the options discussed so far, I favor option 2 for simple style extensions and option 4 for anything more complex.</p><p>If the component in question just needs a small change in some new context, a variation (modifier/subclass) is usually simpler and makes more sense. On the other hand, if the component in question is really its own thing, built on top of a base component, requiring a multi-level inheritance hierarchy, or composing several complex styles together, it’s probably better to make those relationships known through <code>@extend</code> statements and explicitly listed dependencies.</p><p>In general, when faced with these decisions it’s important to not just think about solving the immediate problem at hand. You should also consider how your choices will limit your options in the future. Are you coding yourself into a corner, or are you leaving yourself room to build new features and adapt to future design requirements.</p><aside class="Footnotes"><ol class="Footnotes-items"><li id="footnote-1">Technically, most preprocessors won’t actually guarantee correct source order based on the order of <code>@import</code> statements; <code>@import</code> simply means <em>this file must exist in the source before I include myself</em>. In practice, however, if all your component files <code>@import</code> their dependencies in the correct order, the final stylesheet’s order will also be correct.</li><li id="footnote-2">This can be accomplished via the <a href="http://dev.w3.org/csswg/css-scoping/#host-selector"><code>:host-context()</code></a> selector, though arguably its usage should be mostly avoided for all the reasons listed in this article.</li></ol></aside>