3 Examples of the TAC Methodology In Action

In this post, we're going to build three components, each of which demonstrates how to follow different parts of the TAC CSS methodology.

If you haven't read about the TAC CSS methodology you can still follow along, but to fully understand some of the details in the examples below I'd suggest reading that.

Badge evolved...

This example demonstrates how a TAC CSS component can evolve to a JavaScript-enhanced Custom Element.

In the original TAC article, we were introduced to a Badge component. HTML didn't have a tag for us to use, so we defined a custom x-badge tag. We also defined a custom count attribute for setting the number and hiding/showing the Badge. It was all done with just CSS. The whole component was as simple as this:

/* badge.css */

/* Base styles */
x-badge {
  display: none;
  min-width: 1.25rem;
  height: 1.25rem;
  border-radius: 0.625rem;
  place-content: center;
  background-color: blue;
  color: white;
}

/* Count attribute */
x-badge[count]:before {
  content: attr(count);
  padding: 0 6px;
}

/* Display Badge when count is greater than zero */
x-badge[count]:not([count=""]):not([count="0"]) {
  display: inline-flex;
}

Now let's imagine we're using this Badge component to highlight the number of open orders in a food truck point-of-sale web app. User feedback suggests a notification sound for new orders would be helpful to cooks working under a noisy exhaust fan. This means we'll need to add JavaScript to our CSS component. The status quo has been to rebuild components with React because older CSS methodologies don't have a backward-compatible path for adding JavaScript features.

Unfortunately, nothing mainstream has worked well in the Web Components era because all existing CSS methodologies were based on classes, not tags, and React could not and would not be made to play nice with Web Components, hence the status quo.

Anyway, the notification bell. TAC was designed for such an evolution! We simply add a Custom Elements definition that plays a sound when count goes up:

// badge.js
customElements.define('x-badge', class extends HTMLElement {
  #soundEffect;

  constructor() {
    super();
    this.#soundEffect = new Audio('bell.wav');
  }

  static get observedAttributes() {
    return ['count'];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    // If new count is greater than the old count,
    // ring the notifcation bell.
    if (name === 'count' && Number(newVal) > Number(oldVal)) {
      this.#soundEffect.play();

      // Here's another fun option using the SpeechSynthesis API.
      // Define a "notify" attribute where, if present, plays 
      // the notification bell, but if set announces the text.
      // For example:
      // Plays bell
      // <x-badge count="<update_count>" notify>
      // Says: "New order"
      // <x-badge count="<update_count>" notify="New order">
      if (this.hasAttribute('notify')) {
        const text = this.getAttribute('notify');
        if (text) {
          const utterance = new SpeechSynthesisUtterance(text);
          window.speechSynthesis.speak(utterance);
        }
        else {
          this.#soundEffect.play();
        }
      }
    }
  }
})

Nothing else changes. An app's code doesn't need to change. Badge's styles remain untouched and working as before. There is nothing to install or compile or configure and there are zero proprietary libraries to manage going forward. It's the ideal evolutionary path for components.

If you're building a design system, this is even more remarkable as we have introduced JavaScript in a way that creates no conflicts with existing tech stacks. You could have a Django app, a Gatsby site, and a Vue SPA and all of them could seamlessly consume this Badge component. That's the power of web standards in your design system!

Customize HTML

This second example shows how to add custom attributes to native HTML elements.

The first step when following the TAC methodology is to identify a component tag, and the first place to look for tags is HTML itself.

