All Notes

How to have great taste

Last updated

Sep 05, 2024

The people I’ve worked with who are truly great at what they do (e.g., building products, making open-source libraries, or architecting systems) all have incredible taste.

I want to be great at what I do. So, how can I have better taste?

Below is an ongoing log of what I have learned. If you know of any other techniques, please tell me about them!

Get more parts on the table

One of the core components of innovation is to have more “parts on the table,” to know more things that you can connect together in new ways.

This also helps with taste immensely. By seeing more examples, you have more data to be able to assess what works and what doesn’t.

From Shankar Ganesh calls this “Grow your exposure”:

If all you’ve watched in life are two movies, chances are you’ll think both of them are pretty good.

But if you’ve watched a thousand, you’ll really know to separate the great from the good. And the good from the terrible.

Dissect the greats

From Shankar Ganesh, who calls this “Tear apart what goes into world-class”:

A wine or food connoisseur typically breaks down wine or food into its components – taste, aroma and texture, and has an opinion about how they come together to create a complete experience.

Similarly, to grow your taste in building software, you need to start distilling down and discern the components that have gone into the products you’re exploring.

By doing this for years, you can build a repertoire of patterns and building blocks that go behind creating something world-class.

I deeply believe that this works. I know of a few people who are great at what they do and do this regularly:

  1. The best product designer I’ve ever worked with, Brian Lovin, publishes app dissections where he analyzes the interactions of apps he loves and interviews great designers

  2. Samuel Hulick of UserOnboard.com dissects the user onboarding journeys of popular apps

  3. Harry Dry of Marketing Examples dissects great marketing examples

I do not doubt that doing these sorts of dissections regularly played a big part in them becoming great at what they do.

Iterate, iterate, iterate

The only way I know of to make something great is to iterate:

  1. Build and ship the first version

  2. Dissect what works and what doesn’t

  3. Then, ship an iteration to test your dissection

  4. Repeat

Importantly, while doing this:

Make it work, make it right, make it fast—in that order!

  • If you try to make it right first before shipping, you won’t have enough feedback to make iterations in the direction of “right.”

  • If you try to make it fast before you make it right, you’ll have to make it fast again once you make it right (which will require bigger changes).

So, do it in that order: make it work first, then ship it to get feedback (now that it works!), use that feedback to iterate until it’s right, and then focus on making it fast.

Taste is not enough, you also have to know how to apply it

As Ira Glass puts it in The Gap, even when you finally have built up great taste, that doesn’t mean you know how to apply it:

For the first couple years you make stuff, it’s just not that good. It’s trying to be good, it has potential, but it’s not. But your taste, the thing that got you into the game, is still killer. And your taste is why your work disappoints you. A lot of people never get past this phase, they quit.

Once you have great taste, you also have to do the work to learn how to apply your taste. (even if, like famously Rick Rubin, you only learn to speak to it, but not how to make great things yourself)

An example of developer experience taste: how the styled-components API came to be

styled-components is one of the most popular open-source libraries of all time (Top 300 repository on GitHub). Glen Maddern and I combined our experiences and taste to create an abstraction that I believe is truly great and has withstood the test of time.

Let me walk you through the pieces we had on the table and the iterations of API design that they led to.

Making it work

First piece: Existing options for component libraries styling

I worked at Thinkmill on ElementalUI (our open-source component library), which was styled using Less. However, that meant that users had to have webpack, css-loader, and less-loader set up just right to use ElementalUI in their apps.

We wanted to make it simpler for users to set up: no more build-step.

We explored the existing CSS-in-JS solutions (Radium and JSS at the time), as they didn’t require a build-step. But no library had theming support, which we needed for ElementalUI.

This was the first piece we had on the table: the knowledge of the problems with building component libraries and an understanding of the space at the time and why none of the existing solutions worked for our use case.

Second piece: Style-specific components

The second part we had on the table was style-specific components like Grid, Row, etc. The community had discovered this pattern in the prior months, and Michael Chan, a friend of ours, had been talking about them with us.

This part led to our first breakthrough: what if we move styling to a component level by default? What if you couldn’t write styles any other way?

This is where we came up with the function('tag') syntax for creating style-specific components and enforcing that as the only way of styling an application.

const Comp = elem('div')(/* TODO: styles here somehow */)

<Comp /> // Render Component with styles

Third piece: Injecting CSS from JavaScript

We both wanted styles to live in JavaScript, but neither of us was a fan of CSS as objects. Our first experiment was a function syntax where each possible CSS rule was a function, e.g. margin(1).

But after trying out a prototype, it didn’t feel right to us. We really liked the expressiveness of the actual CSS syntax.

Glen had previously invented CSS Modules, so the third piece we had on the table was that he knew how to inject styles into the DOM from JavaScript.

