How Avocados Helped Me Make a CSS Framework

Photo by Louis Hansel on Unsplash

How Avocados Helped Me Make a CSS Framework

Travis Waith-Mair's photo
Travis Waith-Mair
·Dec 2, 2021·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

One of the accomplishments I am most proud of is the layout library I created and maintain: bedrock-layout. dev. The library is unapologetically built for React apps and uses styled-components, a CSS-in-JS library built for React. Given that I write so much in React these days, this gets me from zero to full layout in a speedy time frame.

The one difficulty about that decision is that if I ever need to write anything without React or styled-components, I no longer can npm install a component from my library and go. I have to end up recreating it from scratch all over again.

Because of this reason, I have wanted to make a CSS-only version of Bedrock Layout Primitives for some time now. My problem is that I have wanted to keep as much of the same experience as I get from the layout primitives, namely props, in a CSS-only form.

Using conventions like BEM, encourage you to keep adding more and more verbose class names that when mixed in with our decorative classes, really make the intention of the classes easy to lose:

<header class="split hero split-fraction-1/4 center-text" style="--gutter:1rem">
  <!-- Hero content here-->
</header>

You can imagine how much worse this would get if coupled with a utility class library, like tailwind.css. For this reason, I knew I didn't want to keep polluting the class list, but I didn't know how to achieve what I did want.

Introducing the AVO method

Luckily, I was introduced to a different way of writing my CSS called the AVO method. The AVO method, which stands for Attribute Values-Objects, is a BEM dialect created by Michael Chan that uses data attributes instead of class names. This is unique in that I can point my selectors to the data attribute and use the value assigned to the data attribute. The above example can be rewritten like this:

<header
  data-bedrock-split="fraction:1/4"
  style="--gutter:1rem"
  class="hero center-text"
>
  <!-- Hero content here-->
</header>

At first glance, you might think that not much has changed, but you will notice some fantastic benefits right off the bat on further inspection. First, using a data attribute forces you to keep the styling of the data attribute together, unlike the class-based methodologies that can't stop you from putting your classes in any order you want. Even more important is that you truly get that prop-like API that we love in our front-end frameworks.

There is no way I can do it justice in this short post, so I highly recommend you check out Michael's fantastic content on the subject:

For the rest of the post, I want to show some good patterns I found when converting my React-based components to CSS-based components.

How to translate boolean props and props with a fixed number of values

Boolean props are very easy to implement. Much like how you do boolean props in React, you can select based on the existence of the string.

For example, The Center component has two boolean props: centerText and centerChildren. To convert this to AVO, one needs to select based if that string exists, like this:

[data-bedrock-center~="center-children"] {
  display: flex;
  flex-direction: column;
  align-items: center;
}

[data-bedrock-center~="center-text"] {
  text-align: center;
}

Then we can apply it like this:

<div data-bedrock-center="center-text">
  <!-- text alignment will be centered -->
</div>

<div data-bedrock-center="center-children">
  <!-- children will be centered -->
</div>

<div data-bedrock-center="center-text center-children">
  <!-- text alignment and children will be centered -->
</div>

You will notice in the above example that we are using ~=. If we only use =, the string would need to be an exact match. However, ~= lets us look for any of the space-delimited words to find a match.

Note this will not match on partial words. If I used the string center-children-vertically, this would not match. There is a way to do this kind of math, but I only recommend it for a particular scenario, which I will go over later.

This strategy also works well if your prop's values are enumerable, or in other words, there is a fixed amount of acceptable values. Here I would use a naming pattern where you provide both the prop name and value delimited by a semi-colon: <prop-name>:<value>.

For example, the Split component has a fixed set of values for it's fraction prop: 1/4, 1/3, 1/2, 2/3, 3/4, auto-start, and auto-end. This would translate to fraction:1/4, fraction:1/3, and so on.

With this in mind, we can write our CSS like this:

[data-bedrock-split~="fraction:1/4"] {
  grid-template-columns: 1fr 3fr;
}

[data-bedrock-split~="fraction:1/3"] {
  grid-template-columns: 1fr 2fr;
}

[data-bedrock-split~="fraction:1/2"] {
  grid-template-columns: 1fr 1fr;
}

[data-bedrock-split~="fraction:2/3"] {
  grid-template-columns: 2fr 1fr;
}

/* ect */

and we can use that in our html, like this:

<div data-bedrock-split="fraction:1/4">
  <div />
  <div />
</div>

<div data-bedrock-split="fraction:1/3">
  <div />
  <div />
</div>

<div data-bedrock-split="fraction:1/2">
  <div />
  <div />
</div>

<div data-bedrock-split="fraction:2/3">
  <div />
  <div />
</div>

How to translate with unknown values

Sometimes props rely on the user to provide a value, and there is no way to control those values ahead of time. In those scenarios, you probably want to reach for a custom property. CSS custom properties, also known as CSS variables, let you define your own properties and values, which you can then use in your CSS. Often we see these values being declared in the root, like this:

:root {
  --my-custom-size: 15px;
  --my-custom-color: dodgerblue;
}

The thing is that, nothing says can't be declared inline, like this:

<div style="--my-custom-size: 15px; --my-custom-color: dodgerblue;"></div>

This allows us to use them in a very "prop" like way. For example, The Center component has a prop called maxWidth that accepts any CSS length or percentage. We can translate that into a CSS custom property like this:

[data-bedrock-center] {
  max-inline-size: var(--maxWidth, 100%);
}

and we can then use that in our HTML like this:

<div data-bedrock-center style="--maxWidth: 75%">
  <div>
    Nulla luctus nisl nec dui auctor volutpat. Phasellus condimentum elementum
    enim in pharetra.
  </div>
</div>

Some of you might be saying, wait, inline styles are bad, aren't they? And the answer to that is: no, they are not bad. CSS follows an exception-based styling where you start with the most general styles and then work your way down to the most specific styles. Inline-styles are meant to be the most specific parts of your styles, the ultimate exception. If you used them for all your styling, then yes, it would be bad, but these are being used to make one specific change to a specific element on the page, and so, no, they are not bad to use in this case.

Styling based on the existence of a prop

Sometimes you only want to add styles if a prop exists and is not based on its value. For example, the user can use the Inline component to pass in a CSS length to a switchAt prop to define the threshold that the component will switch to a stacking layout. If the threshold isn't provided, I don't even want certain styles applied.

To achieve this in the AVO method, we need to change our selector slightly. Instead of using ~=, we are going to use *= instead. This will let us search for the existence of a string, no matter if it's part of a larger string. So we can write our AVO style of the switchAt prop like this:

[data-bedrock-inline][style*="--switchAt"] {
  flex-wrap: wrap;
}
[data-bedrock-inline][style*="--switchAt"] > * {
  min-inline-size: fit-content;
  flex-basis: calc((var(--switchAt) - (100% - var(--gutter))) * 999);
}

The above code is looking for the existence of the --switchAt custom property being passed inline to the style attribute. If it does, it will apply styles to both the parent and all the children. Otherwise, these styles will not be applied. So now we can write that in HTML like this:

<div data-bedrock-inline style="--switchAt: 45rem">
  <div />
  <div />
  <div />
  <div />
</div>

Hopefully, this has been enough to get you interested in trying it out. If you want to see the full implementation of the Bedrock Layout CSS version, check out the Github repo. Also check out the content over at chan.dev/avo.

Did you find this article valuable?

Support Travis Waith-Mair by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this