8. Frameworks, Libraries, and JavaScript – Architecting CSS: The Programmer’s Guide to Effective Style Sheets

© Martine Dowden and Michael Dowden 2020
M. Dowden, M. DowdenArchitecting CSShttps://doi.org/10.1007/978-1-4842-5750-0_8

8. Frameworks, Libraries, and JavaScript

Martine Dowden1  and Michael Dowden1
(1)
Brownsburg, IN, USA
 

In a real-world application, your CSS does not function in isolation. This chapter covers some of the important considerations of a modern front-end web application, including how your choice of CSS or JavaScript frameworks may impact your application styles.

JavaScript

OK, yes this is a CSS book, so why are we suddenly talking about JavaScript? The reality is that a significant amount of front-end development is done using some sort of framework and/or UI library, many of which rely on JavaScript. Furthermore, JavaScript is often used to manipulate CSS as a result of state change or user interaction.

According to Stack Overflow’s 2019 annual survey, the most popular programming language for the past seven consecutive years is JavaScript. The breakdown of the top ten can be found in Figure 8-1.1
Figure 8-1

Most Popular Programming, Scripting, and Markup Languages in 2019 According to Stack Overflow

The most popular web framework is still jQuery, followed by React and Angular. The top ten breakdown is shown in Figure 8-2.2
Figure 8-2

Most Popular Web Frameworks in 2019 According to Stack Overflow

Manipulating CSS Using JS

Made easy by libraries such as jQuery, and vanilla JavaScript properties such as element.ClassList, we have been manipulating our CSS via our JS for years. But what exactly does that do to specificity and cascading? Table 8-1 lists some of the common ways of affecting visual output through JS and their impact on specificity.
Table 8-1

CSS Altering JavaScript Methods

Property and Methods

What It Does

Effects on Specificity

element.ClassList

    •  add()

    •  remove()

    •  replace()

    •  toggle()

    •  item()

    • contains()

Reads and manipulates the classes attached to a particular element.

Because styles are being applied by referencing classes, cascading and inheritance will not be significantly impacted.

element.style

Examples:

elem.style =

"color: blue, font-size: 12px"

Or

