WET Astro headings

Producing headings in a reproducible way with Tailwind and Astro requires some hoop jumping. To save you from solving the same problem in your own codebase, I share a monstrously messy Astro component I use on this very website to produce consistent headings with little effort.

The following solution makes it possible to render headings like so:

<Heading as="h1" size="lg">This isn't as big as I was hoping</Heading>

I find Typescript enumerations exhausting to work with so cheat with use of strings where an closed set is more accurate, but life’s all about trade offs. If you’d like something more robust, I exchange my time for fungible life tokens.

---
import { twMerge } from "tailwind-merge";
import type { HTMLTag, Polymorphic } from "astro/types";

type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & {
  // Technically, we could use `keyof typeof sizes` but I couldn't care less
  // about variants and enums and all that type masterbation.
  size?: string;
};

const headingClasses: { [key: string]: string } = {
  h1: "text-5xl sm:text-6xl lg:text-7xl font-bold",
  h2: "text-4xl sm:text-5xl lg:text-6xl font-bold",
  h3: "text-3xl sm:text-4xl lg:text-5xl font-bold",
  h4: "text-2xl sm:text-3xl lg:text-4xl font-bold",
  h5: "text-xl sm:text-2xl lg:text-3xl font-bold",
  h6: "text-lg sm:text-xl lg:text-2xl font-bold",
};

const sizes: { [key: string]: string } = {
  "5xl": headingClasses["h1"],
  "4xl": headingClasses["h2"],
  "3xl": headingClasses["h3"],
  "2xl": headingClasses["h4"],
  xl: headingClasses["h5"],
  lg: headingClasses["h6"],
  base: "text-base sm:text-lg lg:text-xl font-bold",
  sm: "text-base sm:text-lg lg:text-xl font-bold",
  xs: "text-sm sm:text-base lg:text-lg font-bold",
};

const { as: Tag = "h1", class: classes, size, ...props } = Astro.props;

const sizeClasses = size ? sizes[size] : headingClasses[Tag];
---

<Tag
  class:list={twMerge(
    "tracking-tight",
    "text-neutral-900",
    "dark:text-neutral-100",
    sizeClasses,
    classes,
  )}
  {...props}
>
  <slot />
</Tag>

I considered removing my comment about variants and enums, but decided a peek inside my unfettered mind might make it apparent we’re all screaming on the inside, especially me.

Or, more honestly, that even in private projects where I’m the only contributor, I feel the need to excuse pragmatic decisions that I consider to be cop-outs. You’re not the only one with unrelenting standards and over-responsibility. Don’t forget to be kind to yourself!