Skip to content

Instantly share code, notes, and snippets.

@ModulesUnraveled
Last active October 7, 2021 02:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ModulesUnraveled/9e1ec31e8a193fb23bdb9dc5ff74c2ef to your computer and use it in GitHub Desktop.
Save ModulesUnraveled/9e1ec31e8a193fb23bdb9dc5ff74c2ef to your computer and use it in GitHub Desktop.
Comparison of Tailwind example code, and how we'd write it at Four Kitchens

Tailwind vs. Four Kitchens Methodologies

Intro

For this post, I took a look at the card examples provided on the Tailwind website at https://v1.tailwindcss.com/components/cards.

In order to keeps things simple, I chose to focus on one example, the "Horizontal" card.

"Horizontal" example from tailwindcss.com

First of all, the name is poor because on mobile devices, it's stacked... That's why we believe a component's name should "describe it's purpose, rather than its appearance".

But on top of that, looking at the code, I'm even sure where the card styles actually start. As you can see in the screenshot, there's padding around the card... Looking at the code, there's no obvious way to know if that's coming from the card styles, or the component that's wrapping the card.

Tailwind Card Code

Tailwind's Code

Below is the card markup example from the Tailwind site.

<div class="max-w-sm w-full lg:max-w-full lg:flex">
  <div class="h-48 lg:h-auto lg:w-48 flex-none bg-cover rounded-t lg:rounded-t-none lg:rounded-l text-center overflow-hidden" style="background-image: url('/img/card-left.jpg')" title="Woman holding a mug">
  </div>
  <div class="border-r border-b border-l border-gray-400 lg:border-l-0 lg:border-t lg:border-gray-400 bg-white rounded-b lg:rounded-b-none lg:rounded-r p-4 flex flex-col justify-between leading-normal">
    <div class="mb-8">
      <p class="text-sm text-gray-600 flex items-center">
        <svg class="fill-current text-gray-500 w-3 h-3 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
          <path ... removed for brevity />
        </svg>
        Members only
      </p>
      <div class="text-gray-900 font-bold text-xl mb-2">Can coffee make you a better developer?</div>
      <p class="text-gray-700 text-base">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.</p>
    </div>
    <div class="flex items-center">
      <img class="w-10 h-10 rounded-full mr-4" src="/img/jonathan.jpg" alt="Avatar of Jonathan Reinink">
      <div class="text-sm">
        <p class="text-gray-900 leading-none">Jonathan Reinink</p>
        <p class="text-gray-600">Aug 18</p>
      </div>
    </div>
  </div>
</div>

Refactored using BEM

And here's what it would look like using our preferred methodology.

<div class="teaser-card">
  <div class="teaser-card__image" style="background-image: url('/img/card-left.jpg')" title="Woman holding a mug">
  </div>
  <div class="teaser-card__content">
    <div class="teaser-card__text">
      <p class="members-only-tag"> <!-- Notice, this isn't teaser-card__member-tag -->
        <svg class="members-only-tag__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
          <path ... removed for brevity />
        </svg>
        Members only
      </p>
      <h3 class="teaser-card__heading">Can coffee make you a better developer?</h3> <!-- Note I changed this from div to a heading tag. The actual heading level should be dynamic to account for context. -->
      <p class="teaser-card__snippet">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.</p>
    </div>
    <div class="author-teaser"> <!-- Again, note that this is not teaser-card__author-teaser -->
      <img class="author-teaser__image" src="/img/jonathan.jpg" alt="Avatar of Jonathan Reinink">
      <div class="author-teaser__content">
        <p class="author-teaser__name">Jonathan Reinink</p>
        <p class="author-teaser__date">Aug 18</p>
      </div>
    </div>
  </div>
</div>

Observations

Markup and File Size

First, you'll notice that there's a lot less markup in the BEM example. This doesn't necessarily mean there's less code in the codebase, because the stylesheet will be larger in order to accommodate the necessary styles. However, when we read the HTML, it's more clear what's going on, and when we look at the css, because it's componentized, and isolated, it would be similar to looking at the list of classnames above.