elem.setAttribute("style", "color:blue; font-size: 12px"

Or

elem.style.color = "blue"

Adds styles inline on the element.

Very difficult to override because they are inline. If the element already has inline styles, they will be overridden by what is being applied by the JavaScript.

Both a huge advantage and risk, JavaScript has the ability to easily override anything declared by CSS but does not have to.

If we don’t want to mix CSS rules in our JavaScripts, the methods given to us via classList allow for adding and removing classes to the element without ever touching the CSS itself. This brings with it the huge benefit of styling definition staying in CSS files and not being split between JS and CSS files.

Affecting styles or the DOM itself brings the advantage that the specificity of CSS being applied matters very little since it will be overridden. Via this technique, regardless of any other context, as a developer, we can dictate that if a user performs a certain action, or a particular state is achieved, a particular set of styles will be applied regardless of other styling, or context. This technique is often used to show and hide dialog boxes, for example, notification messages. The advantage of this technique is that the style changes can stay with its trigger, or action, and regardless of context, the styles being applied will not be overridden by preexisting CSS.

There are situations where JavaScript allows us to do things that are simply not possible with pure HTML and CSS solution. A perfect example would be using event listeners for the animation’s timing, start, or end (see Listings 8-1 to 8-3 and Figure 8-3).
<body>
  <div class="image">
    <img src="art.png" alt="modern art" id="image">
    <button onClick="rotateImage()" id="button">Rotate Image</button>
  </div>
</body>
Listing 8-1

Animation Listener HTML

html, body {
  padding: 36px;
  margin: 0;
}
@keyframes rotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.rotate { animation: rotate ease-in-out 500ms 1; }
.image { text-align: center; }
img { max-width: 100%; }
button {
  display: block;
  margin: 1rem auto;
  padding: 1rem;
  width: 25%;
}
Listing 8-2

Animation Listener CSS

var image, button;
(function() {
  'use strict';
  image = document.getElementById('image');
  image.addEventListener('animationend', reEnableButton);
  button = document.getElementById('button');
})();
function reEnableButton() {
  button.disabled = false;
  image.classList.remove('rotate');
}
function rotateImage() {
  image.classList.add('rotate');
  button.disabled = true;
}
Listing 8-3

Animation Listener JavaScript

Figure 8-3

Animation Listener Output

The button triggers the JavaScript to disable the button and add a class of rotate which makes the image spin once. Because on page load, we set up the JavaScript to listen for animation’s ending, once the animation is terminated, we can reset the page. The button is reenabled, and the rotate class is removed. In this example, even though we are manipulating the CSS with JavaScript, the CSS classes are still defined and maintained in the CSS file, and therefore, inheritance and cascading are not altered or affected.

Component-Based Architecture

When using component-based architecture, it becomes very important not only to theme your application to your application’s brand/specification, but also the UI library itself. Each library will have various levels of themability and intricacies in terms of ease, as well as what is actually possible to style. When selecting a UI library, understanding the customizability and how themable a library or framework is can save you from a lot of headaches down the road. Encapsulation  – restricting the component CSS to the component itself – allows for writing CSS that only applies to a particular component and not to the rest of the application. If a UI library’s components has very strict encapsulation and few theming options, it will be incredibly difficult to style.

There are many different libraries and frameworks that create or use components. Each has slightly different implementations. We are not going to look at all of them. When looking at how modern-day JavaScript frameworks create and interact with components, most either use web components or emulate them. Worth noting is that especially if the component is emulated, it will behave slightly differently than what I will be describing in the following text. Angular has an emulated model and continues to support an equivalent shadow piercing combinator (::ng-deep) but can be set to use web components instead, or set to have no encapsulation at all. In React, it depends on how the CSS was set up in the project. The options also run the gamut. Understanding exactly how much encapsulation your framework provides will help make better decisions as to how to structure your CSS.

Libraries and Frameworks

By definition, libraries are collections of declarations that the application will use. A framework is an abstraction and provides basic functionality, or a skeleton for the application. A framework may contain one or more libraries.

UI libraries such as jQuery UI3 or Angular Material4 provide a series of components or widgets that can be added to an application. They come ready made with styles and functionality. To customize their looks, they need to be themed. Theming can either be done via tools, which spit out the necessary CSS such as themerollers, or be done more manually following guidelines. Either way, the themability and therefore customizability of the said library will vary. The variability is a direct result of how the components are constructed and how easy the author has made it for element to be customized. Further customization beyond what the theme will allow can often prove quite difficult and result in using extremely specific selectors, such as the use of !important. It is therefore very important, when considering libraries, to look into what theming capabilities it has as well as how easy it is going to be to customize so its elements can match your application.

The architecture of the library itself may also affect how it is used and in some cases may provide for multiple approaches. Bootstrap5 is interesting because its structure allows for two radically different implementations, each with their downfalls and benefits.

The first, and probably most common implementation, is importing both the CSS and JavaScript directly in the page, from a local source, via CDN, or using a package manager such as NPM, NuGet, or RubyGems. The framework in its entirety is available and being applied. This means that a number of classes already have styles added to them and are ready to use on the web site. Some components, like modals, have functionality that relies on JavaScript associated with them. These will also be readily available.

The drawbacks of this approach lie in three places:
  1. 1.

    Naming is no longer semantic.

     
  2. 2.

    The styles are essentially being controlled by the HTML.

     
  3. 3.

    Everything is imported in its entirety even if it’s not being used.

     
Consider a static web site with three pages using the same basic layout for all three of its pages. The layout contains one main section and an aside to the right of the main (see Listings 8-4 and 8-5 and Figure 8-4).
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Bootstrap</title>
  <meta charset="UTF-8">
  <!-- bootstrap -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body class="container">
  <h1>Food</h1>
  <div class="row">
    <main class="col-md-9">
      <div class="jumbotron">
        <h2>Yum</h2>
        <p>Gummi bears chocolate bar powder brownie… </p>
        <button class="btn btn-warning">Call To Action</button>
      </div>
      <h2 id="cupcakes">Cupcakes</h2>
      <p>Chocolate chocolate bar tart cookie chocolate… </p>
      <a class="btn btn-light">Read More</a>
    </main>
    <aside id="bacon" class="col-md-3">
      <h2>Bacon</h2>
      <p>Bacon ipsum dolor amet pork loin chicken ham… </p>
      <p>Jowl spare ribs turkey cupim, pork chop sirloin… </p>
      <a class="btn btn-light">Read More</a>
    </aside>
  </div>
  <!-- Bootstrap scripts -->
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
Listing 8-4

Bootstrap HTML

html, body {
  padding: 36px;
  margin: 0;
}
Listing 8-5

Bootstrap CSS

Figure 8-4

Bootstrap Output

The benefit of this approach is its simplicity. Either with a custom theme, or just using defaults such as in the earlier example, it is very fast to get up and running. A number of basic styles already exist and can be used to just create a layout. The big downfall is in the maintainability of the code. Keeping consistency becomes very difficult if there are multiple call to action buttons across multiple pages.
<button class="btn btn-warning">Call To Action</button>

If the btn or the btn-warning class is updated, all buttons that include this generic class across the application will be updated, whether a call to action or not. The class gives no indication of what it might be used for, or worse, such as in this example, it is being used because of its color, rather than for a warning. The only other option is to go find all the call to actions in the application and update their class name.

Rather than the style sheet controlling how elements look, the style is now tightly bound to the HTML. This is also true of the layout, wanting to change the aside to taking a third of the page rather than a quarter would involve going to each page, and updating the HTML.

The other option is to take advantage of the available Sass mixins that Bootstrap makes available (see Listings 8-6 and 8-7).
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Bootstrap</title>
  <meta charset="UTF-8">
  <!-- Application CSS -->
  <link rel="stylesheet" href="./styles.css">
</head>
<body>
  <h1>Food</h1>
  <div class="container">
    <main>
      <div class="call-to-action">
        <h2>Yum</h2>
        <p>Gummi bears chocolate bar powder brownie… </p>
        <button>Call To Action</button>
      </div>
      <h2>Cupcakes</h2>
      <p>Chocolate chocolate bar tart cookie chocolate… </p>
      <a class="read-more">Read More</a>
    </main>
    <aside>
      <h2>Bacon</h2>
      <p>Bacon ipsum dolor amet pork loin chicken ham pancetta… </p>
      <a class="read-more">Read More</a>
    </aside>
  </div>
</body>
</html>
Listing 8-6

Bootstrap Mixins HTML

@import './node_modules/bootstrap/scss/functions';
@import './node_modules/bootstrap/scss/variables';
@import './node_modules/bootstrap/scss/mixins';
@import './node_modules/bootstrap/scss/jumbotron';
@import './node_modules/bootstrap/scss/buttons';
html {
  padding: 36px;
}
body {
  padding: 36px 15px;
  margin: 0 auto;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
  font-weight: 400;
  line-height: 1.5;
  color: #212529;
  box-sizing: border-box;
}
h1, h2 {
  margin-top: 0;
  margin-bottom: .5rem;
  font-weight: 500;
  line-height: 1.2;
}
h1 { font-size: 2.5rem ;}
h2 { font-size: 2rem ;}
.container {
  box-sizing: border-box;
  @include make-row(15px);
  & > * {
    box-sizing: border-box;
    @include make-col-ready(1rem);
  }
}
button { @extend .btn }
.call-to-action {
  box-sizing: border-box;
  @extend .jumbotron;
  button {
    @include button-variant($yellow, $yellow);
  }
}
a.read-more {
  @extend .btn;
  @include button-variant($gray-100, $gray-100);
}
@media (min-width: 756px) {
  body {  max-width: 540px; }
}
@media (min-width: 768px) {
  body {  max-width: 720px; }
  main { @include make-col(9); }
  aside { @include make-col(3) }
}
@media (min-width: 992px) {
  body {  max-width: 960px; }
}
@media (min-width: 1200px) {
  body {  max-width: 1140px; }
}
Listing 8-7

Bootstrap Mixins SCSS

This approach is not going to be as easy to get up and running with. Since it uses SCSS, it will require the ability to process SCSS into CSS. Knowledge of SCSS and of what is available for mixins in the framework is also required. Once past the setup and learning curve, however, we get some great benefits. Because we are now assigning the styles to classes via @include and @extend instead of applying generic class names to elements in the HTML, we know our elements will look the same on all the pages. Elements across the entire application can also be updated from one place rather than searching the site for all instances of a particular concept. Lastly, only the parts of Bootstrap I am using are being imported which reduces page weight.

!Important

Whenever trying to theme a component library or tweaking the styles from a CSS framework, specificity can sometimes be challenging to wrangle, as the library or framework may already be using selectors that are quite specific; therefore, it may be tempting to use !important. Here be Dragons!

Although there are situations where there truly is no other choice, or important truly is the lesser evil, these instances are few and far between.

The use of !important increases the precedence of a declaration making it very difficult to overide or to include in a normal cascade. No longer can you target a more specific selector to change the style of the element. You now need another more specific important. This vicious cycle makes code incredibly difficult to debug and maintain and even harder to expand.

So when overriding styles, take care that if you do use !important, it is done sparingly and with intent rather than frustration.

Knowing the architecture of the library selected, and its capabilities, will help make informed decisions as to how to structure your code for better long-term maintainability and performance.

Web Components

In the Document Object Model (DOM) , custom elements can be created and encapsulated by being attached to the DOM using Shadow DOM. Shadow DOM is a subtree of DOM elements that can be attached to the rendering document composed of a shadow host, shadow root, and shadow tree (see Figure 8-5).
Figure 8-5

Shadow DOM

Components created using this technique are fully encapsulated, and the author has complete control as to what the consumer will be able to style vs. not because everything within the Shadow DOM is akin to a black box from the perspective of the parent page or component. For a short while, we were able to ignore the encapsulation by using shadow piercing combinators such as >>> or ::deep, but these have been deprecated or removed from most browsers in favor of the upcoming CSS Shadow Parts 6 specification currently being refined. Even after this new specification is implemented, however, the author of the component will still control what users will be able to fiddle with; specificity, !important, and shadow piercing combinations will continue to fail to edit styles that the component author did not open to being altered.

Architecturally speaking, the benefit of web components allows for styled web components that can be dropped into any UI without worry of being altered by the parent application’s CSS. See Listings 8-8 to 8-11 and Figure 8-6.
<html lang="en">
<head>
  <title>Web Components</title>
  <link rel="stylesheet" href="./styles.css">
  <meta charset="utf-8">
</head>
<body>
  <h1>Components</h1>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed neque semper ante mattis tempor. Morbi volutpat ex ante, eget vulputate ligula pulvinar gravida.</p>
  <p></p>
  <section class="components">
    <my-card title="Coffee">
      <slot name="content">
        <p>Aged cortado, carajillo saucer wings aftertaste...</p>
      </slot>
    </my-card>
    <my-card title="Cats" class="dark">
      <slot name="content">
        <p>Chew master's slippers I is not fat, I is fluffy...</p>
      </slot>
    </my-card>
  </section>
  <div class="actions">
    <button type="button">More Components -></button>
  </div>
  <script src="./script.js"></script>
</body>
</html>
Listing 8-8

Web Component HTML

const actionButtonEvent = new CustomEvent('actions', {
  bubbles: true ,
  detail: { text: 'ok button' }
});
class MyCardComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    const card = document.createElement('div');
    card.setAttribute('class', 'card');
    const titleText = this.getAttribute('title');
    if (titleText) {
      const title = document.createElement('h2');
      title.innerText = titleText;
      card.appendChild(title);
    }
    const slot = document.createElement('slot');
    card.appendChild(slot);
    const actions = document.createElement('div');
    actions.setAttribute('class', 'actions');
    const button = document.createElement('button');
    button.innerText = 'OK';
    button.setAttribute('type', 'button');
    button.addEventListener('click', () => {
      this.dispatchEvent(actionButtonEvent);
    });
    actions.appendChild(button);
    card.appendChild(actions);
    const style = document.createElement('style');
    style.textContent = `@import './card.css'`;
    card.appendChild(style);
    shadow.appendChild(card);
  }
}
(function() {
  'use strict';
  customElements.define('my-card', MyCardComponent);
  document.querySelector('my-card').addEventListener('actions', e => console.log('outer actions event', e));
})();
Listing 8-9

