2

My Approach To Writing Maintainable CSS

 3 years ago
source link: https://tenbit.co/blog/my-approach-to-writing-maintainable-css/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
By TEN BITCOMB, 
03.02.2019

My Approach To Writing Maintainable CSS

It's been over a year since I wrote my original article on how I tend to write CSS for the web apps I work on, and some of my opinions have changed.

Thus, I think it's time I release a revised explanation of how I write CSS and why I find my methods to be ideal.  CSS is commonly regarded with disdain, some reasons being good and many bad; I, as well, had a deep hatred for CSS and how seemingly flimsy it was, but now that I've learned from most of my mistakes, I can say that I rather enjoy writing it.

I hope you can learn to enjoy writing it, as well!  Implementing a great design, as well as being able to easily and reliably modify it over time, comes with a feeling of satisfaction.  You don't even need to be a designer to get joy out of styling web pages, as there are time-tested layouts and typographic styles you can steal from and adjust to taste.

On CSS Frameworks

A lot of web developers avoid the pitfalls of writing CSS by using frameworks such as Bootstrap or Materialize. Frameworks are excellent for rapidly prototyping applications, and they can even work well for apps in production, so long as you aren't planning to deviate from their style.

Over time, I've come to appreciate full-fledged CSS frameworks less and less for the following reasons:

  • Junior developers end up believing that frameworks are the standard, and they are less likely to learn to write proper CSS in the first place because their framework of choice becomes a crutch.
  • The idea of a "framework" is becoming dated, as package managers and new concepts like Web Components make the idea of importing a does-it-all framework growingly obsolete. Why not instead import only the components you need and tailor them how you want without being restricted to a single version of a framework?
  • The vast majority of CSS frameworks end up being poorly-maintained, and the few developers working on such frameworks often don't have the time to fix even glaring problems.
  • Now that we have Flex and Grid, there's even less reason to go with frameworks that clutter the DOM to address the same problems. Flex alone is easy and works well enough in most cases that I'd really have to question why someone would need a 3rd party grid system.

These days, I almost ever use frameworks when starting new projects. I've been burned too many times by bugs that go unfixed, even in Bootstrap, or lousy patterns that become a hassle to work around. Most "workarounds" for styling within a framework are more akin to hacks, anyway.

As referenced in the list above, if I must use someone else's styling, it will be through either importing a specific style sheet or by using a web component in isolation, as anything I do to override their styles won't pollute the styles in the rest of my application.

If you're just learning how to write web applications, I'd say put off CSS frameworks for later. Learn how to write basic CSS so you can either learn to love it or actually appreciate frameworks for what they do for you down the road(letting you stick to writing code).


There is, of course, no one way to write your CSS. There are, however, ideas I tend to stick to that have served me well.

Nearly All Selectors Should Have the Same Specificity

One of the aspects of BEM methodology that makes it so popular is the lack of cascading. Indeed, cascading is a feature of CSS that is seemingly really useful and powerful(overpowered, really), but can easily make your stylesheets hairy and hard to work with.

Think of cascading in style sheets as the same as having one's code littered with several nested if-statements. As it turns out, you rarely ever need cascading, especially if you aren't writing overly-generalized selectors.

For example, if you were to eliminate cascade, your CSS selectors might go from something like this:

  .block .element.modifier {
    color: blue
  }

To this:

  .block__element--modifier {
    color: blue
  }

Do you have to use BEM? Hell, no. Use a naming convention of your choice. I just prefer the clarity of BEM.

By keeping your selectors all at the same level, you significantly reduce the possibility of competing & conflicting selectors. Anyone with a modicum of experience dealing with other people's CSS knows that heavily-nested selectors make debugging styles to be hellish.

NOTE: One exception I've found to the anti-nesting guideline is in styling programmatically-generated text. You will notice that the body of this blog post contains very few class names; this is because I both hand-write the HTML and use contenteditable, and document.execCommand doesn't make it easy to add class names to generated elements. Creating a .prose class name and extending the styles of nested tag names off other base classes seemed like an acceptable solution with minimal leakage potential.

You may find yourself in a situation where nesting a selector is the better way or the only way to achieve a particular style. This is fine; as long as you are cognizant of what you are doing, keeping as many selectors at the same level as possible will still benefit you.

It's important that you don't use !important

Do you do this a lot?

  .component {
    margin-left: 10px !important;
  }

You must not do this. Ever. By using !important, you are sabotaging yourself by adding styles that are difficult to override. If you keep everything at the same specificity level, as well as follow other principles outlined in this blog post, you should never* need to use !important. The use of this flag is a sign that your CSS too complicated.

* You may be forced to use !important when overriding styles imported by a third-party. Ideally, you shouldn't be importing crap CSS in the first place.

Generalization is generally bad, so avoid it
when possible

You aren't writing a framework; you're writing an application. The chance that your application code and styles are going to be reused elsewhere is extremely low in reality. Even if components in your application are reused in your other applications, generalizing your CSS will lead to a lot more problems than they solve. Why? Because it causes coupling between your applications.

