title && ()

All websites look the same: Let's bring the colors back!

An opinionated guide to colors in the web

Sep 2, 2024

The Hot, mild, and cool cycle

Art genres seem to go through the same patterns. They are born hot, vibrant and full of the energy an act of rebellion demands. Once they become more popular, they turn mild, expansive and sometimes bland. As the genre fades away in popularity, it becomes cool, niche and rational.

The gartner hype cycle on a hot to cool spectrum

It’s a cycle of visceral birth and rational death, like the old adage:

If a person is not a liberal when he is twenty, he has no heart; if he is not a conservative when he is forty, he has no head.

Let’s go back to the jazz age to illustrate the full spectrum:

During the roaring 20s, youngsters would go to speakeasies to drink alcohol (which was prohibited), take drugs, and dance Charleston. Boom, rattle, bang! The beat of jazz was accelerated, syncopated and considered a "devil's music" for traditional music listeners. Hot!

During the 30s, jazz became the most popular genre in the US. With radio broadcasting, big bands, and swing dancing. It dominated music as no other genre ever has. With new musicians entering the scene, the music grew more sophisticated, leading to burning hot bebop and later the cooler... cool jazz. Mild.

By the late 50s, the genre already collapsed, giving way to rock'n'roll. Musicians became so good that they got out of touch with their audiences. Jazz turned more rational, technical and abstract, which made the genre adopt the "music for intelectual snobs" fame. You know a genre is in the last stage of their cycle when someone tells you: "if you don't like it, is because you don't understand it". Cool …

Jazz, surprisingly, managed to stay interesting even at its peak. Unlike luxury brands, which are more popular and boring than ever, partly because it's believed that rebranding to sans-serif is necessary to increase their total addressable market.

This article is about Web design

But I digress.

When it comes to web design, it feels like we're going through the same cycles: from the hot, colorful chaos of Geocities, through the mild intricacies of skeuomorphism, only to arrive at the cool, minimalist style that's popular today.

You've probably heard the complain that all apps look the same nowadays. While this may be an exaggeration, it's true that most web apps share a similar modern, monochrome, and somewhat dull appearance.

We are all trying to imitate the alphas of the current meta (Stripe, Notion, Linear, Figma) with a repertoire of astonishing techniques such as animations, 3D illustrations and CSS effects. Or just plainly assuming that “design has already been solved” and taking an off-the-shelve solution such as tailwind UI or shadcn.

Nothing captures the zeitgeist of 2024 better than memes. I think the most relevant one is the “every website looks like Linear”.

The purple aurora Linear look permeates everything

So my take is that we are in the late mild-stage, transitioning to the cool one. Tired: feelings & vibes. Wired: knowledge & skills.

We are designing more generic web apps in order to maximize their total addressable market: Brave designs are more divisive and lead to a handful of lovers and haters, but a bland one appeals to everybody.

Additionally, designers have polished their skills to signal status and compete with one another, inevitably distancing themselves from their audience.

Skill issues and the birth of new genres

I was never going to beat these girls on what they do best, the dynamic and the power moves, so I wanted to move differently, be artistic and creative because how many chances do you get that in a lifetime to do that on an international stage?

Raygun – creator of the "kangaroo break dance" subgenre

Once a genre's popularity starts peaking, it becomes increasingly difficult to remain relevant. All the practitioners compete to become the alpha, getting very, very good at it.

It's tough out there.

But some, feeling the pressure of their skill issues, grow increasingly frustrated, flip the middle finger to the status quo, and fork the genre into something new where they can make their mark. Dadaism rejected the rationalism of modern art, and punk flipped off the pretentiousness of rock 'n' roll—both arguably more entry-level than what came before.

Perhaps Raygun was into something, and we are going to see animal-inspired dancing as a subgenre of break dance?

Back to web app design.

I'm not a designer by trade myself, and I feel massively under skilled to compete in the current meta. Hence, like Raygun, I refuse to compete within the same genre.

I rebel against bold Inter headlines with a gradient mask, trying to steal the teenage-engineering look, the pixel-perfect border radius math, and those luscious, over-layered shadows you could practically make love to.

I reject the rational, and serious design coolness to embrace the emotional, and playful one. And for me, the obvious first step is to say: "Colors welcome back, we've missed you."

At this point, you might be wondering: Did I invent an entire theory of art genres just to justify my skill issues and my use of color? Hell yeah!

I would cross the rocky mountains riding an army of bisons to ransack San Francisco before learning how to design those fancy buttons that shed light when you hover them—although this is actually not true, I'm pretty chill and I do like learning things, but I hyperventilated while writing and I didn't want to break the flow.

Welcome back colors!

I see a lot of designers reject the use of color.

Some will raise accessibility concerns, which are valid if your affordances rely solely on color. But when used decoratively, they can enhance your design without compromising accessibility.

Others will claim that designing with colors is harder. That's true. Desaturated or monochrome palettes are like jeans and a t-shirt—they’ll always look fine. But as soon as you start playing with colors, there are more ways to mess things up. In the rest of this article, I'll walk you through my process for designing with color, which might just help you give it a shot.

An easy way to create a color system is to constraint yourself to only use:

  • One “black” color

  • One “white” color

  • One “gray” scale

  • N color scales

Hues

First step is to pick the hues. Many articles will try to shovel a lot of science stuff down your throat to make it look like picking hues is a science. But in reality, it's mostly vibes and figuring out why some things look nice after the fact. Like most artistic learning, just look around and once you see something you like, try to figure out why. To explore ideas, you can use books such as Interaction of Color, or the Dictionary of Color Combinations.

I find that starting with hues makes the process a lot easier, since the rest of the choices tend to be more about utility than creativity.

For example, while designing fika, I've based the whole design on five hues: emerald, indigo, amber, rose and tangerine. I choose sophisticated names to feel better about myself: green, blue, yellow, pink and orange are for normies.

Do these colors follow any formula like Triadic or Tetradic colors? I have no idea. I just wanted colors that “felt” different enough, and math cannot be used to calculate this since it’s a social thing. Different people categorize colors differently, but clearly our biggest categories are green and blue—so much that we used to put them both in the same “grue” category.

a map of colors with their categories by xkcd
xckd results from his color categorization survey

Contrast

Contrast defines how distinct two colors appear from each other, and there are many ways to measure it.

The simplest method is WCAG2, but its simplicity makes it flawed because it overlooks the fact that different colors can have different perceived lightness.

different hues with their perceived lightness
Blue is perceived as the darkest, while yellow comes across as the brightest.

To avoid false positives (and negatives) such as the infamous "orange button problem", there is a new way to measure contrast called APCA. I highly recommend adopting it.

To define contrast, we'll create lightness curves that apply across all color scales. This way, UI elements can use different hues while maintaining a consistent contrast.

This was historically hard to achieve because, again, perceived lightness is different for each hue. But luckily, there is also a new color space called OKLCH which addresses this problem.

OKLCH is a color space similar to HSL (Hue, Level, Saturation), but the C stands for Chroma (which is more or less like Saturation), and the L is the Perceptual Lightness. This means that you can finally craft color scales with the same contrast and "colorfulness" characteristics, without guessing.

With all these new tools, working with colors nowadays is fantastic!

Lightness

To create a lightness curve, you want to follow an S-shape.

This approach maximizes the contrast between dark and light tones, as most UI elements will use colors at the extremes. Middle tones are seldom used due to their lack of sufficient contrast.

We want most of our palette to be on the extremes of our lightness spectrum, avoiding the low-contrast middle

Saturation / Chroma

Chroma follows a normal-ish distribution instead, with very low levels in the light and dark tones, and higher levels in the middle tones to compensate for their lack of light contrast.

I like to add a bit more saturation to the dark tones, but that's just my personal preference. The downside is that it breaks symmetry.

An example of an asymmetric Chroma curve, with higher saturation in the middle tones and lower saturation in the extremes

Symmetry is important if you are planning to reuse the same scales for a dark theme. In my experience, though, that dream doesn’t hold up because you can’t just reverse the colors and expect the light/shade metaphors to work the same. Also, I find more pleasant when dark tones are more vibrant.

comparing saturation palettes
Symmetrical saturation feels too "bland" or "vibrant" for both dark and light themes
comapring asymmetrical saturation palettes
Asymmetrical saturation scales feel much better, but then you can't reuse the same scale for light and dark themes.

One exception to this is your “gray” scale, which you will use for neutral elements such as backgrounds or long-form text, which need a much flatter saturation curve.

When you decide about that scale, you will either drop saturation altogether, or have a very flat curve for a warmer or colder vibe.

OKLCH

Once you have the hues and the lightness/saturation curves, it's time to combine them. One challenge you may face is that not all hues are made equal, and some fall short in the darks or the light tones.

Luckily OKLCH allows for an extended range of colors P3 which can ease this limitation, but, expect some tiny adjustments since some colors (like cyan or orange) have extremely very narrow ranges.

oklch picker screenshot
You can use OKLCH color picker to understand how P3 colors can help you extend the range of some hues

If this article seems too much and you don't know where to start, I highly recommend downloading Harmony as a starting point. It's a collection of color scales created by the talented people at Evil Martians. Harmony has symmetric contrast, which makes them a good fit if you plan to add a dark theme.