Web Component JavaScript (script.js)

:host {
  font-family: sans-serif;
  --primary: mediumvioletred;
  --background: white;
  --text: #242529;
  --buttonText: white;
}
:host(.dark) {
  --background: #242529;
  --text: white;
}
.card {
  background: var(--background);
  color: var(--text);
  box-shadow: 1px 1px 1px var(--primary), 0px 0px 2px lightgrey;
  padding: 1rem;
}
h2 { margin: 0 1rem 0 0; }
.actions {
  text-align: right;
  margin-top: 1rem;
}
button {
  background: var(--primary);
  color: var(--buttonText);
  font-family: sans-serif;
  padding: .5rem 1.5rem;
  border: none;
}
Listing 8-10

Web Component CSS (card.css)

html, body {
  background: #fafafa;
  padding: 36px;
  margin: 0;
}
section.components {
  display: flex;
  margin: 0 -1rem;
}
my-card { margin: 1rem; }
.actions { text-align: center; }
button {
  background: rgb(187, 255, 120);
  border: none;
  border-radius: 1rem;
  box-shadow: 1px 1px 1px 1px gray;
  margin-top: 2rem;
  padding: .5rem 3rem;
}
.dark {
  font-family: monospace;
  font-size: 1.0625rem;
}
p, h2 { font-variant: small-caps; }
Listing 8-11

