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 instead use opt for maps keyed with strings. Life is short and all about tradeoffs. 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!