There are over 100 HTML tags and we should be using every single one that matches our use cases. Reinventing tags or adding an abstraction layer between native tags and the developer contradicts the philosophy of TAC and doing so overlooks the value of vanilla HTML and wastes engineering resources (and can be very frustrating when you don't get direct access to native element features!).

So, leverage HTML. Take advantage of all HTML offers and then add to it when needed. Here's a simple example of doing that for buttons:

Native HTML button with a custom "ordinal" attribute
<style>
  /* Base styles for ordinal buttons */
  button[ord] { ... }

  /* Ordinal-specific styles */
  button[ord=primary] { ... }
  button[ord=secondary] { ... }
  button[ord=tertiary] { ... }
</style>

<button ord="primary">Primary button</button>
<button ord="secondary">Secondary button</button>
<button ord="tertiary">Tertiary button</button>

This hierarchy of buttons is an extremely common design pattern and it's common to name them using ordinal number words. In the past, that often looked like:

<button class="btn btn-primary">
or
<button class="button button--primary">

Nothing wrong with that, but there's a better way.

Because this ranking of buttons has meaning beyond generic styles it is considered a variation. TAC follows HTML's lead and implements variations as attributes, not classes. In this case, HTML offers nothing that can be leveraged for an ordinal attribute (aria-level is not good here), so we defined a custom enumerated attribute with the values “primary”, “secondary”, and “tertiary”.

But like the class-based examples above, TAC is still using just CSS. No JavaScript is interfering here with the native button element, so even if HTML were to add an ord attribute there is no harm done because the implementation is done with CSS only and developers are free to style elements based on their attributes.

Furthermore, this approach leaves the plain <button> element untouched because button elements are only styled in this case if the custom ord attribute is used.

This approach of looking really closely at HTML, using all its relevant tags and attributes and then adding more as needed is a very powerful method of scaling the UI layer in a way that remains surprisingly lightweight, framework-agnostic, and retains all the advantages of HTML (e.g. SEO, accessibility, easy to code and test, IDE-ready, debuggable, and portable). Here are a few more examples of things you can do to HTML:

Add 'none' to list's (deprecated) type attribute
<style>
  ul[type=none] { list-style: none }
</style>
<ul type="none">
  <li>No bullets</li>
</ul>

Define custom theme attribute for the details element
<style>
  details[theme=none] { ... }
  details[theme=toggle] { ... }
</style>
<details theme="none">
  <summary>Unstyled, but still interactive</summary>
  ...
</details>
<details theme="toggle">
  <summary>Has a custom expand & collapse style</summary>
  ...
</details>

Define a custom 'striped' boolean attribute for tables
<style>
  table[striped] tbody tr:nth-of-type(odd) { background-color: lightgray }
</style>
<table striped>
  <tbody>
    <tr>Background</tr>
    <tr>No background</tr>
    <tr>Background</tr>
    <tr>No background</tr>
  </tbody>
</table>

Sort arrows - nothing custom is needed for table sort arrows. All leverage!
<style>
  th[aria-sort] button { all: unset; cursor: pointer }
  th[aria-sort=ascending] button:after { content: '↑' }
  th[aria-sort=descending] button:after { content: '↓' }
</style>
<th aria-sort="ascending">
  <button aria-pressed="true" aria-label="Sort things"></button>
</th>

This can understandably make some developers nervous. Yes, it is possible, unlikely, but possible that the things you add to HTML could collide with future versions of HTML. Say WHATWG adds striped to table's spec next year. This does not automatically mean there will be issues - we're just setting a background color - but there could be so it's important to keep that in mind as you add customizations.

Creating Relationships

These examples show how relationships can be used to create more complex components.

Some components are really simple, like Badge, or custom buttons, or a striped table, but what about more complicated components? Something like a site header or a dialog? TAC can do it all. Let's start with the latter.

Dialog (aka Modal)

Let's use dialog to prove a quick point.

Here we have what traditionally would be a more complicated JavaScript-based component with lots of considerations for keyboard interaction, like pressing the Escape key, and z-index management, which is typically avoided by using JavaScript to awkwardly append modal to <body>. But since we're following TAC our guiding principle is to leverage what already exists and the first step is to identify a tag, starting with HTML tags first. And in this case, HTML has <dialog>.

The Dialog element is a great starting point. It offers us a lot of API to work with and has a lot of accessibility baked in. Leverage that!

The only thing we can't get from the native dialog is a close button. No problem. Define a slot for the basic dialog close button:

<style>
  dialog button[slot=close] { ... }
  dialog button[slot=close]:before { content: '×' }
</style>
<dialog>
  <button slot="close" aria-label="Close dialog"></button>
  <div>Dialog content...</div>
</dialog>

The exact styling of the close button is up to you. What's important is we’re leveraging HTML and creating a custom relationship between the dialog element and the button element with a slot attribute, i.e. a button[slot=close] child with a dialog parent. The application would bind a click handler to the close button and use dialog’s close() method to close it (or remove dialog's open attribute).

It's worth noting that in practice a basic close button (usually positioned in the top-right corner) is needful, but dialogs most often use an action/cancel pattern. Using our ordinal buttons from above, we could define a relationship between dialog and those as well.

Site header

In this example, we'll define a rigid element structure to ensure a required style and layout, but still leave it open for your content. The styles are going to be very basic because the concept is what we're learning here, not CSS.

As usual, we start with defining a tag and first look to HTML. In this case, we have <header>, which we will use, but because header elements can be used in many places and this one is unique, it needs to be a child of a custom tag, so let's define this basic relationship:

x-siteheader {
  padding: 12px;
  background-color: lightgray;
}

x-siteheader,
x-siteheader > header {
  display: flex;
  align-items: center;
}

The required markup is then:

<x-siteheader>
  <header>
  </header>
</x-siteheader>

Site headers need a logo, so let's build that into the component:

x-siteheader:before {
  content: '';
  width: 50px;
  height: 50px;
  background-image: url("logo.png");
}

Site headers have links, so let's define the structure and styles for those:

x-siteheader > header > nav > a {
  padding: 6px
}
x-siteheader > header > nav > a:hover {
  background-color: lightslategray;
}

Now the required markup is:

<x-siteheader>
  <header>
    <nav>
      <a href="home">Home</a>
      <a href="products">Products</a>      
      <a href="support">Support</a>
    </nav>
  </header>
</x-siteheader>

At this point, we have a custom HTML tag that defines our site header component, it has a built-in logo, and enforces a semantic structure for navigation. Let's take it further by adding a section for extra header elements.

x-siteheader-extras {
  display: flex;
  align-items: center;
  gap: 12px
}

x-siteheader-extras > button:hover,
x-siteheader-extras > a:hover {
  background-color: lightslategray;
}

The updated markup:

<x-siteheader>
  <header>
    <nav>
      <a href="home">Home</a>
      <a href="products">Products</a>      
      <a href="support">Support</a>
    </nav>
  </header>
  <x-siteheader-extras>
    <a href="cart">Cart <x-badge count="1"></x-badge></button>
    <a href="account">Account</a>
  </x-siteheader-extras>
</x-siteheader>

If or when your site header needs to be more sophisticated, ditch the required content and define a Custom Element and you can make it do anything:

Self-contained, handles everything, works everywhere!
<x-siteheader></x-siteheader>

That's 3 examples of how to apply the TAC CSS methodology. I hope it's inspired you to reconsider how powerful HTML is and how you can create true custom component APIs using just HTML, CSS, and JavaScript.