Web Component Page CSS (styles.css)

Figure 8-6

Web Component Output

Notice how the button styles do not interact with each other even though styles are being applied directly to the button element in both style sheets. This is powerful because it means that components and applications can be built independently and used with each other without the fear of naming collisions. Some things do transfer through, however. The component, through the :host and lesser supported :host-context selectors, can have awareness of its context. In this case, the component is specifically looking to see whether its host has a class of dark. If it does, then it is styled differently. However, these are preplanned styles from the author. Should another class name be passed to this specific component, it would continue to display its defaults, seen on the left of Figure 8-7.
Figure 8-7

Node Tree Including Slots

Some styles do bleed into the component. Elements in the slot or classes assigned directly to the host (the custom tag) are subject to both the component styles and page styles. Notice the paragraph tags in Figure 8-6; they take styles from both the parent and the component as described below.

Left component:
Browser default (serif) -> :host (sans-serif)
Right component:
Browser default (serif) -> :host (sans-serif) -> .dark (monospace)

:host is more specific than the browser defaults, and in turn, .dark is more specific than :host.

So why did the header also become monospace, when the buttons were unaffected? By adding a font-family of sans-serif to .dark, we essentially set monospace as the default font-family on the :host, that is as far as we can pierce into the component. The buttons have their own typeface specified inside the component CSS which is overriding the :host default and were therefore unaffected. Further attempts to reach into elements inside the component via specificity, such as doing .dark button { ... } even if no styles are set for that property, will not work.

The reason we were able to style the paragraph tags, and will continue to be able to do whatever we want with them, is because they are in a slot. The contents of the slot are actually being controlled by the parent. Looking at the DOM tree, one can see that the slot content lives outside of the shadow tree as seen in Figure 8-7.

The slot inside the shadow tree is nothing more than a placeholder. The content of the slot is in a sibling node of the shadow-root, and not in the shadow-tree itself. It is therefore not encapsulated like the rest of the component and can be styled like any other element on the page. Because it is a child of the host, however, styles assigned to the shadow host will cascade normally to those elements.

Styling Applications That Use Web Components

When creating an application that uses components, it is easy to start thinking only in terms of small reusable items and to lose sight of the greater picture. Consistency of typeface, colors, button styling, and so on across the application is something I think we can all agree is a good thing. However, if we are rewriting those styles in every component, we are setting ourselves up for discrepancies and a maintainability nightmare. How tightly encapsulated the components are affects the approach.

Regardless of encapsulation, however, starting with a file in which some reusable values are set to semantic, easily readable variables to be imported in all of the places helps in two ways: The same file is used everywhere, so consistency is gained, and because those values it contains are also being maintained in one place, should the primary brand color switch from blue to purple, one is not stuck looking for every instance of the color in the application. When using a precompiler such as Sass or Less, this is also a great place to set up some mixins. For more information about precompilers, see Chapter 7. Listing 8-12 shows an excerpt of what such a file might look like.
/∗ brand colors ∗/
--primary: #8A4F7D;
--accent: #88A096;
--border-color: #DDDDDD;
--link-color: var(--accent);
--background: #FAFAFA;
--font-family: sans-serif;
--box-shadow: 1px 1px 1px var(--primary), 0px 0px 2px lightgrey;
/∗ breakpoints ∗/
--small: 500px;
--medium: 800px;
--large: 1200px;
...
Listing 8-12

Sample Variable File

If the application still has a base style sheet whose styles are applied in and out of all components, it is a great place to put defaults such as what a link should look like and behave like on hover and focus, what headers should look like, and what are the application’s base font and colors. This is a great place to set up your theme. (see Listing 8-13).
@import 'variable.css';
html, body {
  Background: var(--primary);
  padding: 0;
  margin: 0;
  font-family: var(--font-family);
  color: black
}
h1, h2, h3, h4, h5, h6 {
  color: var(--primary);
}
a:link,
a:visited {
  color: var(--accent);
}
a:hover,
a:focus {
  text-decoration: underline;
}
...
Listing 8-13

Sample Theme File

Once these two files are set up, components should only need to worry about layout and exceptions, things that are specific to that component and nothing else. A great gauge of a style or set of styles belonging in one of these files would be if you find yourself copying and pasting the same thing over and over again. If this is the case, it is time to consider whether these styles need to be imported or set as defaults somewhere.

If the components are tightly encapsulated and a theme file that cascades styling throughout the application is not possible, variables that can be imported become really critical. Importing an entire theme file into every component will just bloat the application because even though maintained in one place, it will essentially be copied in each component. Possibilities here include the use of preprocessors in order to create mixins, or breaking the theme file up into smaller chunks, buttons, tables, links, and so on so that only the needed portions get imported.

Summary

In this chapter we covered the common interaction mechanisms between CSS and JS to show how JS interacts with (and sometimes interferes with) our style sheets. We also looked at how the architecture of the libraries we use can affect how we structure our code. In the next chapter we will dive into various architectural best practices along with specific CSS architectural patterns, showing their strengths and weaknesses.