Vital Parts for Writing Great CSS
by Nicklas EnvallThe goal of this article is to decrease CSS anxiety by giving information about things that'll most likely cause frustration in your encounters with CSS - if not known. The sections will expose you to things that exist, and you can then look more into them on your own.
Table of contents:
- CSS Selectors
- Box model
- Positions & Stacking Context
- CSS Effects: Transitions, Transform, Animations
- How to write Clean CSS
- CSS Preprocessors
CSS Selectors
It all starts with you being able to target which elements you want to style. CSS Selectors select elements and add a style to them. There are five basic selectors, while three of them are the most used (id, class, and tag).
1. The Tag Selector
The tag selector (also known as type selector) targets elements by tag name. Examples of tags are, div
, p
, and h1
. Its specificity is 0:0:1
.
h1 { color: red; } <!-- match: gets color red --> <h1></h1> <!-- no match --> <div> <!-- match --> <h1></h1> <!-- no match --> <p></p> </div>
2. The Class Selector
The class selector targets elements based on the value of the elements' class attribute. Its specificity is 0:1:0
. In CSS, you annotate a class by putting a dot (.
) in front of its name like, .nameGoesHere
.
.box { … } <!-- match --> <div class="box"></div> <!-- no match --> <div class="circle"></div> <!-- match --> <p class="box"></p>
3. The ID Selector
The id selector targets elements based on the value of the elements' id attribute. Its specificity is 1:0:0
. You annotate an id selector by putting a hashtag (#
) in front of its name, like #idGoesHere
.
#dog { … } <!-- match --> <div id="dog"></div> <!-- no match --> <div id="cat"></div> <!-- match → <p id="dog"></p>
4. The Attribute Selector
The attribute selector is similar to the class selector but instead works with any attribute. The attribute selector has a specificity of 0:1:0
.
[value="open"] { … } <!-- match --> <div value="open"></div> <!-- no match --> <div value="close"></div> <!-- no match --> <div class="open"></div>
5. The Universal Selector
The universal selector targets all elements and has a specificity of 0:0:0
. You annotate a universal selector with an asterisk (*
).
* { … } <!-- match --> <div></div> <!-- match --> <h1></h1> <!-- match --> <p></p>
What is Specificity?
Specificity determines which style gets applied based on how specific the rule is. For example, we can have multiple selectors with different values for color
target the same element. Which color the element gets depends on the specificity of the selectors.
The selector with the highest specificity wins.
A simplified way of looking at it is ID > CLASS > TAG
. Here's an example where the h1
tag will get a font size of 30px:
<!DOCTYPE html> <html> <head> <style> h1 { font-size: 10px; } /* 0:0:1 */ .className { font-size: 20px; } /* 0:1:0 */ .className .className2 { font-size: 25px; } /* 0:2:0 */ #myId { font-size: 30px; } /* 1:0:0 */ </style> </head> <body> <h1 id="myId" class="className className2">hello</h1> </body> </html>
Pseudo Selectors
There are two types of pseudo-selectors, which are pseudo-class and pseudo-element. We can distinguish the two by the fact that one uses one semicolon, and the other uses two. Pseudo-class uses one semicolon (selector:pseudo-class
) while pseudo-element uses two (selector::pseudo-element
). But what separates them more than that?
Pseudo-classes defines a specific state of an element, allowing us to apply style to that particular state. Pseudo-classes come in different types, like dynamic (:hover
, :focus
, :visited
) and structural (:first-child
, :first-of-type
, :last-child
, :nth-child
). We also have functional pseudo-classes like :nth-child(n)
.
Pseudo-elements are elements that we create with our CSS code. This type of keyword allows us to create elements at a certain part of the selected element(s) that would not be purely possible in the document tree alone. The element that a pseudo-element is connected to is referred to as its' originating element (originating-element::pseudo-element
). Two well-known examples of pseudo-elements are ::after
and ::before
.
Side note: You can also use CSS combinators to forge a relationship between selectors. The combinator is put between selectors to combine them.
Box Model
The box model is a wrapper around an element. The CSS below results in the image shown below as well:
.box { width: 10px; padding: 10px; border: 10px solid black; }
Take a close look, and you'll see that the element's "true width" is 30px. The reason for this is that when you set height, or width, it by default only affects the content area (the blue most inner part). Not knowing this usually confuses developers trying to arrange elements or create a gutter.
Imagine that you want to have two div elements beside each other, but end up with something like this:
<style> body { border: 1px black solid; height: 100px; } .box { height: 50px; width: 50%; padding: 2px; float: left; } </style> <body> <div class="box" style="background: blue;"></div> <div class="box" style="background: green;"></div> </body>
As we see both boxes have a width of 50%
, which together is 100%
. But the padding value is 2px
which causes the second div to be pushed on a new line. The padding also adds heights which causes it to overflow the parent body
(the green div is outside the borders).
What we are witnessing is the default behaviour of the box model. Luckily we can change the box model's behaviour by using the box-sizing
property. The default value is content-box
, by changing it to border-box
we make the properties width
and height
include padding
and border
. This means that increasing the border or padding would make the inner content more narrow. The image below is the result of adding box-sizing: border-box;
to our box class.
A common thing to do is to set border-box
as the global default. But also adding box-sizing: inherit
with universal selectors to ensure that we can override it, for example, if we use a third party library, then those elements might not be designed with border-box.
:root { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; }
Vertically Collapsed Margins
Collapsed margins occur when the top and bottom margins between elements are merged and the biggest margin size becomes the gutter. For example, imagine that all people need 3 meters of personal space, that would mean that two persons can be 3 meters from each other. They do not have to be 6 meters from each other. We can prevent collapsing by using overflow: auto
, border
, padding
, or flexbox. Lastly, remember that horizontal margins don't collapse.
Relative units
Relative units are based on another length unit and commonly used for padding and font sizes. So it's important to know how these work when working with the box model. The opposite of a relative unit is a static unit (also known as absolute units), which the unit px
is. em
, rem
, and %
, on the other hand, are all relative units. em
, for example, is relative to its element’s font size or the element's inherited font size. Let's look at two examples:
-
An element with a font size of 16px and padding of 2em would have a padding of 32px. This is because
16 * 2 = 32
. -
An element with a font size of 3em would multiply the amount of
px
of its parent’s font size three times. So, if parent div has 20px, child div has 3em, then20 * 3 = 60
.
Then we also have rem
, which is similar to em
but is instead only relative to the root element, which is good for nesting elements and responsive design.
Flows & Positions & Stacking
Elements are laid out based on the document flow (normal flow). That means all elements are painted top to bottom. Sometimes your design requires disrupts in the flow. Also, in some cases, you will even want to extract specific elements from the flow to position them elsewhere. How do we extract them from the flow? Well, our elements' default position is static
. All values except static
will surely take the element out of the document flow. Elements that are not static
are positioned elements. The most common positions are:
- fixed position: is relative to the viewport.
- absolute position: is relative to the closest positioned ancestor element.
- relative position: is relative to its own position. You'll often use this position to establish a containing block for a child that uses position absolute.
Stacking Context
The stacking context is like a stack faced toward the user, the element on top is the visible one if overlapping occurs. You can think of the stacking context as layers where elements belong to a layer. Nested elements appear on top of their parents, each nesting resulting in a higher level of the stack.
In more mathematical terms, it's the z-axis. With the property z-index
, we can manipulate the stack order. The z-index
value represents a layer on the z-axis. Applying z-index also creates a new stacking context group, of which the element that the z-index was applied to becomes the root of. But note that z-index only works on positioned elements.
The stacking context is created during browser rendering, after creating the rendering tree the browser paints the elements in the following order:
- Root element of stacking context
- Elements with positions and negative z-index.
- Non-positioned elements.
- Elements with positions and z-index auto.
- Elements with positions and positive z-index.
Layout Options
Back in the day, it was common to use Float
as the base for the layout for websites. Today we have more powerful layout options such as Flexbox
and Grid
.
Flexbox
Flexbox is a parent and child relationship. We activate the Flexbox layout by using display: flex
or display: inline-flex
on an element. By doing that, we turn that element into a flex-container and its children become flex-items. By default, all flex-items have the same height - even though their content would produce different heights.
<div class="flex-container"> <div class="flex-item">1</div> <div class="flex-item">2</div> <div class="flex-item">3</div> </div>
The flex-container provides us with a repertoire of properties. On the flex-container, we add certain values to CSS properties to decide how the flex items should be laid out. Some impact the ordering and orientation while some impact the alignment. This is because the flex-items (children of the flex-container) abide by the Flexbox rules instead of block, inline, and inline-block rules.
Grid
Grid is a parent and child relationship. We activate the Grid layout by using display: grid
or display: inline-grid
on an element. By doing that, we turn that element into a grid-container and its children become grid-items. Sounds kind of like Flexbox, right?
<div class="grid-container"> <div class="grid-item">1</div> <div class="grid-item">2</div> <div class="grid-item">3</div> <div class="grid-item">4</div> </div>
The core difference is that Grid is two dimensional (columns AND rows) while Flexbox is one dimensional (rows OR columns). Furthermore, Flexbox is dependent on the flex-items contents which greatly dictate the result, while the Grid is more dependent on its container. Then, of course, there are more differences between the two, but that is out of the scope of this article. Sometimes you'll find you want to use both at the same time, which is perfectly valid.
CSS Effects: Transitions, Transform, Animations
Effects grab users' attention, reinforce interactions, and smoothen sudden changes. But most of all, it gives us a way to communicate with users that text or voice cannot. There are three important properties when working with browser animation: transition
, animation
, and transform
.
Transitions
Transitions in CSS allows us to animate between a start and end state. A transition is triggered on a CSS property change. You can define your transitions with the famous transition
shorthand property, transition: [property] [duration] [timing-function] [delay];
. As you see, you can control the animation speed, duration, and even delay until transition should commence. Just make sure to use a unit like s
or ms
, 0
is not sufficient.
If you need to customize the timing functions you can either create your own time functions with cubic-bezier()
or use steps()
. If you open up your dev tools in a modern browser and inspect an element with the transition property you’ll see that you can open up an editor to manipulate the curve:
Note how the transition's timing function changed when I changed the curve. You can of course also just write the cubic-bezier()
function. You can also add multiple different transitions with a comma (,
). transition: background 1s linear, color 1s linear 1s
.
Animations
Animations are made up of two key things, @keyframes
and the animation
property. You define each keyframe and subsequently the keyframes combined create an animation.
@keyframes bounceFontSize { 0% { font-size: 1.5em; } 50% { font-size: 1em; } 100% { font-size: 1.5em; } }
The animation property is shorthand for animation: [name] [duration] [timing function] [iteration count]
. There are some things to keep in mind when working with animations, however. For example, when repeating an animation you need to ensure the ending values match the beginning values if you want this change to be smooth. You can use animation-fill-mode
if you need to add the animation style before it has begun.
Transforms
Transforms in CSS are a set of functions that lets us shape the appearance of our elements. Functions such as translate
, rotate
, skew
, and scale
lets us move and modify the appearance of our elements. With 3D transforms we can create the illusion of depth, we do so by using perspective
.
Transforms are important for performant transitions and animations. You should favor transform over explicit positioning or explicit sizing - because it’s more performant. Both opacity
and transform
costs less for the browser to handle than other CSS properties. It’s good to know if you want to achieve 60 FPS. 60 FPS is a subject that deserves its own article, so let’s continue.
How to write Clean CSS
Yes, even CSS needs to be clean and thought through. Bad CSS and a growing codebase will lead you down into a dark road of misery. So, how do we go about writing clean CSS, making it scalable and maintainable?
Well, luckily for us, a lot of developers before us have experimented and come up with different CSS methodologies. Let's very briefly look at some popular ones:
-
Object-oriented CSS (OOCSS): was created by Nicole Sullivan in 2008. OOCSS takes principles from object-oriented programming and applies it to CSS by treating elements as objects. Allowing utilization of the single responsibility principle, decoupling, and much more.
-
Block, Element, and Modifier (BEM): is created by Yandex. BEM gives us a strict naming convention to create independent blocks. The convention looks like,
block__element--modifier
. Examples are,block
,block__element
, andblock--modifier
. -
Scalable Modular Architecture for CSS (SMACSS): is created by Jonathan Snook. SMACSS recognizes 5 groupings for organizing CSS, which are code, base, layout, module, state, and theme.
They all embrace Modular CSS, which at its essence is about separating pieces of our UI interface into reusable parts. The methodologies push us into writing Modular CSS code. But why? Well, adopting a more modular approach lets us use proven principles such as "favor composition over inheritance" and "a class should have only one reason to change". So imagine that your CSS modules are not dependent on each other; you'll be able to change your CSS code without it unknowingly breaking somewhere else on the page.
We also have things like child elements and modifiers. Child elements depend upon the module and with it, we avoid things like nesting selectors. With modifiers, we can modify our module with extension without modifying the actual module, for example, .btn .bth--large
. Modifiers are a great example of the Open Closed Principle, which says "software entities should be open for extension, but closed for modification".
Let's look at an example of BEM in action:
<div class="movie movie--comedy"> <h2>Movie1</h2> <img class="movie__poster" src="/image.png"> <button class="btn btn--like">like</button> <button class="btn btn--dislike">dislike</button> </div> <div class="movie movie--drama"> <h2>Movie2</h2> <img class="movie__poster" src="/image.png"> <button class="btn btn--like">like</button> <button class="btn btn--dislike">dislike</button> </div>
/* movie.css */ .movie {} .movie--comedy {} .movie--drama {} .movie__poster {} /* btn.css */ .btn {} .btn--like {} .btn--dislike {}
I recommend taking a closer look at the methodologies to find out more. However, be aware that there's more to creating Clean CSS than what these methodologies cover. This section showed you that it's possible to write maintainable CSS, but we initially have to put some effort into learning how.
CSS Preprocessors
A preprocessor is a program that processes input data into output data that becomes an input for another program. So you should, of course, never edit the output itself, because the flow is:
Input -> Preprocessor -> Output/Input -> Program
.
In the context of CSS, we refer to them as CSS preprocessors, and there are numerous of them out there. But why do we use CSS preprocessors? Well, CSS itself is imperfect, it lacks many features one might want, which often leads to unmaintainable code, and duplications.
CSS preprocessors try to solve the imperfections of CSS. They often do so by being more similar to a "real programming language" by adding features and having its own syntax. Often by adding features like variables, loops, functions, and many other things. But just remember that preprocessors are not immune to bad code. Unfortunately, many people often sell preprocessors too verbosely. Yes, they can be great if used correctly - but it does not guarantee that your code will become cleaner. In some cases, your code can even get worse. Just adding a preprocessor to your project does not improve anything by default.
Nevertheless, these preprocessors are supersets of CSS, which means you'll still at its core be writing CSS but just with more features and a slightly different syntax. Most of the preprocessor often add the same core features, so now we'll look at the core features. Then we'll briefly look at the four most popular CSS preprocessors are SASS, LESS, Stylus, and PostCSS.
Core Features of CSS preprocessors
Most of the CSS preprocessors have similar or the same features. Now let’s go through the most common features (examples are written in SASS).
Variables
A good reason to use CSS-preprocessors is variables. Variables help with reusability, which will speed up the development process, and will in the end make your project more maintainable. CSS itself indeed has variables, but are not supported in IE and older versions of browsers.
$backgroundColor: blue; body { background: $backgroundColor; }
Outputs:
body { background: blue; }
Nesting
Nesting allows us to nest our selectors within selectors to create shortcuts. But, nesting can be heavily misused, I recommend reading this well-written article as to why that is.
.parent { .child { color: green; } }
Outputs:
.parent .child { color: green; }
Mixin & Extend
Mixins are similar to variables but instead lets us reuse blocks of styles. Mixins decrease duplication of shared styles. Often you can add arguments to your mixins, making them function-like.
Extensive and poor use of mixings will bloat your CSS. Luckily we also have extend
which will create comma-separated selectors.
@mixin block-of-style { color: green; background: green; } body { @include block-of-style; font-size: 12px; } h1 { @include block-of-style; } p { @extend h1; }
Is processed to:
body { color: green; background: green; font-size: 12px; } h1, p { color: green; background: green; }
Functions
Some preprocessors have built-in functions that are useful for things like color lightning/darkening or generating random numbers, etc. Some preprocessors also let you create your own functions:
@function add($termOne, $termTwo) { @return $termOne + $termTwo; } article { width: add(5, 10) + px; }
Outputs:
article { width: 15px; }
Famous CSS Preprocessors
-
Sass (Syntactically Awesome Style Sheets) was created in 2006, open-source, and coded in Ruby. It has two different syntaxes, SASS and SCSS. SASS uses an indented syntax similar to Python. SCSS syntax is more similar to CSS since it instead uses curly brackets. You either use the
.scss
or the.sass
extension to indicate which syntax you’re using. -
Less (Leaner Style Sheets) was inspired by Sass and provides similar features. It was written originally in Ruby but later rewritten in JavaScript.
-
Stylus is influenced by both Sass and Less which supports both indented syntax and regular CSS style.
-
PostCSS is a plugin-based preprocessor, so you have to add plugins to make it do something useful. A commonly used plugin is Autoprefixer which will add all the necessary vendor prefixes to your CSS. Then there’s a whole bunch more of plugins out there that you can add to minify, lint, etc.