CSS

Overview

With the styling power that CSS provides as a base technology, it is very easy and tempting to write styles that apply in one situation that should not in another. In line with our architectural goal of composition, we now target component styles using the Block Element Modifier (BEM) naming convention. While this is not foolproof because all styles still reside within the global namespace within a web browser, the BEM convention drastically reduces the likelihood of a style regression caused by naming clashes, specificity battles and the Cascade. We manipulate basic CSS by using SCSS, a superset of CSS that offers features that allow succinct authoring techniques while outputting selectors that appropriately target markup while also allowing extensibility by other developers.

Implementation

SCSS usage

As part of the Tui framework we made the decision to switch our CSS preprocessor over to SCSS. SCSS has become the leading CSS preprocessor and provides us with better build performance.

The existing CSS from the Basis theme has already been converted over to SCSS in the Legacy theme. The Ventura CSS sits on top of the existing legacy styles, but is predominantly CSS variable overrides from core Tui component style definitions.

Key differences between SCSS and LESS

SCSS is an imperative language while LESS is a declarative language - this impacts where and how variables should be defined. Previously with LESS, variables could be used before their declaration and if defined multiple times the last definition was used throughout, while with SCSS the variables must be defined before use.

Global variable
$red: #ff0000;
Chaining/static variables
$red: #ff0000 !default;
Creating a mixin
@mixin box-shadow($shadow) {
	box-shadow: $shadow;
}
Calling a mixin
@include box-shadow($shadow);

Block Element Modifier (BEM) methodology

We have introduced the BEM methodology as part of the Tui framework.

BEM is a popular CSS naming methodology. The key benefits of using this methodology are that it provides a consistent approach throughout the code base and helps you to manage both inheritance and specificity.

BEM provides elements with unique classes allowing CSS to be targeted at particular use cases. This essentially creates a limited scope where styles are applied, reducing the amount of styles inherited through the cascade. Elements having unique classes also reduces the need to have nested CSS selectors when targeting a particular element, which often leads to issues with specificity.

The following example shows how the BEM class structure has been applied to the checkbox component:

<div class="tui-checkbox tui-checkbox--large">
	<input class="tui-checkbox__input" id="uid-1" type="checkbox" value=""/>
	<label class="tui-checkbox__label" for="uid-1">OK</label>
</div>

For classes with multiple words we use the camelCase naming convention:

<div class="tui-checkboxGroup">
	<input class="tui-checkboxGroup__inputBox" id="uid-1" type="checkbox" value=""/>
	<label class="tui-checkboxGroup__label" for="uid-1">OK</label>
</div>

Blocks

A BEM block is a standalone entity that is meaningful on its own. They can be nested but semantically they remain equal. Within Tui we treat each of our Vue components as a block entity.

The block sets the class namespace for all of its inner elements which semantically ties them together. The block name should be descriptive enough to express what it is and unique enough to avoid name conflicts with other blocks. For multi-word block names we use camel case.

Checkbox block entity
<div class="tui-checkbox">...</div>

The Tui framework has a convention of prefixing each block with tui-, this should not be used by non-totara components or plug-ins. 

Elements

Each block has its own set of unique elements which cannot be used or styled by other blocks and have no stand-alone meaning. Their classes are prefixed with the block class followed by a double underscore.

<div class="tui-checkbox__input">
<div class="tui-checkbox__label">

Sub-elements further extend their parent element class with a dash prefix.

<div class="tui-checkbox__label-text">
<div class="tui-checkbox__label-icon">

Blocks with several layers of elements usually suggests that a subcomponent is more appropriate.

Modifiers

The modifier is a flag used to modify the appearance, state or behaviour of a block or element. Examples of modifiers include disabled, active, large, small, hasButton. The modifiers allow blocks to be customised while keeping the styles self-contained. Modifiers should be used instead of overwriting the styles from outside of the block with specificity.

Modifiers are prefixed with the block class followed by a double dash.

<div class="tui-checkbox tui-checkbox--large">
<div class="tui-checkbox tui-checkbox--disabled">

Block modifiers will usually be enabled or disabled by their parent components, within Vue we use standalone boolean props to manage this.

Block
  <div
    class="tui-checkbox"
    :class="{
      'tui-checkbox--large': large,
    }"
  >
Parent block
<Checkbox :large="true"/>

Variables

Within the Tui framework we have two types of style variables, SCSS variables and CSS variables.

CSS variables are the preferred approach going forward but there are a number of places where we are required to have static variables such as in CSS media queries.

In Totara 16 and earlier we only support root-level CSS variables to allow for full compatibility with IE11.

Global variables are fetched before the static and component CSS and are included from the following file:

client/component/tui/src/global_styles/_variables.scss

Static CSS

For any styles that can't be generated at a component level or for elements that aren't generated by components we have a static SCSS file. The styles in this file are included before any of the component styles.

client/component/tui/src/global_styles/static.scss

Importing SCSS

To import more .scss files into static.scss, use the system component-based frankenstyle import paths (for example @import 'theme_customtheme/oldstyles/totara/nav';), rather than relative importing, which is not supported at this time.

Code style

A number of minor coding style changes were introduced as part of the Tui frameworks.

SCSS uses 2 space indentation instead of 4 space and rule ordering was introduced for the CSS to keep the code more consistent. We enforce this with Prettier during the development and integration process.

Tips and known limitations

Style isolation

The Tui framework does aim at style isolation, however it does not make use of real isolation via the Shadow DOM. This particular feature was not initially used because we required support for IE11, which does not support the Shadow DOM. There is an alternative available within Vue SFCs, 'scoped styles', however we also have not adopted this approach which generates a unique identifier and applies that to selectors. The reason we have not adopted this approach is because of style inheritance across themes - if there is a unique identifier on a given selector then a child theme wanting to override that selector would need to also somehow know about the unique identifier.

Because we have not adopted Shadow DOM style encapsulation or scoped styles, all styles are still in the global scope. We rely on the BEM naming convention to greatly decrease the likelihood of selector clashes.

Because of our ongoing dependency on the Legacy theme, which styles parts of the UI such as the primary navigation and footer, we still have globally scoped Bootstrap and Legacy-based far-reaching styles battling for dominance with Tui framework specifics. We could not rely on the CSS value unset because that too is not supported in IE11, therefore, Tui components provide the overrides they need to operate in isolation.

SCSS functions and CSS Variables

As part of the switch to SCSS several of the existing methods are no longer supported. Any methods that manipulate a passed non-predefined value such as a CSS variable will no longer work, this includes the following: Darken(), Lighten(), mix() & button-variant().

Recommended reading