In the long term, it will be easier to build styles that are component-specific, and if these styles get reused in another application, they can be overridden in that application to fit its own style. Writing highly composable CSS borders on writing a framework, which will usually be a waste of your time.

Don't do this:

  <div class="wrapper panel rounded clearfix flex padded">
    ...
  </div>

Do this:

  <div class="checkout-items product-page__checkout-items">
    ...
  </div>

Your .checkout-items selector should encompass what a list of checkout items should look like across your application. If there are different versions of a concept of a checkout items list, you can either use a modifier(e.g. .checkout-items--alt), a page-specific selector, or create a different component with different selectors.

By not generalizing you make your CSS easily replaceable, as you aren't tying the look of a component to multiple generic selectors. As much as reusability sounds nice, replaceability is more often preferable.

Choose the right units

In my original blog post on this topic, I asserted that only px units should be used and that em and rem should be avoided. I no longer think this is entirely true, although I also think the exclusive use of px is still perfectly valid.

The truth is that a pixel on a web page is not a true pixel, and 1px hasn't represented a single hardware pixel for a very long time. Just as em and rem are relative units, px is relative to the browser's reference pixel. Want to write your CSS using only px? Go right ahead.

In short, this is how you should use different units:

Anywhere You can't go wrong with px or rem. Prefer rem if your content is conducive to custom font sizes in the user's browser. Element Sizing Use px, %, or rem. Avoid em always. Font Size Use px or rem. em can be used sometimes if your font size should be relative to the inherited font size, but I usually avoid it since I've yet to encounter a problem that could only be solved that way. Padding and Margins Use px or rem only.

NOTE: While px requires a bit less mental arithmetic, rem , being relative to the root font size, allows users to adjust the default font size of their browser and have that setting reflect in your web pages. Sometimes web pages aren't conducive to a user's custom font settings, but if yours contain mostly prose(like the one you're reading), then rem is preferred.

What about pt, pica, cm, inches, etc.?

If you are targeting print media, then using pt might be a good idea. Same goes for other units like cm and in, which are truly absolute units. Other than for print, you probably don't need to use them.

Opt for ordering by appearance, followed by alphabetic ordering

I have a passion for reducing unnecessary cognitive load, and the concept is highly unappreciated among developers, in my experience. Little things such as the ordering declarations might seem trivial on their own, but grains of sand build a sandcastle.

Assuming an aside would normally appear in the middle of a page with sections, instead of this:

  .aside {
    padding:    1.5rem;
    font-size:  0.85em;
    margin-top: 2rem;
    background: #f3e8be;
    margin-bottom: 2rem;
  }
  .section {
    padding: 3rem;
    margin: 1rem;
  }

Try this:

  .section {
    margin: 1rem;
    padding: 3rem;
  }
  .aside {
    background: #f3e8be;
    font-size:  0.85em;
    margin-top: 2rem;
    margin-bottom: 2rem;
    padding:    1.5rem;
  }

Granted, I sometimes deviate from this convention either unwittingly or through laziness, but sticking to it at least most of the time reduces cognitive overhead.

Separate layout and stylistic concerns

A great way to prevent your CSS from being convoluted is to handle page layout with separate selectors from those that define the look and feel of individual elements/components.

This makes a ton of sense because a component can live on any page or route in your app, thus a component shouldn't need preconceived "knowledge" of the environment it currently resides in. Inversely, the layout implementation shouldn't be tangled with the inner implementation of components.

What this means, in summary, is that the style for the parent element of a component shouldn't have a defined margin, or float property, etc., and the CSS defining the layout for a specific page shouldn't be stylistic properties on the components residing in that page, especially when it comes to the elements inside a component.

This keeps your style sheets very clear and easy to manipulate. Separating your concerns means that changing the styling of a component won't(usually) affect its position on a given page, and changing the CSS for a page layout shouldn't impact the contents of a component. You are avoiding side effects this way and promoting replaceability.

One way that people separate layout selectors from stylistic selectors is to use prefixes;  BEMIT is an extension on BEM that adds prefixes for things like objects(o-*), layouts(l-*), utilities(u-*), booleans(is-*, has-*), etc. Having used BEMIT in the past, I'm not a fan of it, but that's really a matter of taste. BEM is already verbose enough, and I didn't find that adding multiple prefixes added much value.

The way that I separate layout selectors from stylistic selectors in my CSS is to delegate stylistic selectors to class names, and to delegate layout selectors to IDs. This makes a lot of sense because distinct elements on a page deserve their own IDs; if an element is repeated on a page, its layout should then be controlled by a wrapping element with a distinct ID.

Let's say we wrote this markup for a page showing a news article:

  <header id="masthead" class="masthead">
    <div id="masthead__content" class="masthead__content demo-component">
      Masthead
    </div>
  </header>
  <main id="article">
    <div id="article__left-sidebar" class="sidebar">
      Left Sidebar
    </div>
    <article id="article__body" class="prose">
      <h2>Lorem ipsum</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas placerat facilisis justo sed lobortis.</p> 
      <p>Proin ornare libero at ligula blandit dignissim. Donec ultricies congue tellus, at porttitor ex egestas non.</p>
    </article>
    <div id="article__right-sidebar" class="sidebar">
      Right Sidebar
    </div>
  </main>
  <footer id="footer" class="footer">
    Footer
  </footer>      

It should be obvious that we want to display a holy grail style layout.

If I were to write the stylesheet to implement the layout of this page, I would write something like the following:

  /* app/styles/layouts/application.css */

  #masthead__content {
    margin: 0 auto;
    width: 100%;
  }
  
  #footer {
    width: 100%;
  }
  
  /* app/styles/layouts/article.css */
  
  #article {
    display: flex;
    margin: 0 auto;
    overflow: hidden;
    width: 100%;
  }
  
  #article__left-sidebar {
    flex-basis: 15%;
    margin-right: 42px;
    width: 100px;
  }
  
  #article__body {
    flex-basis: 100%;
  }
  
  #article__right-sidebar {
    flex-basis: 15%;
    margin-left: 42px;
    width: 100px;
  }
  
  @media(max-width: 640px){
    #article {
      display: flex;
      flex-direction: column;
    }
    
    #article__left-sidebar, #article__right-sidebar {
      margin: 0;
      width: 100%;
    }

    #article__body {
      flex-basis: initial;
    }
  }
  
  // app/styles/components/masthead.css
  .masthead {
    background: purple;
    color: white;
    padding: 1rem;
  }
  
  // app/styles/components/sidebar.css
  .sidebar {  
    background: lightgray;
    padding: 0.5rem;
  }
  
  // app/styles/components/prose.css
  .prose {
    background: white;
    padding: 0.5rem;
  }
  
  // app/styles/components/footer.css
  .footer {
    background: darkorange;
    box-sizing: border-box;
    padding: 1rem;
  }

Here's the layout in action:

   Controller  
    action   

      
    
    @action
     
       
    

  

    ApplicationController

If you're on desktop, you'll notice that resizing the screen below 640px in width will cause the article content to collapse into a single column. Those using mobile won't be able to see the full three-column layout.

The magic of Sass mixins

You might be wondering what to do when a component needs to have a different value for a stylistic property, such as padding or font-size, at different page widths or on different pages.

While new CSS features like custom properties(aka CSS variables) are becoming more widely adopted, preprocessors such as Sass are here to stay for quite some time, and offer many useful features that CSS just won't be supporting for time to come.

With Sass, being my CSS preprocessor of choice, I use mixins to allow my component styles to expose an "API" to my layout styles so the layout can tell components to adjust to different conditions without getting tangled in the inner workings of the styles for those specific components. It's a great way to maintain a separation of concerns while still maintaining flexibility.

Let's imagine that we've decided that, while the purple masthead looks good on desktop screens, a different background color would look better on mobile screens. Rather than have the layout styles get muddled in what the background and text colors should be for the masthead component, and instead of expecting the component style to know anything about breakpoints at different screen widths, we can create a mixin that defines a specific look for the mobile version of the masthead.

  // app/styles/components/masthead.scss

  @mixin masthead--mobile {
    background: pink;
    color: black;
    padding: 0.8rem;
  }
  
  .masthead--mobile {
    // for testing purposes or JS toggling
    @include masthead--mobile;
  }

Then we can include it in the layout style for the application:

  // app/styles/layouts/application.scss

  @media(max-width: 640px){
    #masthead {
      @include masthead--mobile;
    }
  }

Now our masthead will change colors when the screen-width is below 640px.

The hideousness of this example layout notwithstanding, I find this solution to be elegant because the layout doesn't need to be involved with knowing the actual color of the masthead element or the fact that its text color should be adjusted to contrast; all it has to do is "tell" the component to appear how it's expected to look on mobile.

By separating concerns, a new developer can jump in and modify a component's style without having to also worry(too much) about how it will be positioned on pages and at different screen widths. They can simply focus on making a component look good in isolation and, so long as they don't break the "API" a la the component's mixins, they can have confidence in their work not conflicting with styles arbitrarily imposed upon it from elsewhere in the application.

Conclusion

To paraphrase a quote I once heard(by someone whose name I don't remember), it is limitation that facilitates the best art. In other words, when forced to work with a limited set of tools, good artists will usually make better art than one with all the tools their disposal.

While I wouldn't say that one should limit themself for the sheer sake of it, knowing when and when not to use a tool is crucial to the outcome. Despite its detractors, CSS is powerful. It allows developers to apply layer upon layer of nested styling with the ability to select by ID, or class name, or element property.  You can do a lot with it, but things can get hairy in short order.

In reality, the vast majority of webpages don't need most of the complexity that CSS provides. By choosing to limit the power given to you by CSS, you will make things easier for you in the long run.

Styling webpages can be fun! Just don't let the power of CSS work against you.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK