Text

Heading

A section title, h1 to h6, where the level follows the page outline and the size is chosen apart from it.

Built on
<h1> to <h6>text-wrap: balancecalc()
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
0.9 kB
Dependencies
React

Billing

Payment method

Past invoices

Try its options

Loading its options.

Code
import { Heading } from "@/components/equip/heading";

export default function Demo() {
  return (
    <div style={{ display: "grid", gap: 8, width: 224 }}>
      <Heading level={2} size="xl">Billing</Heading>
      <Heading level={3} size="m">Payment method</Heading>
      <Heading level={4} size="xs">Past invoices</Heading>
    </div>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;width:224px">
  <h2 class="eq-heading" data-size="xl">Billing</h2>
  <h3 class="eq-heading" data-size="m">Payment method</h3>
  <h4 class="eq-heading" data-size="xs">Past invoices</h4>
</div>

Examples

Sizes

Each size is a multiple of --eq-text-l or --eq-text-m, so raising the text tokens raises every heading with them.

Release notes

Release notes

Release notes

Release notes

Release notes

Code
import { Heading } from "@/components/equip/heading";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 12 }}>
      <Heading level={2} size="xl">Release notes</Heading>
      <Heading level={2} size="l">Release notes</Heading>
      <Heading level={2} size="m">Release notes</Heading>
      <Heading level={2} size="s">Release notes</Heading>
      <Heading level={2} size="xs">Release notes</Heading>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:12px">
  <h2 class="eq-heading" data-size="xl">Release notes</h2>
  <h2 class="eq-heading" data-size="l">Release notes</h2>
  <h2 class="eq-heading" data-size="m">Release notes</h2>
  <h2 class="eq-heading" data-size="s">Release notes</h2>
  <h2 class="eq-heading" data-size="xs">Release notes</h2>
</div>

Page header

Team members

Everyone here can open projects in the Larder workspace. Owners can also change billing.

Code
import { Heading } from "@/components/equip/heading";
import { Button } from "@/components/equip/button";

export default function Example() {
  return (
    <header style={{ display: "flex", gap: 16, alignItems: "flex-end", justifyContent: "space-between", flexWrap: "wrap" }}>
      <div style={{ display: "grid", gap: 4, maxWidth: 420 }}>
        <Heading level={1}>Team members</Heading>
        <p style={{ margin: 0, color: "var(--eq-ink-2)", fontSize: "var(--eq-text-m)", lineHeight: 1.55 }}>
          Everyone here can open projects in the Larder workspace. Owners can also change billing.
        </p>
      </div>
      <Button variant="accent">Invite people</Button>
    </header>
  );
}
HTMLworks in any framework, with the part's CSS
<header style="display:flex;gap:16px;align-items:flex-end;justify-content:space-between;flex-wrap:wrap">
  <div style="display:grid;gap:4px;max-width:420px">
    <h1 class="eq-heading" data-size="xl">Team members</h1>
    <p style="margin:0;color:var(--eq-ink-2);font-size:var(--eq-text-m);line-height:1.55">Everyone here can open projects in the Larder workspace. Owners can also change billing.</p>
  </div>
  <button type="button" class="eq-button" data-variant="accent" data-size="m">
    <span class="eq-button-label">Invite people</span>
  </button>
</header>

Level and size apart

These card titles are h3 elements for the outline of the page, but drawn at size xs so they sit quietly inside each card.

Usage this month

Build minutes

1,840 of 3,000 used. Resets on 1 December.

Bandwidth

212 GB served from 14 regions.

Code
import { Heading } from "@/components/equip/heading";

export default function Example() {
  const card = { display: "grid", gap: 4, padding: 16, border: "1px solid var(--eq-line)", borderRadius: "var(--eq-radius-l)" };
  const body = { margin: 0, color: "var(--eq-ink-2)", fontSize: "var(--eq-text-s)", lineHeight: 1.5 };
  return (
    <section style={{ display: "grid", gap: 12 }}>
      <Heading level={2} size="m">Usage this month</Heading>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 12 }}>
        <div style={card}>
          <Heading level={3} size="xs">Build minutes</Heading>
          <p style={body}>1,840 of 3,000 used. Resets on 1 December.</p>
        </div>
        <div style={card}>
          <Heading level={3} size="xs">Bandwidth</Heading>
          <p style={body}>212 GB served from 14 regions.</p>
        </div>
      </div>
    </section>
  );
}
HTMLworks in any framework, with the part's CSS
<section style="display:grid;gap:12px">
  <h2 class="eq-heading" data-size="m">Usage this month</h2>
  <div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));gap:12px">
    <div style="display:grid;gap:4px;padding:16px;border:1px solid var(--eq-line);border-radius:var(--eq-radius-l)">
      <h3 class="eq-heading" data-size="xs">Build minutes</h3>
      <p style="margin:0;color:var(--eq-ink-2);font-size:var(--eq-text-s);line-height:1.5">1,840 of 3,000 used. Resets on 1 December.</p>
    </div>
    <div style="display:grid;gap:4px;padding:16px;border:1px solid var(--eq-line);border-radius:var(--eq-radius-l)">
      <h3 class="eq-heading" data-size="xs">Bandwidth</h3>
      <p style="margin:0;color:var(--eq-ink-2);font-size:var(--eq-text-s);line-height:1.5">212 GB served from 14 regions.</p>
    </div>
  </div>
</section>

Props

PropTypeDefaultWhat it does
level1 | 2 | 3 | 4 | 5 | 6noneWhich element it renders. Required, and chosen from the outline of the page, not from how big it should look.
size"xl" | "l" | "m" | "s" | "xs"follows levelHow big it looks: 32, 24, 20, 16 or 14px at the default tokens. Level 1 is xl, 2 is l, 3 is m, 4 is s, 5 and 6 are xs.
...restComponentProps<"h2">noneEvery native heading attribute, including id for in-page links.

Anatomy

root.eq-headingThe h1 to h6 element, with its look on data-size.

Notes

  • Screen reader users jump between headings and hear their levels, so go down one level at a time and do not skip from h2 to h4 to get a smaller look; use size for that.
  • Use one h1 per page, naming what the page is.
  • Sizes are multiples of --eq-text-l (xs is --eq-text-m), so a product that raises its text tokens gets larger headings with no other change.
  • The heading has no margin; space it with the layout around it.
  • Lines are balanced with text-wrap: balance so a two-line title does not leave one word alone; engines without it wrap normally.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/heading.tsx23 lines
import type { ComponentProps } from "react";
import "./heading.css";

type Level = 1 | 2 | 3 | 4 | 5 | 6;
type Size = "xl" | "l" | "m" | "s" | "xs";

export type HeadingProps = ComponentProps<"h2"> & {
  /** Which element it renders, h1 to h6. Pick it from the outline of the page, not from the look. */
  level: Level;
  /** How big it looks. Defaults to the size that goes with the level. */
  size?: Size;
};

const sizeForLevel: Record<Level, Size> = { 1: "xl", 2: "l", 3: "m", 4: "s", 5: "xs", 6: "xs" };

export function Heading({ level, size, className, children, ...rest }: HeadingProps) {
  const Tag = `h${level}` as const;
  return (
    <Tag {...rest} className={["eq-heading", className].filter(Boolean).join(" ")} data-size={size ?? sizeForLevel[level]}>
      {children}
    </Tag>
  );
}
components/equip/heading.css40 lines
@layer equip {
  /* The base scale stops at 16px, so heading sizes are multiples of the text tokens:
     a product that raises --eq-text-l gets larger headings with it. */
  .eq-heading {
    --_size: calc(var(--eq-text-l) * 1.25);
    --_leading: 1.25;
    --_track: -0.01em;

    margin: 0;
    color: var(--eq-ink);
    font: var(--eq-weight-strong) var(--_size) / var(--_leading) var(--eq-font);
    letter-spacing: var(--_track);
    text-wrap: balance;
  }

  .eq-heading[data-size="xl"] {
    --_size: calc(var(--eq-text-l) * 2);
    --_leading: 1.15;
    --_track: -0.022em;
  }

  .eq-heading[data-size="l"] {
    --_size: calc(var(--eq-text-l) * 1.5);
    --_leading: 1.2;
    --_track: -0.016em;
  }

  .eq-heading[data-size="s"] {
    --_size: var(--eq-text-l);
    --_leading: 1.35;
    --_track: -0.005em;
  }

  /* The smallest step sits at body size; the weight alone sets it apart. */
  .eq-heading[data-size="xs"] {
    --_size: var(--eq-text-m);
    --_leading: 1.4;
    --_track: 0;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index