This led us to our second breakthrough: what if you could write actual CSS syntax in your JavaScript? And the library injected it for you optimally?

The API evolved and looked something like this, with a string of CSS being passed into the second function:

const Comp = elem('div')(`
color: blue;
`)

Fourth piece: Tagged template literals

Glen whipped up a super simple prototype overnight, but he used this super weird syntax I’d never seen before to make it easier to type—tagged template literals.

This was the fourth piece we had on the table: tagged template literals had just been added to JavaScript, the language, and Glen knew about them.

Our timing was impeccable because transpilers that enable people to use modern JS language features in their existing apps had just become widely used. Before Babel and others, nobody could adopt new language features like that until years later when all browsers implemented it.

The API evolved to this (note the missing () around the final template literal):

const Comp = elem('div')`
color: blue;
`

Contrary to normal template literals, functions interpolated into tagged template literals can be called from the tagged template literal function.

That became the third breakthrough: using interpolated functions in tagged template literals for conditional styling based on props.

Now the API looked like this:

const Comp = elem('div')`
color: ${props => props.primary ? 'blue' : 'red'};
`

<Comp primary={true} /> // → blue
<Comp primary={false} /> // → red

Fifth piece: React context

I think this was the point when we whipped up the first seriously usable package to make this happen. We both used it to rework a small app and saw how nice it felt to use.

The first thing we tackled from there was theming. This was big for both of our use cases, so we wanted to make sure we got it right. We knew that we wanted to have dynamic themes.

The fifth piece we had on the table was an understanding of React’s Context feature. While it’s now widely understood and adopted, it was a relatively obscure part of React’s API back then.

Modeled off of react-redux et al we went with a Provider-style API for passing the theme down the tree:

<ThemeProvider theme={someTheme}></ThemeProvider>

Getting the theme into context was the easy part, but how were we going to inject it into the components? There were three possibilities:

const Comp = elem('div')`
// First idea: Two arguments
${(props, theme) => props.primary ? theme.main : theme.background};

// Second idea: One argument destructured
${({ props, theme }) => props.primary ? theme.main : theme.background}

// Third idea: Injected as props.theme
${props => props.primary ? props.theme.main : props.theme.background}
`

This might’ve been the decision we spent the most time on, and that took the most taste. Any one of these three options would’ve probably been fine—but there was one option that was better than the others upon close examination:

  1. The first idea was nice, but if you just wanted to use the theme you had to assign an empty variable which is annoying and unnecessary. (color: ${(_, theme) => theme.main}))

  2. The second idea improved upon the first idea by faking “named arguments” in JavaScript with ES6 destructuring to allow you to pick just the variables you need.

    1. That was our preferred API for a while—until we realised that we had no idea what to do in ES5. What do you even call that argument to the function if you’re not restructuring?!

    2. It also added unnecessary cognitive overhead for folks who hadn’t seen destructuring before.

  3. The third idea was the one we eventually settled on. It still allowed you to just get the theme (({ theme }) =>, props =>). Even more importantly, it has great interplay with Reacts default component APIs:

    const Comp = elem('div')`
    color: ${({ theme }) => theme.primary};
    `

    // Want to set a default theme on a component level? Just use defaultProps 🎉
    Comp.defaultProps = {
    theme: { primary: '#000' },
    }

    // Want to override the theme in this one specific case of the component? Just pass in the `theme` prop!
    <Comp theme={{ primary: 'blue' }} />

Combining all these pieces together in just the right way was almost obvious to us within a few days; it didn’t feel laborious or difficult to come up with. However, we could only do it because we had all the necessary pieces.

In fact, if we hadn’t come up with this API, I bet somebody else would’ve soon come up with something similar because I doubt we were the only ones with all these pieces on the table.

But, at the same time, there probably weren’t many people on the planet with all these pieces. So, who knows…

Making it right

Once we had the core API down and spent a few weeks writing extensive docs (that are still largely the same today as they were back then), we launched the library.

From the community feedback and our own experiences, we kept iterating on many smaller decisions to make the library just right:

  • Adding elem.tag shorthands like elem.div instead of having to call the function with a string like elem('div') meant editors could start autocompleting after fewer characters were typed

  • Adding the ability to restyle components via elem(Component)

  • Naming the library styled-components and naming the core API styled.div, which matches how you would describe what the library does in natural language (“I’m making a styled div!“)

  • Adding the ability to override the styles of another component when it’s rendered within a specific parent

  • Allowing Sass-style nesting of CSS rules

  • Changing the global style injection API from an imperative injectGlobal function to a declarative createGlobalStyle component function to enable updates to global styles from state.

Making it fast

And then, once we felt like the abstractions were just right, we started focusing on performance and making it fast. Super fast.

Other notes about Developer Tools and/or Personal Growth