Andrija KapetanovićAndrija Kapetanović
Andrija Kapetanović (dark mode)Andrija Kapetanović (dark mode)

Getting the Fundamentals Right: CSS

38 minute read

When I started learning web development, I had a specific learning order in mind which was HTML, CSS, and JavaScript. I did spend some time learning HTML, but things didn't click right away, and I didn't see the importance of writing good HTML from the start, because I haven't experienced any of the problems yet.

But with CSS, web development for me started to get very interesting. It was almost magical to see all the things that you can do with CSS, style pages, lay out elements in a certain way, and make them come to life with animations. Of course, I was very far from being able to do that. There was a huge skills gap, a gap between where I wanted to be and where my current skills allowed me to be. It takes a lot of practice to be able to close it, and actually be able to build the things you envision.

If this article closes that gap at least a bit for you, I'll consider it an accomplishment. Before we dive into it, I'd like to emphasize the fact that every one of these chapters and subchapters that follow can be a blog post on its own, so my goal is to give you an overview of what I believe to be fundamentals for building a good mental model of CSS. And now let us begin.

What is CSS?

CSS stands for Cascading Style Sheets, meaning that it's a style sheet language used for styling of a document written in markup language, and it's one of the core technologies of the Web. As the name implies, there is an order of precedence in CSS, which is called the cascade.

The cascade is a set of rules that determine how browsers resolve conflicts between different CSS rules that target the same element. In other words, the "cascading" refers to the way CSS applies styles based on specificity and the order of rules. Specificity is the algorithm by which browsers decide which CSS declaration has precedence over others. The idea is that more specific rules will override more general ones. The algorithm can be represented as a three-column value of three categories:

  • id column, includes only ID selectors, e.g. #my-id, which adds 1-0-0 to the specificity value.
  • class column, includes class selectors, e.g. .my-class, attribute selectors [type="submit"] and pseudo-classes, such as :hover :nth-child(), etc. Each of these adds 0-1-0 to the value.
  • type column, includes type selectors, e.g. div, a, p, and pseudo-elements such as ::after, ::placeholder, and ::backdrop. Each of these adds 0-0-1 to the value.

We will discuss the basic syntax of CSS below, but just to illustrate the point of the specificity weight value, let's take a look at the following example:

<button class="form-btn" id="form-btn" type="submit">Submit</button>

<style>
button {
background-color: aquamarine;
color: white;
}

.form-btn {
background-color: blueviolet;
color: white;
}

[type="submit"] {
background-color: lightcoral;
color: navy;
}

#form-btn {
background-color: navy;
color: white;
}
</style>

The color schema that "wins" in this case is #form-btn because it has the specificity of 1-0-0. Let's break it down. The button selector has a weight of 0-0-1, the .form-btn selector has a weight of 0-1-0, the [type="submit"] selector has a weight of 0-1-0, and the #form-btn selector has a weight of 1-0-0. The #form-btn selector has the highest specificity, so it will override the other selectors.

Notice that the order of the rules in the CSS file doesn't matter in this case, but let's say we don't have the #form-btn selector in the CSS file, then the order of the rules would matter. Let's not forget the C in CSS, the cascade. The .form-btn selector, and the [type="submit"] selector have the same weight so the last rule that is defined in the CSS file would be the one that is applied. This is why it's important to have a consistent order of rules in your CSS file, and to have a consistent naming convention for your classes and IDs.

Behind the Scenes

To get a better understanding of how CSS works, we need to look behind the scenes of the browser rendering process. When a browser receives an HTML document, it goes through a series of steps to render the page as we went through in the Getting the Fundamentals Right: HTML.

The browser first parses the HTML document, converts HTML code into tokens, to create nodes and build the Document Object Model (DOM) tree, which represents the structure of the document. The browser then constructs a render tree, which is a visual representation of the DOM tree with style information applied. The render tree is used to determine the layout of the page and render the content on the screen. At the same time, the browser fetches and parses the CSS files linked to the HTML document. It also processes any inline styles and <style> elements within the HTML. Similar to HTML, the CSS is tokenized into individual components (selectors, properties, values). These tokens are used to construct the CSS Object Model (CSSOM), a tree-like structure that represents the CSS rules and their relationships. We can also define the model as a set of APIs that allow the manipulation of CSS via JavaScript. The browser matches CSS rules to the DOM nodes; it calculates the final styles for each node based on the CSS cascade, inheritance, and specificity rules. In other words, the browser combines the DOM tree and the CSSOM to create the Render Tree. The Render Tree contains only the nodes required to render the page. Using the Render Three the browser calculates the layout of each element. This process involves determining the size and position of the elements on the screen. This step is often referred to as "reflow".