The end result as far as total file size (combined HTML and CSS) will likely be pretty similar, assuming everything is compressed with gzip. gzip is very good at compressing repeated strings (which can happen when you write similar css for multiple components.) (For example, in this example from pingdom they saw a 159.06 KB uncompressed CSS file reduce to 28.86 KB once compressed!)

Sub-components

In the BEM example, you'll also notice that the card component contains other components that aren't card-specific (members-only-tag and author-teaser.) These smaller components (in our example) are self-contained, and can be shared between any number of components (e.g. Card, News-item, Article, etc.) and will ALWAYS look consistent regardless of where they are implemented because their styles are captured in one place (the smaller component's stylesheet) rather than being re-styled in each implementation (e.g. the Card, the News-item, etc.) This helps prevent fractures in the design. e.g. in case you need to adjust the styles of the author-teaser, you do it in one place versus having to search the codebase for all examples of its usage... which would be much more difficult in the Tailwind example, because there's no bespoke class name to search for...

Contextual CSS

Another thing you'll notice is that in the Tailwind example, each element of the card is styled without regard to the styles of the elements around them, and each has to be styled manually to work with the ones surrounding it (like a flex container, and flex children). The alternative is something we do when appropriate, and that's to use pseudo selectors to select contextual items rather than specific items.

Before I show what I'm talking about, Tailwind does support a selection of pseudo selectors. You should be able to do everything with links and buttons (like :hover, :active, :focus etc. and then select, first, last, even, and odd children) but there is a specific set of selectors that are supported. And that doesn't always support what I'm about to show.

One shortfall of Utility-Class-Based Systems

One thing that utility-class-based systems don't support well is contextual css. I'll explain what I mean by that with the example below. In those systems, you add classes to individual elements, and they can have almost no impact on other elements, even if they should be tightly coupled by design. I'll use a card grid as an example.

Card Grid

We often create layout components, like a "grid" that support a variety of components as children. We might have a "Card Grid", and a "Person Grid", and a "News Grid" that all contain different components (markup and styles). But they all follow the exact same grid design patterns. They're one column on small (mobile) screens, three columns on medium screens, and six columns on large screens. We could create different "grid" components for each of these, and copy and paste the grid styles between them, or even share the styles through mixins, or something. But we prefer to just have one "Grid" component that works for all of them, since the grid portion of the component should ALWAYS be consistent between them. By ALWAYS, I mean that if we decide the spacing between grid items should change, it should change on all of the grid variations, not just one of them.

In order to do this, we would write our css something like this:

.grid {
  @include breakpoint($break-m) {
    display: flex;
    flex-wrap: wrap;
  }

  & > * {
    @include breakpoint($break-m) {
      flex: 1 1 33%;
    }

    @include breakpoint($break-l) {
      flex-basis: 16.67%;
    }
  }
}

What this code does is says "At the medium breakpoint, the grid will flex (instead of the default stacking), and immediate children of the grid element (grid-items, if you will) are 33% (three across). Then at the large breakpoint, grid-items are roughly 16% (to allow 6-across).

(Code explanation: The > means "Immediate children" and the wildcard selector * means, anything - as opposed to > .card which would only apply to immediate children that also have the .card class.)

With this code, any component can be placed inside the Grid (Card, Person, News-item, etc.) and they will all follow the same grid rules without having to add styles to any one of the specific grid item components.

This type of grid component isn't possible with a utility-class-based framework, like Tailwind because you'd have to add the child styles to each component that goes inside the grid... But then, what if you have another grid variation. Or the news item should be used outside of a grid?...

Our preference is to style components in such a way that they can be viewed in isolation, and look good regardless of container size (browser, component, etc.) Then, let the container components (like Grid, Section, etc.) define the margin around, width of, etc. child elements.

So, in the Tailwind example at the beginning of this post, we would not put the max-w-sm and lg:max-w-full on the card itself. We believe the card should always be full-width, and the containing component is what defines how wide the items are at any point in time. So the container is where those styles would be implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment