This is a personal collection of guidelines to architect a web project using CSS. This is a conclusion I've come to after trying to disband from TailwindCSS. This may be prone to change as I've only really divorced from TailwindCSS a month ago, but these are my discoveries.

This will probably be a living document depending on how lazy I am.

This also doesn't have a conclusion, because, um, yea.

Why?

This is a framework that is not targeted at smaller websites, or something like blogs, or fun websites. A lot of the rules that work there make life miserable when you have to add more shit.

This is for larger web applications in which you might have a lot of components, and you don't want to deal with Bullshit.

Layers

With the @layer at-rule, we can declare in which order which rules take precedence. I take inspiration of these layers and their usage from the ITCSS (Inverted Triangle Cascading Style Sheets) system.

In my projects, I now set up my base style sheet like the following:

@layer normalize, theme, global, component, util;

@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap') layer(theme);
/* This is based off of the TailwindCSS preflight. */
@import url('./preflight.css') layer(normalize);

@layer theme {
  :root {
    color-scheme: light dark;

    --color-page-background: #EEEEEE;
  }
}

@layer global {
  body {
    font-family: Inter, sans-serif;
    background-color: var(--color-page-background);
  }
}

@layer component {
  .card {
    .card__header {}
    .card--bordered {}
  }
}

@layer util {
  .__hidden {
    display: none;
  }
}

normalize

The normalize layer is where I load a CSS reset or normalization using import.

In my own projects, I use a CSS reset based off of TailwindCSS's preflight.

tailwindcss/packages/tailwindcss/preflight.css at d0a1bd655bfcc51818d2ae064eddef14f4983f67 · tailwindlabs/tailwindcss
A utility-first CSS framework for rapid UI development. - tailwindlabs/tailwindcss
https://github.com/tailwindlabs/tailwindcss/blob/d0a1bd655bfcc51818d2ae064eddef14f4983f67/packages/tailwindcss/preflight.css#L1

I use this because I often have to stick to design guides and I try to use semantic HTML so that my interfaces stay accessible. I do not want to deal with the cascade and apply my own resets when I want to use a ul element for something that has the purpose of a list but doesn't have the immediate presentation of how lists are displayed by browsers by default.

theme

@layer theme {
  :root {
    color-scheme: light dark;

    --color-page-background: #EEEEEE;
  }
}

I use the theme layer for two purposes:

  • I use it to set global configurations for things like variables. For example, --color-page-background in that previous example.

  • I use it to load external libraries into.

The first purpose probably makes sense, but the second reason probably seems confusing. My mental model is that I'm loading the configuration of styling for external resources, like fonts, or how other applications are configured.

@import url('https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css') layer(theme);

In the future, I may expand this to include a library layer specifically for libraries like tom-select, and whether to include that before or after the normalize is something I'll decide as I come across edge cases.

global

@layer global {
  body {
    font-family: Inter, sans-serif;
    background-color: var(--color-page-background);
  }
}

I use the global layer for 2 purposes:

  • I use it to configure incredibly broad elements that cascade throughout the whole application, such as configuring the font from the body element.

  • I use it to configure styles from other third party libraries, such as overriding the styles from tom-select.

As an aside, this is why I generally try to avoid element selectors.

component

@layer component {
  .card {
    .card__header {}
    .card--bordered {}
  }
}

This is pretty simple. Define components (blocks) using BEM in this layer.

BEM — Block Element Modifier
BEM — Block Element Modifier is a methodology, that helps you to achieve reusable components and code sharing in the front-end.
https://getbem.com/

If your block is meant to be used with a specific element, I usually put a comment with the intended element to be used, but it isn't something to be subscribed to.

/*ul*/.messages {}

util

@layer util {
  .__hidden {
    display: none;
  }
}

This layer is for one-off utilities to apply to elements that should act as exceptions. I would recommend basically never using these, but they can be helpful if you want to apply a class really quickly to change its behaviour that don't really make sense to apply as states.

Use Semantic Elements

Use semantic elements for defining the structure and flow of document, such as ul and li.

The intention for how you present should come from things like classes, presentation shouldn't be inferred by the structure.

I avoid custom elements personally because it's a lot easier to use elements that are already made for describing the data that I'm holding rather than making custom elements that describe how it will be presented.

This makes it easier to make your interface accessible.

I use these add-ons to check quickly how the structure of my site is and how accessible it is:

axe DevTools® - Web Accessibility Testing – Get this Extension for 🦊 Firefox (en-CA)
Download axe DevTools® - Web Accessibility Testing for Firefox. Add accessibility auditing to the Firefox Developer Tools
https://addons.mozilla.org/en-CA/firefox/addon/axe-devtools/
WAVE Accessibility Extension – Get this Extension for 🦊 Firefox (en-CA)
Download WAVE Accessibility Extension for Firefox. Evaluate web accessibility within the Firefox browser. When activated, the WAVE extension injects icons and indicators into your page to give feedback about accessibility and to facilitate manual evaluation.
https://addons.mozilla.org/en-CA/firefox/addon/wave-accessibility-tool/

Also, as an aside, this page is quite useful as well:

ARIA in HTML
This specification defines the authoring rules (author conformance requirements) for the use of [[[wai-aria-1.2]]] and [[[dpub-aria-1.0]]] attributes on [[HTML]] elements. This specification's primary objective is to define requirements for use with conformance checking tools used by authors (i.e., web developers). These requirements will aid authors in their development of web content, including custom interfaces and widgets, which make use of ARIA to complement or extend the features of the host language [[HTML]].
https://w3c.github.io/html-aria/

Use SCSS

SCSS still provides a lot of useful features such as mix-ins. Make mix-ins for things like breakpoints and so you DRY.

Making life harder just so you can use Vanilla Features? You can do that if you want, but I'd rather not.

@mixin desktop {
  @media screen and (min-width: 768px) {
    @content;
  }
}

@layer component {
  .something {
    @include desktop {
      /* apply shit to desktop here */
    }
  }
}