Check their article on OKLCH and also their fantastic color picker.

If you work with Figma, I also highly recommend using the okcolor and Polychrom Figma plugins to workaround the lack of support for OKLCH and APCA contrast.

Awesome. You have a color system ready to go and it's time to implement it. Let's delve dive into it! (every article should contain a clue of being written by a human, here’s mine)

Coding a color palette

I'm aware that all the cool kids use tailwind since "they've solved CSS and there is no need to reinvent the wheel". But I still like using vanilla-extract. It fits better my mental model and I feel more productive with it. I hope this doesn't make you angry and you can keep reading this article. We can all learn from each other.

First thing we do is to create a colors.ts file with a definition of the colors, types and some convenient functions to operate it:

// colors.ts
const colors = {
    base: {
        '0': 'oklch(99.8% 0.002 90)',
        '1000': 'oklch(18% 0.043 100)',
    },
    sand: {
        '50': 'oklch(99% 0.008 90)',
        // ...
        '950': 'oklch(22% 0.02 100)',
    },
    // omitted for brevity
    emerald: { ... },
    rose: { ... },
    indigo: { ... },
    orange: { ... },
    amber: { ... },
} as const

export const hues = [ 'sand', 'emerald', 'rose', 'indigo', 'orange', 'amber' ] as const

export type Namespaces = keyof typeof colors
export type Hue = (typeof hues)[number]
export type BaseHue = Exclude<Namespaces, Hue>
export type ShadeTypes<H extends Hue> = keyof (typeof colors)[H]

export type BaseColor = `${BaseHue}${ShadeTypes<BaseHue>}`
export type ScaleColor = `${AccentHue}${ShadeTypes<AccentHue>}`
export type Color = BaseColor | ScaleColor

const flatten: { [key: string]: string } = {}

for (const hue in colors) {
    for (const shade in colors[hue as Hue]) {
        const shades = colors[hue as Hue] as any
        flatten[`${hue}${shade}`] = shades[shade] as string
    }
}
export const color = flatten as Record<Color, string>

The most useful function is mapHues. It's a function that returns an object for every hue in your color system. This is particularly useful when creating variants, since it helps us define the code once for all the hues.

// colors.ts
export function colorize(hue: Hue, shade: ShadeTypes<Hue>) {
    return `${hue}${shade}` as ScaleColor
}

export function mapHues(
    map: (fn: (
        shade: ShadeTypes<Hue>) => string,
        hue: Hue
    ) => {}
) {
    return hues.reduce((acc, hue) => {
        acc[hue] = map((shade) => color[colorize(hue, shade)], hue)
        return acc
    }, {} as { [K in Hue]: {} })
}

This is how you use it in your vanilla-extract CSS files:

// alert.css.ts
export const alertRecipe = recipe({
  base: {
    borderRadius: space['4'],
    borderWidth: 1,
    paddingBlock: space['12'],
    paddingInline: space['18'],
  },
  variants: {
    hue: mapHues(color => ({
      color: color('800'),
      background: color('100'),
      borderColor: color('200'),
    }))
  },
})

And then you can easily use it in your component like this:

// alert.tsx
function Alert(p: { hue: Hue, text: string }) {
  return (
    <div class={alertRecipe({ hue: p.hue })}>
      {p.text}
    </div>
  )
}

Which results in the following components:

I also like to create toggleColor which is a function o automatically finds us the symmetric lightness for a given color. This is very useful for components that need to use the high or low end of your color palette, depending on whether they sit on top of a light or dark background.

// colors.ts
export function toggleColor(
    hue: Hue,
    shade: ShadeTypes<Hue>,
    condition: boolean
) {
    const finalShade = condition ? String(1000 - Number(shade)) : shade

    return colorize(hue, finalShade as ShadeTypes<Hue>)
}

Now we can use this to “toggle” the lightness:

// card.tsx
type Props = {
  hue: Hue,
  dark: boolean,
  title: string,
  description: string
} & ParentProps

function Card(p: Props) {
  return (
    <Box
      background={toggleColor(p.hue, '100', p.dark)}
      borderColor={toggleColor(p.hue, '200', p.dark)}
    >
      <Header1
        color={toggleColor(p.hue, '900', p.dark)}
      >
        {p.title}
      </Header1>
      <Text
        color={toggleColor(p.hue, '800', p.dark)}
      >
        {p.title}
      </Text>
    </Box>
  )
}

And this is how this component looks like for all the combinations of hue and dark:

Wrap up

This was a long one! I hope this post convinced you to fight the cool and bring some joyful colors back to the internet.

© 2024 fika title && () create your own blog