Once the layout is calculated, the browser paints the pixels to the screen. This step involves filling in the colors, borders, shadows, and text. It's the process of converting the render tree into actual pixels on the screen.

In the final step, the browser may need to composite different layers together, especially for complex elements like those with opacity, transforms, or animations. This ensures that the rendered output is correct and optimized for performance.

All of this is a lot of work, and there's a lot of variables to be considered, so browsers use several techniques to optimize the rendering process, such as:

  • lazy loading, i.e., delaying the loading and rendering of elements that are not immediately visible.
  • CSS preprocessing, compiling and minifying CSS files to reduce file size and improve load times.
  • hardware acceleration, using the GPU for rendering complex visual effects and animations.
  • caching, storing CSS files and images to avoid repeated downloads on subsequent visits.

Basic Syntax

Now that we have some basic understanding of what CSS is and how it's processed, let's dive into the syntax of CSS. According to MDN, key CSS syntax includes:

  • rules
  • selectors
  • declarations
  • properties (including custom properties)
  • values (including shorthand values)
  • at-rules and descriptors

Rules

CSS rules are used to apply styles to HTML elements. A CSS rule consists of a selector and a declaration block.

p {
color: dodgerblue;
font-size: 16px;
}

In our example, p is the selector, selecting every paragraph of the document to which our styles are attached to, and the declaration block contains two CSS rules, setting the color property to dodgerblue and the font-size property to 16px.

Selectors

Selectors are used to target HTML elements to apply styles to them. There are different types of selectors, including element selectors, class selectors, ID selectors, and pseudo-selectors.

p {
}

.my-class {
}

#my-id {
}

input[type="text"] {
}

a:hover {
}

p::first-line {
}

In our example, we have an element selector, a class selector, an ID selector, an attribute selector, a pseudo-class selector, and a pseudo-element selector in that order.

Declarations

Declarations are used to set the style properties of the selected elements. A declaration consists of a property and a value, separated by a colon and ending with a semicolon. Declarations are contained within a declaration block.

p {
color: fuchsia;
font-size: 1.2rem;
}

Following that logic, here we are declaring two properties to a paragraph element, property color with the value fuchsia, and the property font-size with the value 1.2rem.

Properties (including custom properties)

Properties define what aspect of the element's style you are changing.

p {
color: dodgerblue;
font-size: 16px;
}

:root {
--brand-color: #45aaf2;
}

p {
color: var(--brand-color);
}

In the example above, we have two properties, color and font-size, and a custom property --brand-color defined on the :root pseudo-class. Custom properties are a powerful feature of CSS that allow you to define a value once and use it in multiple places.

Values (including shorthand values)

Values are assigned to properties and can be specific values, keywords, or functions. Shorthand values allow you to set multiple related properties at once.

p {
margin-top: 10px;
margin-right: 20px;
margin-bottom: 15px;
margin-left: 35px;
}

p {
margin: 10px 20px 15px 35px;
}

p {
margin: 10px 20px;
}

p {
margin: 10px 20px 0;
}

p {
margin-inline: 10px;
margin-block: 20px;
}

In the first declaration, we are explicity listing properties, and assigning values to each property while in the second declaration, we are using a shorthand value to set the margin of a paragraph element. The order with four values is top, right, bottom, and left. A great analogy to remember this order is the clock, starting from the top and going clockwise.

In the third declaration, we are using a shorthand value to set the margin of a paragraph element, but this time we are only providing two values, which will set the top and bottom margins to 10px, and the right and left margins to 20px.

In the fourth declaration we are using a shorthand value on the margin property, but this time we are only providing three values, which will set the top margin to 10px, the right and left margins to 20px, and the bottom margin to 0.

The last declaration allows us to explicitly define margin to the inline and block directions. We will go more into detail on inline and block direction concepts later on, but for now let's imagine our element being on an axis; x-axis is the inline direction, and y-axis is the block direction.

At-rules and descriptors

At-rules are special CSS statements that begin with an @ symbol and are used to apply styles conditionally or to import other stylesheets. Descriptors are properties used within at-rules.

@import url("styles.css");

@media (min-width: 768px) {
.title {
font-size: 2rem;
}
}

@container (min-width: 768px) {
.description {
font-size: 1.5rem;
}
}

@font-face {
font-family: "MyCustomFont";
src: url("mycustomfont.woff") format("woff2");
font-weight: bold;
font-style: normal;
}

@keyframes slidein {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}

.cta {
animation: slidein 3s;
}

There's a lot of interesting CSS at-rules, and you can check out a list of all of them on MDN docs.

The Box Model

After understanding the basic syntax of CSS, we can dive a bit deeper into some of the models that are used in CSS. One of the most important models in CSS is the box model. The box model is a fundamental concept in CSS that describes how elements are rendered on the web. According to the box model, every element on a web page is a rectangular box, and each box has four areas:

  1. content - the innermost part of the box where the actual content of the element (text,images, etc.) is displayed.
  2. padding - the space between the content and the border of the box. Padding adds extra space inside the element, around the content area.
  3. border - the border wraps around the padding and the content. It can be styled with different widths, colors, and styles.
  4. margin - the outermost layer that surrounds the border. The margin creates space between the element and its neighboring elements.

Margin

Border

Padding

Content

The Gift Box

Let's build up a mental model which will help us understand the box model. Imagine you are wrapping a gift.

  1. content: the gift itself, represents the actual content of the box. This is what you care about the most, and it's the core of the element.
  2. padding: the bubble wrap or tissue paper you use to protect the gift. This extra space ensures that the gift (content) doesn't get damaged a has bit of a cushion.
  3. border: the gift box that holds everything together. The box can have different styles, colors, and thicknesses, just like the border of an element.
  4. margin: the space you leave around the gift box when you place it on the table or under a Christmas tree to separate it from other presents. This space ensures that the box doesn't touch or overlap with other objects or boxes nearby.

Understanding the box model is a core mental model for understanding the way the digital space is distributed on a web page. It helps you control the layout and spacing of elements of your web page, and allows you to realize visually appealing designs, and resolve some inevitable layout issues.

The Box-Sizing Property

While we're on the topic of box model, I'd also like you discuss the box-sizing property, which is used to control how the width and height of an element are calculated.

By default, the box-sizing property is set to content-box, which means that the width and height of an element are calculated based on the content area only. This can sometimes lead to unexpected layout behavior, especially when padding and border are added to the element. To avoid potential issues, you can set the box-sizing property to border-box, which includes padding and border in the calculation of the width and height of the element.

That's why in most modern front-end frameworks, this property is set to border-box, establishing a new default. In regular framework-less JavaScript projects, a common tool to 'normalize' your CSS, and avoid such issues is to use normalize.css, a small CSS reset library which you import into your global CSS file, and it makes sure that useful default browser styles are preserved, while the unwanted ones are removed.

To let this concept sink in, let's go back to the analogy of the gift box. Let's imagine that we have two ways to measure its size. With the default, content-box, you measure only the space inside the box where the gift fits. If you then add padding (e.g., tissue paper) and a border (e.g., the box walls), the total outer size of the box increases which is strange, when you think about it. Imagine having a magic box which expands due to tissue paper we added in the box.

With border-box, you measure the outer size of the box including the walls and any padding inside. If you add more padding or a thicker border, the inner space for the gift gets smaller, but the outer dimensions stay the same. This is more aligned with our intuition how the physical world works. In CSS, either consciously or subconsciously, we project our understanding of the physical world to the digital sphere.

Flow Layout

Let's say you're creating a fresh new web page, and you want to add some text to it and you start with the <p> tag, add the content, and close it with </p>. Before applying any changes to the layout, this element will be displayed in Normal Flow or Flow Layout. It is the default layout behavior for HTML documents.

Elements are layed out in a block and inline direction. The block elements stack on top of each other depending on the Writing Mode of the HTML document. The most typical writing mode among many international writing modes, is left-to-right (Latin and Indic scripts). There's also right-to-left (Hebrew or Arabic scripts), bidirectional (mixture of left-to-right and right-to-left scripts), and vertical (Asian scripts). If we imagine a graph with two axes, the inline elements are laid out on the x-axis in the left-to-right writing mode, while the block direction is laid out on the y-axis.

