Build a dashboard with CUBE CSS

An in-depth guide to going from HTML all the way to a full styled banking dashboard, using the CUBE CSS methodology

CUBE CSS allows us to build seemingly complex front-ends with relative ease and to demonstrate that, in this tutorial, we’re going to build a banking dashboard which covers the whole methodology.

See what you are building

Before we start, it’s recommended that you check out the CUBE CSS docs or read the introduction post to get a feel for what we are working.

Starter files permalink

To focus this tutorial on the CSS only, we’re going to use a HTML boilerplate that I built earlier, with all of the necessary folders and assets, so the first thing you need to do is download those and set them up wherever you need to.

Download starter files

Now they are downloaded, your folder structure should look a bit like this:

Code language
Diff - don’t copy
index.html
fonts
Images
scss
├── blocks
└── utilities

Setting up Sass permalink

We’re going to use Sass for this tutorial along with a project I maintain called Gorko, which allows us to generate utility classes.

The first thing we’ll do is install Sass, so in your terminal, run the following:

Code language
bash
npm install sass

This installs canonical Sass, which is the version of Sass you should be using, if possible, because you get all of the latest features quickest. It’s also currently the only way to use the most modern CSS features within Sass.

You should have a package.json file in your starter files, so open that up and replace the "scripts" section with the following:

Code language
json
"scripts": {
"start": "npx sass scss/global.scss css/global.css --watch",
"build": "npx sass scss/global.scss css/global.css --style=compressed"
},

You should now be able to run npm start which will run Sass and then watch for any file changes.

Now that we have Sass installed, let’s install Gorko. In your terminal, run the following:

Code language
bash
npm install gorko

Let’s now add some config for Gorko. In your working folder, in the scss folder, create a new file called _config.scss and add the following to it:

Code language
scss
/**
* BASE SIZE
* All calculations are based on this. It’s recommended that
* you keep it at 1rem because that is the root font size. You
* can set it to whatever you like and whatever unit you like.
*/

$gorko-base-size: 1rem;

/**
* SIZE SCALE
* This is a Major Third scale that powers all the utilities that
* it is relevant for (font-size, margin, padding). All items are
* calcuated off the base size, so change that and cascade across
* your whole project.
*/

$gorko-size-scale: (
'300': $gorko-base-size * 0.8,
'400': $gorko-base-size,
'500': $gorko-base-size * 1.33,
'600': $gorko-base-size * 1.77,
'700': $gorko-base-size * 2.4
);

/**
* COLORS
* Colors are shared between backgrounds and text by default.
* You can also use them to power borders, fills or shadows, for example.
*/

$gorko-colors: (
'primary': #231651,
'secondary': #ff8484,
'secondary-shade': #ff5151,
'tertiary': #2c988c,
'tertiary-glare': #d6fff6,
'quaternary': #2374ab,
'light': #fafafa,
'light-shade': #eeeeee,
'grey': #c4c4c4
);

/**
* CORE CONFIG
* This powers everything from utility class generation to breakpoints
* to enabling/disabling pre-built components/utilities.
*/

$gorko-config: (
'bg': (
'items': $gorko-colors,
'output': 'standard',
'property': 'background'
),
'color': (
'items': $gorko-colors,
'output': 'standard',
'property': 'color'
),
'font': (
'items': (
'base': '"IBM Plex Sans", Helvetica, Arial, sans-serif',
'mono': '"IBM Plex Mono", Courier New, Courier, monospace'
),
'output': 'standard',
'property': 'font-family'
),
'gap-top': (
'items': $gorko-size-scale,
'output': 'standard',
'property': 'margin-top'
),
'pad-top': (
'items': $gorko-size-scale,
'output': 'standard',
'property': 'padding-top'
),
'text': (
'items': $gorko-size-scale,
'output': 'responsive',
'property': 'font-size'
),
'weight': (
'items': (
'medium': '500',
'bold': '700'
),
'output': 'standard',
'property': 'font-weight'
),
'breakpoints': (
'md': '(min-width: 48em)'
)
);

I won’t go into too much detail with this because the Gorko documentation does that already, but essentially, what we have here is some colour, fonts and a size scale (perfect fourth) which then informs various low-level utilities. This is the U in CUBE that we are working with.

Now that we have the config, let’s add a reset. We’re going to use this modern reset here. In your working folder, in the scss folder, create a new file called _reset.scss and add the following to it:

Code language
scss
// Modern CSS reset: https://github.com/hankchizljaw/modern-css-reset

/* Box sizing rules */
*,
*::before,
*::after
{
box-sizing: border-box;
}

/* Remove default padding */
ul[class],
ol[class]
{
padding: 0;
}

/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul[class],
ol[class],
figure,
blockquote,
dl,
dd
{
margin: 0;
}

/* Set core root defaults */
html {
scroll-behavior: smooth;
}

/* Set core body defaults */
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
}

/* Remove list styles on ul, ol elements with a class attribute */
ul[class],
ol[class]
{
list-style: none;
}

/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}

/* Make images easier to work with */
img,
picture
{
max-width: 100%;
display: block;
}

/* Natural flow and rhythm in articles by default */
article > * + * {
margin-top: 1em;
}

/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select
{
font: inherit;
}

/* Blur images when they have no alt attribute */
img:not([alt]) {
filter: blur(10px);
}

/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

We’ve got our base setup now, so let’s start building out our global styles. In your working folder, in the scss folder, create a new file called global.scss and add the following to it:

Code language
scss
// First up: config
@import 'config';

// Next: pull in gorko for design tokens
@import '../node_modules/gorko/gorko.scss';

// Pull in modern reset
@import 'reset';

We’re pulling in our config, which informs Gorko. Then, we pull in our reset. In your output (css/global.css), you should now see some reset styles and utility classes.

Global styles permalink

The base of CUBE CSS is global styles, so let’s add some of those now. Open up scss/global.scss and add the following to it:

Code language
scss
// Global CSS starts
body {
line-height: 1.5;
overflow-x: hidden;
padding-bottom: get-size('600');

@include apply-utility('weight', 'medium');
}

h1,
h2,
h3
{
line-height: 1.2;
}

h1 {
font-size: get-size('700');
}

h2,
h3
{
font-size: get-size('600');
}

a {
color: currentColor;
}

table {
border-collapse: collapse;
}

th {
text-align: left;
}

:focus {
outline: 2px dotted;
outline-offset: 0.25rem;
}

The design of this UI is pretty simple, so there’s not much to do here, but as you can see, our focus is element selectors, where key elements, such as headings, get some rules set. We also cover global focus styles to make sure each focusable element gets a consistent treatment.

Utilities and tokens permalink

With the globals in place, let’s start adding some core utilities and design tokens to our HTML. Most of the tokens are already implemented in the HTML because this would be a hell of a boring tutorial if we added them all manually, but to give you a feel for how things work, we’ll add some key ones now.

First of all, run your site locally and take a look. It should look a bit like this:

The site with mostly default styles, but typography looks neater

As you can see, with global styles and most of the tokens implemented, it doesn’t look half-bad at all. This is the magic of CUBE CSS: we can do a lot with very little.

Let’s add some more tokens. Open up index.html and on line 11 there should be the opening <body> tag. Replace it with the following:

Code language
HTML
<body class="bg-light color-primary font-base"></body>

That’s globally setting some colour tokens and the base font as the main font. Let’s continue this process by moving to line 12 with the <header role="banner"> element. Replace it with the following:

Code language
HTML
<header role="banner" class="[ site-head ] [ bg-tertiary-glare ]"></header>

We’ve added a site-head block, which we will come back to soon, but the main thing here is we’ve set its background colour with a utility.

Lastly, skip down to line 116. You should see the following: <section class="[ summary ] [ flow radius ]">. Replace that with the following:

Code language
HTML
<section class="[ summary ] [ flow radius ] [ bg-primary color-light ]"></section>

Now, when you reload your local version of this page, it should look like this:

The site now looks much better with design tokens implemented that affect colour and type

Pretty cool, right? So much of the design implementation is already done. Now we’ve got to do the other parts of CUBE: composition, blocks and exceptions.

Composition styles permalink

Right, we’ve done quite a lot already. Now it’s time to look at the bigger picture with composition styles. We’ll start with some high-level layouts.

Wrapper

The wrapper gives us a consistent max-width container with a bit of gutter. This is destined to be a utility because remember: a utility does one job and does it well.

Create a new file called scss/utilities/_wrapper.scss and add the following to it:

Code language
scss
.wrapper {
max-width: 75rem;
margin-left: auto;
margin-right: auto;
padding: 0 get-size('500');
}

The get-size function comes from Gorko and allows us to grab a size ratio value based on the key.

Flow

Next up is flow. This is a super tiny utility that adds margin to child sibling elements, using a lobotomised owl selector. This means that we get tonnes of flexibility, because any element will be affected (unless they are display: inline) and we don’t have to add daft CSS that targets last children to remove spacing.

Create a new file called scss/utilities/_flow.scss and add the following to it:

Code language
scss
.flow > * + * {
margin-top: var(--flow-space, 1rem);
}

We first look for a --flow-space Custom Property and if that’s defined, it gets used. By default, using a fallback, we tell each child element to space itself 1rem from its sibling. The great thing about using a Custom Property is that we can control spacing in context without touching this utility again. Those Custom Properties are affected by the cascade too.

We use a more comprehensive version of this utility in Every Layout, where it’s called The Stack.

Splitter

This utility gives us our main responsive layout, which is two elements pushed away from each other at larger viewports.

Create a new file called scss/utilities/_splitter.scss and add the following to it:

Code language
scss
.splitter {
> :last-child {
margin-top: get-size('500');
}

@include media-query('md') {
display: flex;

> * {
flex-grow: 1;
}

> :last-child {
margin-top: 0;
margin-left: get-size('500');
min-width: 22rem;
}
}
}

Being called splitter, this utility assumes it is dealing with two elements, so it works that way. This is shown right at the start of the utility when the last child has margin added to it.

The rest of this utility adds flex at larger viewports, then flips the spacing.

Visually hidden

Create a new file called scss/utilities/_visually-hidden.scss and add the following to it:

Code language
scss
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

This utility does what it says on the tin: it visually hides stuff. Using this utility instead of using display: none means that screen reader users can still access the content. In our project, this mainly affects the table headers.

Radius

Last up in these utilities is radius. This one isn’t technically a compositional rule, but while we’re in here, we might as well add it.

Create a new file called scss/utilities/_radius.scss and add the following to it:

Code language
scss
.radius {
border-radius: 0.5rem;
}

If there was ever a single-purpose utility, it would be this one!

Wiring up the utilities and composition styles permalink

Open up scss/global.scss and add the following:

Code language
scss
// Utilities
@import 'utilities/flow';
@import 'utilities/radius';
@import 'utilities/splitter';
@import 'utilities/wrapper';
@import 'utilities/visually-hidden';

Here, we are importing all of the CSS we have just written. If you reload your local version now, it should look like this:

The site now looks much better with design tokens implemented that affect colour and type

Adding our blocks permalink

Now we’ve handled global styles, design tokens, single purpose utilities and composition styles, we naturally move on to blocks. We’ll also get into exceptions at this point too.

Site header

This is the main header of the site, but it’s a super tiny block. Create a new file called scss/blocks/_site-head.scss and add the following to it:

Code language
scss
.site-head {
padding: 0.8rem 0;

&__inner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}

// Right margin prevents any collisions with the title
h1 {
margin: 0.5rem 1rem 0.5rem 0;
}
}

The main theme of this block is to create an auto-wrapping flex layout. We’ve only got two elements: a heading and a user block—which we’ll build next—that should never collide with each other. We guarantee this by building a force-field around our <h1> with margin.

User

While we’re in the site head, let’s build the user block. Create a new file called scss/blocks/_user.scss and add the following to it:

Code language
scss
.user {
display: inline-grid;
align-items: center;
grid-template-columns: max-content 50px;
grid-gap: get-size('300');

img {
border-radius: 100%;
}
}

Firstly, we use inline-grid because we want this block to only be as big as the content inside it. For that same reason, we use max-content in the grid-template-columns declaration. Lastly, we again, directly target the HTML <img> element and apply our 100% radius.

Key header

This is the element that introduces our transactions and summary. It provides a label for that data and also some key actions. It’s already got the splitter on it, so in true CUBE CSS fashion, we are using this block to create more contextual specificity.

Create a new file called scss/blocks/_key-header.scss and add the following to it:

Code language
scss
.key-header {
align-items: flex-end;

> :last-child {
display: flex;
flex-wrap: wrap;
gap: get-size('300');

> * {
flex-shrink: 0;
margin: 0.2rem 0 0 0.2rem;
}
}

@include media-query('md') {
> :last-child {
justify-content: flex-end;
}
}
}

This is mostly self-explanatory because the main theme is specifying alignment and justification.

The key area to focus on, though, is the :last-child, which is a group of buttons. We use a wrapping flex container, like in the site-head, but we use gap to space the items. A progressive enhancement pro tip follows as we add a tiny bit of vertical and horizontal margin to the elements. This is our fallback for if gap isn’t supported. Because it’s such a tiny amount of margin, it barely makes a scratch on the overall layout too. Happy days.

Button

It wouldn’t be a UI without a button, so let’s get them out of the way. Create a new file called scss/blocks/_button.scss and add the following to it:

Code language
scss
.button {
@extend .radius;

font: inherit;
display: inline-block;
line-height: 1;
text-decoration: none;
border: 1px solid get-color('secondary');
background: get-color('secondary');
padding: 0.6rem 1.5rem;
position: relative;

@include apply-utility('weight', 'bold');

&[data-variant='ghost'] {
border-color: currentColor;
background: transparent;
}

&:focus {
outline-offset: -0.4rem;
outline: 1px solid;
}

&:hover {
background: get-color('primary');
border-color: get-color('primary');
color: get-color('light');
}

&:active {
transform: scale(0.95);
}
}

This is a pretty generic button. In our context, both buttons are links, but we should always style a button to be a real <button> too. This is why we use font: inherit, to level the playing field by inheriting the base font rules. This rule itself makes the concept of using a <div> for a button completely ridiculous. There’s a pile of other reasons too.

You’ll also spot that we’re using a couple of Gorko functions and mixins to apply some of our tokens. The reason we do it in the block is because it’s common to have a lot of buttons. If each one is styled with utility classes, it gets out of hand really quickly. This is also why we @extend .radius.

Finally, top marks if you spotted an exception. This one: &[data-variant='ghost'], sets specific styles for a “ghost button”, which has no background and instead, has a visible border.

Just before we leave this block, there’s a little trick to make it look “squishy” when pressed (:active), which you can read more about here.

Summary

If you remember, we applied some tokens to this element early in the tutorial. Now it’s time to set up some specific block styles. Create a new file called scss/blocks/_summary.scss and add the following to it:

Code language
scss
.summary {
padding: get-size('500') get-size('500') get-size('600') get-size('500');
line-height: 1.1;

dl,
dt
{
--flow-space: #{get-size('700')};
}

dd {
--flow-space: #{get-size('300')};
}
}

The main thing we are doing here is controlling --flow-space by applying size ratio values. Because this Custom Property value is set in the block, it will only affect these elements. Magic.

Table group

These are our transaction tables. This block features several tables, headed with the date of the transactions. Create a new file called scss/blocks/_table-group.scss and add the following:

Code language
scss
.table-group {
border: 1px solid get-color('grey');
overflow-x: auto;
-webkit-overflow-scrolling: touch;

h3 {
--flow-space: #{get-size('600')};
}

table {
--flow-space: 0.2rem;

width: 100%;
min-width: 30rem;
}

td,
h3
{
padding: 0.5rem 1rem;
}

tr:first-child {
border-top: 1px solid get-color('grey');
}

tr:nth-child(odd) td {
background: get-color('light-shade');
}

td:nth-child(3) {
text-align: right;
}
}

We apply the visual border styles at the highest level. Then, we hide overflow because we want our tables to have a minimum width. You can mess around with responsive tables, but I think that’s a lot of effort for not much return. Instead, if you hide overflow and set a min-width, the data in the tables remains as intended and the user can swipe to see it.

The rest of the block is setting a “striped” effect by targeting odd rows and aligning the last column to the right, because they are numeric values.

Pill

Ok, the last block is here and it’s a good ol’ pill (hello, Bootstrap). Create a new file called scss/blocks/_pill.scss and add the following to it:

Code language
scss
.pill {
display: inline-block;
padding: 0.3rem 0.35rem;
font-size: get-size('400');
text-decoration: none;
line-height: 1;
white-space: nowrap;
text-align: center;

// Apply a default background colour if no token set
&:not([class*='bg-']) {
background: get-color('grey');
}

// Capitalize only in english
[lang*='en'] & {
text-transform: capitalize;
}
}

There’s two tricks in here. The first is if there’s no bg- utility on there, we set the grey colour by default. The second is that we want to capitalise the text, but only if this is English content. We do that by only adding the CSS if an element with an en language attribute is its parent. Handy, right?

Wiring up the blocks

We’ve got all of our blocks, so let’s completely transform the page by wiring them up. Open up scss/global.css and at the bottom of the file, add the following:

Code language
scss
// Blocks
@import 'blocks/button';
@import 'blocks/key-header';
@import 'blocks/pill';
@import 'blocks/site-head';
@import 'blocks/summary';
@import 'blocks/table-group';
@import 'blocks/user';

We are done! Now, when you reload your browser, it should look like this:

The final site looking nice and tidy

Wrapping up permalink

I hope that this tutorial has helped certain aspects of CUBE CSS to make even more sense for you. It’s hard to visualise how a methodology works without getting stuck into something proper with it.

You can download a zip of the final version here or even go ahead and check out a git repository of it.

If this is your first intro to CUBE CSS, you should probably head over to the documentation or read this high-level overview of the methodology.

Until next time, take it easy 👋