Pro Tailwind

03. Dynamic "lookup" style objects

šŸ‡ TL;DR

  • We’ve extracted styles common to all buttons
  • Tailwind Intellisense is working in our baseClasses variable
  • Let’s tackle dynamic styles next, starting with the impact button variants!

ā¬‡ļø Skip to the challenge


🐢 Background

We’re off to a good start. By extracting styles common to all variants of our Button and customizing the Tailwind Intellisense vscode extension, we’re in a good position to tackle some dynamic style management.

And we’ll start with the impact variants.

Three impact levels

The button source code tells us that a button can have three impact values: bold, light, and none.

We have defined a TypeScript type for these impact values:

type ButtonProps = {
  // ...
  impact: 'bold' | 'light' | 'none'
}

If you’re not familiar with TypeScript, don’t worry - you won’t have to write any TypeScript yourself. This is here to help us construct the correct dynamic styles, as we’ll see in a second.

Here’s what these three variants look like:



Go on, click on these buttons. It’s pretty satisfying šŸ¤“

Building a styles ā€œlookupā€ directory

We want to apply different Tailwind classes to the Button based on the value of the impact prop.

Remember, we need to make sure we use entire class strings so that the JIT engine finds and generates those classes in our CSS output.

The best way to make sure all the Tailwind classes we need are present as full strings? Creating an ā€œinventoryā€ or directory of all the possible class permutaitons.

A collection of strings grouping the classses that should be applied for each possible variant permutation.

In the case of the impact prop, we have three possible values - so let’s create an impactClasses lookup directory.

This will be an object with a key for each possible prop value:

const impactClasses = {
  bold: '...',
  light: '...',
  none: '...',
}

Applying dynamic styles

Since the keys of our impactClasses lookup object are mirroring the possible values of the impact prop, we can dynamically reach for the correct key by using the prop value passed to a given Button.

For example, take the following code:

<Button impact="light">Light button</Button>

In that scenario, we want to apply the string of classes located in impactClasses.light.

We can do exactly that by using the bracket syntax:

<button className={impactClasses[props.impact]} />

Great!

But this will only work if the keys inside the impactClasses object perfectly mirror the possible values for the impact prop.

And this is why we’re using TypeScript.


TypeScript Lite to the rescue

In the challenge code, you’ll notice that we’ve annotated the impactClasses object with a TypeScript Record:

const impactClasses: Record<ButtonProps['impact'], string> = {}

Adding that Record sets expectations on this impactClasses object. It should have a key for each possible impact variant, and the value for that key should be a string.

In other words, it ensure that the impactClasses object mirrors the shape of the possible impact variants defined in ButtonProps.

Like Matt Pocock often says, TypeScript is like your teacher looking over your shoulder, and making sure you correctly cater for each variant, and don’t accidentally add any other key to the impactClasses object.

That teacher will keep ā€œbotheringā€ you until you’ve defined the correct impactClasses keys.

It just wants you to succeed!


Merging multiple class lists together

Take the code snippet from before, where we reach for the correct impact Tailwind classes:

<button className={impactClasses[props.impact]} />

With that code, we have thrown away our previous baseClasses containing the common styles for all buttons.

We actually need to merge both the baseClasses and impactClasses in the className attribute!

We could construct a template string that merges them. But I’ve added a neat little helper cx() function in the project:

function cx(...classes) {
  return classes.filter(Boolean).join(' ')
}

This function lets you pass a comma separated list of multiple class strings, and it will take care of merging them them.

It’s just a neat little syntactic helper, to mimic some of the functionality of a library like classnames.

With that function, we can combine our classes like so:

<button className={cx(baseClasses, impactClasses[props.impact])} />

As we compose more and more class lists, this syntax is arguably a little easier to read than a long template string!


šŸ† Your challenge šŸ†

In the Gitpod workspace below:

  1. In src/components/button.tsx, create a key for each impact prop inside the impactClasses lookup object. TypeScript will guide you in this task!

  2. For each key, create a string of the Tailwind classes used for that specific variant, based on what you see in src/partials/hardcoded-buttons.astro.

  3. In button.tsx, merge the correct, dynamic impactClasses in the button’s className attribute.


āœŒļø Good luck!

Open in Gitpod →