Block-level elements take up the full width of the container and start on a new line. Examples include <div>, <p>, <h1>, <section>, <article>, etc. Inline elements, on the other hand, only take up as much width as necessary and do not start on a new line. Examples include <span>, <a>, <strong>, <em>, etc.

Now, in reference to our box model, it's important to note that margins, paddings, and borders affect the spacing and the size of elements within the flow layout. Margins can cause elements to be spaced apart, while padding and borders add to the size of the element's box without affecting the flow.

Margins have an additional quirky behavior, which sometimes leaves developers puzzled. When two vertical margins touch, they collapse into a single margin. It's called margin collapsing. To make this a bit more technical, vertical margins between adjacent block-level elements may collapse into a single margin whose size is the larges of the margins involved. This behavior can affect the spacing between elements.

Flow layout is simple and predictable, making it easy to understand and use for basic page structures. Because it's based on the order of elements in the HTML, it naturally adapts to different screen sizes and orientations, contributing to responsive design. While flow layout is the foundation, it can be limiting when you need more complex structures, so modern CSS provides more advanced layout techniques like Flexbox and grid that can be combined with normal flow to create complex and responsive designs.

Positioned Layout

Sometimes you might want to override the flow layout and position elements in a specific way. This is where the position property comes in. The position property is used to specify the type of positioning method used for an element. The position property can take the following values: static, relative, absolute, fixed, and sticky.

  1. Static position is the default position for all elements. Elements are displayed according to the normal flow of the document. If we try to override the position with values top, right, bottom, and left, they will have no effect.
  2. Relative position allows you to position an element relative to its normal position. It remains in the document flow, and its original space is preserved. You can use the top, right, bottom, and left properties to adjust the element's position from its normal position.
  3. Absolute position removes the element from the normal flow and positions it relative to its nearest positioned ancestor (an ancestor with a position value other than static). If no such ancestor exists, it is positioned relative to the initial containing block (usually the viewport).
  4. Fixed position also removes the element from the normal flow and positions it relative to the viewport and does not move when page is scrolled.
  5. Sticky position is a hybrid of relative and fixed positioning. The element toggles between the relative and fixed positioning depending on the user's scroll position. The element is treated as relative until it crosses a specified threshold, then it becomes fixed.

Each value of the position property except for the static value, which is the default, uses positioned layout on the element. Understanding CSS positioning is essential for creating complex layouts, controlling exact placement of elements on a page, and understanding why elements are displayed in a certain way. It will help you come into a code base, and identify why certain elements are positioned in a certain way, and how you can adjust them to achieve the desired layout.

Float Layout

There's also a property called float which allows elements to be taken out of the normal flow and aligned to left or right of their container, with subsequent content flowing around them.

The float property can take the values left, or right. A common footgun with float is that it can cause layout issues, because if needs to be cleared. Often developers forget to clear the float, which can cause elements to overlap or not be displayed correctly. To avoid these issues, you can use the clear property to clear the float and ensure that elements are displayed correctly. What this essentially does is that it controls the behavior of elements following floated elements, ensuring they are properly aligned within the flow.

If our element contains only floated elements, its height collapses to 0, and if we want it to always be able to resize, we can use the display property to flow-root. It's important to note that floating elements are less commonly used in modern layouts, now that we have more advanced layout techniques like Flexbox and Grid.

Layers

After going through different ways of overriding the flow layout, it's important to introduce the notion of layers in CSS. Layers refer to the different levels at which elements can be stacked and rendered on the web page. These layers determine the visual stacking order of elements and can be influenced by various CSS properties that we mentioned, like position, z-index, float, and stacking contexts.

A stacking context is an element that contains a set of layers, and elements within the same stacking context are ordered based on their z-index values, while different stacking contexts can be nested within each other. A new stacking context is formed by elements with certain properties, such as:

  • position values other than static with a z-index value.
  • elements with opacity less than 1.
  • elements with transform, filter, perspective, clip-path or mask properties.
  • elements with mix-blend-mode, isolation, will-change, contain, or contain-intrinsic-size properties.

Z-index and Stacking order

Another way to override a flow layout is to use the z-index property, which controls the stacking order of positioned elements, elements with a position value other than static. In other words, the z-index property specifies the stack level of an element within a stacking context. Higher z-index values are placed on top of elements with lower z-index values. The z-index property can take any integer value, positive or negative. The default stacking order, without any z-index applied, is:

  • background and borders of the element
  • descendant elements in the normal flow, positioned or unpositioned
  • floating elements - positioned elements with z-index set to auto
  • positioned elements with z-index set to a specific value

When elements overlap, the stacking order determines which element is displayed above the other. I've had many situations where z-index wasn't working as I expected, the elements would not stack on top of another despite the increase in z-index value. The reason for this is that z-index depends on the layout mode. The flow layout never implemented the z-index property. This means that if we don't change the layout mode of one of the children elements or the parent element, the z-index will not have any effect like in the example below.

<style>
h1 {
z-index: 10;
}

img {
z-index: 9;
width: 200px;
margin-top: -20px;
margin-left: 20px;
}
</style>

<section>
<h1>Heading</h1>
<img src="image.jpg" alt="Example image" />
</section>

Other layouts like positioned layout, flexbox layout, and grid layout implemented the z-index, so the stacking order will work as expected.

Top Layer

With the advent of new browser APIs such as dialog and popover, the concept of top layer has emerged, the king among layers. The top layer is a special conceptual layer that spans the entire viewport and is always displayed on top of other elements on the page. It is often used for modals, dialogs, popovers, and other elements that need to be displayed above the rest of the content.

The top layer is created using the position property with a value of fixed or absolute and a high z-index value. Elements in the Top Layer are rendered above all other content, including elements with the highest z-index values. They typically create an overlay effect, often a semi-transparent background to obscure the content underneath. In terms of accessibility, Top Layer manages focus and interaction, preventing interaction with elements outside of it until the Top Layer element is dismissed or closed.

The top layer is a powerful tool for creating interactive and engaging user interfaces, and it allows you to create elements that are always visible and accessible to the user.

Flexbox

Flexbox, also known as CSS flexible box layout, is a layout module designed to provide a more efficient way to distribute space and align items within a container, especially when dealing with complex or responsive layouts. Flexbox is closely related to the flow layout, which we discussed earlier, but it gives more flexibility and control over how elements are placed and aligned within a container.

In the default flow layout, block-level elements stack vertically from top to bottom, filling the available width of their container. Inline-level elements, on the other hand, flow horizontally from left to right, wrapping to the next line when there's not enough horizontal space. The flow layout is straightforward and predictable, but it has limitations when it comes to complex layouts, such as centering elements vertically, aligning items along a cross-axis, or creating equal-height columns.

Flexbox solves these problems by introducing a new layout model which is composed of a flex container and flex items. The flex container is the parent element that contains the flex items, and it uses the display: flex or display: inline-flex property to enable flexbox layout. The flex items are the children of the flex container, and they are laid out along the flex line, which can be either horizontal (the default on the web) or vertical (the default in React Native - mobile development), depending on the container's flex-direction. It's important to note that with flexbox, only the items within the flex container are being put into this flexible layout mode, while the container itself is still in flow layout mode, or whatever layout mode it was in (we can also nest flexbox containers within flexbox containers, or within grid containers).

So, in other words, the flex items in a flex container are laid out either on the main axis or on the cross axis, which is perpendicular to the main axis. The cross axis is vertical by default but changes if the main axis is set to vertical.

For aligning items along the main axis, we can use the justify-content property, and for aligning items along the cross axis, we can use the align-items property. We can also use the align-self property on individual flex items to override the align-items property on the container.

There are many excellent resources for explaining flexbox in great detail, and some of my favorites are the CSS Flexbox Layout Guide by Chris Coyier and An Interactive Guide to Flexbox by Josh Comeau.

Grid Layout

The CSS Grid Layout, commonly known as CSS Grid, is a two-dimensional layout system that allows you to create grid-based layouts with rows and columns. Unlike Flexbox which thrives in the one-dimensional layout system, either row-based or column-based, CSS Grid handles both rows and columns simultaneously, making it ideal for creating complex layouts.

As with flexbox, we have the concept of a grid container and grid items. Besides these two, we also have grid lines, grid tracks, and grid areas. Grid lines are the dividing lines between grid cells, they can be referenced by number, allowing precise placement of items. Grid tracks are the rows and columns created by the grid lines. Each row and column is a grid track. A grid area is a rectangular area made up of one or more grid cells. You can name grid areas for easier placement of items. This creates a nice visual representation of the layout of a component or a page as we can see in the example below.

.wrapper {
display: grid;
grid-template-columns: 250px 1fr 200px;
grid-template-rows: 5rem 1fr 6rem;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
}

CSS Grid reduces the need for complex and often nested structures, making the HTML markup cleaner and easier to manage. We can define grid areas directly within CSS, significantly simplifying the process of aligning elements on a page.

It's inherently responsive, and by using functions like minmax(), auto-fill, and auto-fit, we can create layouts that adapt seamlessly to different screen sizes. Media queries can be used in combination with grid properties to create breakpoints and adapt layouts at various resolutions. Grid makes it easy to align items within cells, rows and columns, and to control the spacing between them with the gap property. As opposed to some other layout algorithms, vertical and horizontal alignments are straightforward, eliminating the need for complex hacks and workarounds.

Again, like with Flexbox, there are many excellent resources for learning CSS Grid, and again, some of my favorites come from the same source as Flexbox. These are CSS Grid Layout Guide by Chris House and An Interactive Guide to Grid by Josh Comeau.

Typography

When we browse the web, the majority of the content that we consume is text. It's one of the core building blocks of the web. Typography is the art and technique of arranging type and text to make it readable, clear, and visually appealing to the reader. In essence, typography is the visual component of the written word, and it gives a certain 'feel' to a web site.

It's quite difficult to choose an appropriate font, or to combine fonts in a way that is visually appealing, and that's why typography is and should be considered an art form. I've often spent hours searching for the perfect font, or combination of fonts for my projects, finally finding it filled with glee, only to show it to someone and hear them say that something just feels 'off'.

Besides the importance of a certain 'feel' to a font, I believe it's necessary to have an understanding of how it's even displayed on the screen. The process of rendering the text on to the screen is a complex one, but to simplify it, we can break it down to two distinct processes which are called kerning and rasterization.

Kerning refers to the process of adjusting the space between individual characters to ensure that the spacing looks visually appealing and balanced. In typography, proper kerning improves the readability of text and enhances its overall visual presentation, making sure that certain letter combinations (like "AV" or "Wa") don't appear too spaced apart or too close together. This process helps to create smooth, proportional text.

In CSS, we have a property called font-kerning which controls whether kerning information, which is built into the font, should be used when rendering text.

p {
font-kerning: normal;
}

The font-kerning property can take the following values:

  • auto (default) - the browser will use kerning information from the font if available.
  • normal - forces the browser to use kerning information, if available.
  • none - disables kerning information, even if the font includes kerning data.

For fonts that include kerning pairs, kerning can enhance the readability of headers or large blocks of text. However, it's important to note that not all fonts have kerning data, and the effect may not be noticeable unless you're working with display fonts or larger text sizes.

Rasterization refers to the process of converting vector-based text and images (scalable and defined by mathematical coordinates) into a pixel-based (bitmap) format. This is important for screen rendering because screens display content as a grid of pixels. When typography is rendered on a screen, the letters (which may be vector shapes in the font file) are rasterized into a bitmap so they can be displayed. This process affects how smooth or jagged the text appears on the screen, particularly at smaller sizes or lower resolutions.

During rasterization, a technique called anti-aliasing is often used to smooth the edges of text, preventing jagged lines and making the text look more refined. This can also improve legibility at small sizes. In CSS, we have a property called font-smoothing which controls the smoothing of text on the screen.

p {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

As we can see in the example above, the property has vendor prefixes for WebKit (Safari and Chrome) and Mozilla (Firefox) browsers. This feature is non-standard at the time of writing, and it's not recommended for use in production, however, it's good to be aware of what it does.

Modern browsers and operating systems use subpixel rendering during rasterization to improve the sharpness of text. This process leverages the fact that each pixel on an LCD screen is made up of three subpixels (red, green and blue), and by adjusting the colors of these subpixels, the text can appear sharper than if each pixel was rendered in black and white. While you cannot directly control the rasterization process with CSS, some properties can influence how text is rendered on the screen, such as text-rendering and font-smoothing.

The text-rendering property provides hints to the browser about how to optimize the rendering of text.

body {
text-rendering: optimizeLegibility;
}
  • auto - the browser will determine the best rendering method based on the text and font properties.
  • optimizeSpeed - the browser will prioritize rendering speed over quality.
  • optimizeLegibility - the browser will prioritize text legibility and enable font features like kerning.
  • geometricPrecision - the browser will provide the most precise rendering but with a cost to performance.

The font-smoothing property (vendor-prefixed) allows control over anti-aliasing ( a technique for minimizing the distortion artifacts when representing a high-resolution image at a lower resolution).

p {
-webkit-font-smoothing: antialiased;
}
  • auto - default smoothing based on the browser or operating system.
  • none - disables anti-aliasing.
  • antialiased - smooths the edges of text by turning off subpixel rendering and using grayscale smoothing instead.

Kerning and rasterization are essential for ensuring that text looks good and is easy to read. Kerning is important when you're working with text on a higher, more conceptual level - you're concerned about how letters fit together and the readibility of the text. Rasterization is more concerned with the pixel-level rendering of that text on the screen, and affects how sharp, smooth, or legible the text look, particularly at different resolutions of font sizes.

Now, choosing the right font for your project is a crucial step in accomplishing the right look and feel. There are two main types of fonts: serif and sans-serif. Serif fonts have small lines or strokes attached to the ends of letters, while sans-serif fonts do not have these lines. Serif fonts are often associated with traditional, formal, or classic designs, while sans-serif fonts are considered more modern, clean, and easy to read. When choosing a font, consider the tone and style of your project, as well as the readability and legibility of the text. It's also important to consider the font size, line height, and spacing to ensure that the text is easy to read and visually appealing.

We can also pair fonts to create contrast and visual interest. When pairing fonts, we can use fonts that have similar characteristics or that complement each other. For example, pairing a serif font with a sans-serif font can create a nice contrast and balance in the design. We can also use different weights, styles, and sizes to create hierarchy and emphasis in the text. When pairing fonts, it's important to consider the overall design and the message you want to convey. You can experiment with different combinations to find the right balance and style for your project. Some excellent font pairing resources are fontjoy, and fontpair which can help you find your perfect pair.

Color

It's time to talk about color. Let's get one thing clear right off the bat: color science is difficult. Color is an essential aspect of web design and styling, allowing developers to define and manipulate the colors of text, backgrounds, borders and other elements. Besides the aesthetic aspect, proper color usage can also improve readability, accessibility, and user experience.

I'm not sure if I ever started a project without spending a lot of time on choosing the right color scheme. And when we add different modes into the mix, such as dark mode, things get complicated real fast. It has a significant impact on the overall look and feel of a website, and it influences the user's perception and experience.

In CSS, colors can be defined using a variety of methods and formats, and to begin with, it supports a set of basic color keywords, such as red, blue, green, black, white, and more. In fact, MDN provides a list of named colors, and for an even more extended list check out the CSS Color Module Level 3 W3 spec.

We can also use the currentColor keyword which refers to the current value of the color property, and can be used for borders, background, etc. This allows us to use the inherited color value on child elements.

<div style="color: red;">
<p style="border: 1px solid currentColor;">
This text will have a red border.
</p>
</div>

RGB stands for Red, Green, Blue, and the rgba function allows you to specify colors with an alpha channel for opacity of a color.

color: rgb(255, 0, 0); /* Red */
background-color: rgba(0, 255, 0, 0.5); /* Green with 50% opacity */

Hexadecimal colors are another common way to define colors in CSS. They are six-digit codes representing the combination of red, green, and blue values.

color: #ff0000; /* Red */
background-color: #00ff00; /* Green */
border-color: #0000ff; /* Blue */

HSL stands for Hue, Saturation, Lightness, and the hsl and hsla functions allow you to specify colors using these values. The last value of the latter function is the alpha channel for transparency. This syntax is handy for creating color schemes, because the user can better grasp what's being changed on each value without having to know the hexadecimal system, but be wary, with HSL you might not always get the color you expect as well discuss below.

color: hsl(0, 100%, 50%); /* Red */
background-color: hsla(120, 100%, 50%, 0.5); /* Green with 50% opacity */

Hue is changed with the first value, and is represented in degrees (deg), as a degree on the color wheel. Even though the color wheel has 360 degrees we can go beyond that value, since after making a full circle, we're just continuing on a new circle on the color wheel, e.g. 390 degrees is equal to 30 degrees.

On our color wheel, the distance from the axis corresponds to saturation, which refers to the intensity or purity of a color. It controls how vivid or muted a color appears. In simpler terms, it defines whether the color is closer to a primary color or more grayish.

Lightness determined how light or dark a color appears. It's essentially the brightness of the color, controlled independently from hue and saturation. Unfortunately, in HSL, lightness is meaningless. With RGB and HSL colors are not perceptually uniform.

HSL(250, 100%, 50%)
HSL(60, 100%, 50%)

In this example, you can see that lightness is only meaningful for a single color, and if we try to compare the lightness of these two colors, they are not perceptually equal.

Unfortunately, HSL's saturation has that same issue. According to HSL, the hue difference between these two pairs is the same.

HSL(30, 100%, 50%)
HSL(50, 100%, 50%)
HSL(230, 100%, 50%)
HSL(250, 100%, 50%)

However, there's a solution for this; a new syntax under the relatively new CIE Lab color space. Until the introduction of LCH, every CSS color was defined to be in the standard RGB (sRGB) color space. It allows us to get access to 50% more colors than with sRGB.

The goal of the CIE color space is to allow for perceptually consistent color. Lab is a human perception motivated color syntax, which means that it's oriented towards how human perceive lightness. It's syntax is not straightforward as with the hsl function; it expresses color as three values: L for perceptual lightness and a and b for the four unique colors of human vision: red, green, blue and yellow.

color: lab(29.2345% 39.3825 20.0664);

A more friendly syntax within the CIE color space is LCH which stands for Lightness, Chroma, Hue, and the lch function allows you to specify colors using these values, and we can categorize it as a hybrid between hsl and lab. Besides the lightness, which this new color space tries to fix, you get to specify the hue and chroma, which is easier to understand than the a and b values.

color: lch(50% 100 120); /* Red */

If you want to play around with LCH, there's a great LCH Color Picker built by Lea Verou. With the development of more advanced hardware, it's exciting to see that CSS is evolving to keep up with the new possibilities. The new color spaces are a great example of that.

CSS Variables

An excellent way to tie all our topics together is with CSS Variables, also known as CSS Custom Properties. Projects always have a set of recurring values, color, font, font-size, spacing, etc. and CSS Variables are the right tool for the job of building a theme, a styling system, and to avoid repetition and difficulties in maintaining our styles.

They are a powerful feature in CSS that allows us to define reusable values that can be applied across stylesheets. These variables enable a more efficient and maintainable approach to writing CSS, particularly when building design systems that require consistency across various elements (such as colors, typography, spacing, etc.). So, for example, when new developers comes into a project, they can easily understand the color scheme, typography, and spacing by looking at the variables defined at the top of the CSS file.

To define a CSS variable, we use the -- prefix followed by the variable name. They are similar to variables in programming languages, allowing to store values and reuse them throughout a project. Generally, we define CSS variables at the top of the CSS file within the html selector, however, we can use the :root pseudo-class which essentially represents the <html> element, it's identical to the html selector, but its specificity is higher.

:root {
--brand-color: #3498db;
--font-size-base: 16px;
--spacing-unit: 1rem;
}

.primary-button {
background-color: var(--brand-color);
font-size: var(--font-size-base);
padding: var(--spacing-unit);
}

In this example, we define three CSS variables: --brand-color, --font-size-base, and --spacing-unit. We then use these variables to set the background color, font size, and padding of the primary-button class. We use the var() function to access and apply the value of a custom property. By using CSS variables, we can easily update the values of these properties across the entire project by changing the variable values in one place. With the centralization of key values (e.g. brand colors, font sizes), we can update the design by changing the variable values in single place. This avoids the need to search and replace values throughout different stylesheets or files.

CSS variables also make it easy to switch between different themes and modes (e.g. light and dark mode) by simply updating the variables. We can define different sets of variables for each theme and apply them based on user preferences or other conditions. Unlike preprocessed variables in Sass or Less which are static and compiled at build time, CSS variables can be changed dynamically at runtime using JavaScript. This allows for more interactive and responsive designs. They also support inheritance and cascading, making it a powerful tool in complex systems.

Final Thoughts

CSS is becoming a more powerful and flexible language with each innovative feature. We can now create complex layouts and animations which were only possible with JavaScript. Understanding the fundamentals will help ease the overwhelm caused by the fast-paced of web development. After all, every new feature is built on that foundational knowledge.

2024 has been a big year for CSS, and UI development in general. For a comprehensive recap of the latest CSS features, and what's on the horizon, be sure to check out Una Kravets' Google I/O '24 talk. Exciting times are ahead, and I can't wait to see what the future holds for CSS.

Resources