Skip to content
  1. Page structure
  2. Heading

Heading

Titles for a page and for the sections inside it, with the HTML level set apart from the size.

Webhooks

Your endpoints receive an event each time an invoice in northwind changes.

Endpoints

Three endpoints receive events in live mode.

import { Heading, Subheading } from '@/components/heading'
import { Text } from '@/components/text'

export default function Example() {
  return (
    <div>
      <Heading>Webhooks</Heading>
      <Text>Your endpoints receive an event each time an invoice in northwind changes.</Text>
      <Subheading className="mt-8">Endpoints</Subheading>
      <Text>Three endpoints receive events in live mode.</Text>
    </div>
  )
}
<script setup lang="ts">
import Heading from '@/components/heading.vue'
import Subheading from '@/components/subheading.vue'
import Text from '@/components/text.vue'
</script>

<template>
  <div>
    <Heading>Webhooks</Heading>
    <Text>Your endpoints receive an event each time an invoice in northwind changes.</Text>
    <Subheading class="mt-8">Endpoints</Subheading>
    <Text>Three endpoints receive events in live mode.</Text>
  </div>
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

import clsx from 'clsx'
import type React from 'react'

type Level = 1 | 2 | 3 | 4 | 5 | 6

const headingText = 'font-semibold text-zinc-950 dark:text-white'

type HeadingProps = { level?: Level } & React.ComponentPropsWithoutRef<`h${Level}`>

export function Heading({ className, level = 1, ...props }: HeadingProps) {
  let Tag = `h${level}` as const

  return <Tag {...props} className={clsx(className, headingText, 'text-2xl/8 sm:text-xl/8')} />
}

export function Subheading({ className, level = 2, ...props }: HeadingProps) {
  let Tag = `h${level}` as const

  return <Tag {...props} className={clsx(className, headingText, 'text-base/7 sm:text-sm/6')} />
}
<script setup lang="ts">
withDefaults(defineProps<{ level?: 1 | 2 | 3 | 4 | 5 | 6 }>(), { level: 1 })
</script>

<template>
  <component :is="`h${level}`" class="font-semibold text-2xl/8 sm:text-xl/8 text-zinc-950 dark:text-white">
    <slot />
  </component>
</template>

Component API

PropDefaultDescription
Headingrenders a <h1> element
level1Rendered tag, from h1 to h6. The size does not change.
Subheadingrenders a <h2> element
level2Rendered tag, from h1 to h6. The size does not change.

Usage

Heading names the screen, once per page. Subheading names each block inside it: a card, a form section, a table. Both are a step smaller from the sm breakpoint up, which keeps dense desktop screens calm while titles stay large enough on a phone.

Page title with a primary action

Put the one action the page exists for on the same line as its title, and let the row wrap on narrow screens:

Customers

import { Button } from '@/components/button'
import { Heading } from '@/components/heading'

export default function Example() {
  return (
    <div className="flex flex-wrap items-center justify-between gap-4">
      <Heading>Customers</Heading>
      <Button>Add customer</Button>
    </div>
  )
}
<script setup lang="ts">
import Button from '@/components/button.vue'
import Heading from '@/components/heading.vue'
</script>

<template>
  <div class="flex flex-wrap items-center justify-between gap-4">
    <Heading>Customers</Heading>
    <Button>Add customer</Button>
  </div>
</template>

Title a section

A Subheading followed by a line of Text introduces a block without competing with the page title:

Payment methods

Cards and bank accounts charged when an invoice is due.

import { Subheading } from '@/components/heading'
import { Text } from '@/components/text'

export default function Example() {
  return (
    <section>
      <Subheading>Payment methods</Subheading>
      <Text>Cards and bank accounts charged when an invoice is due.</Text>
    </section>
  )
}
<script setup lang="ts">
import Subheading from '@/components/subheading.vue'
import Text from '@/components/text.vue'
</script>

<template>
  <section>
    <Subheading>Payment methods</Subheading>
    <Text>Cards and bank accounts charged when an invoice is due.</Text>
  </section>
</template>

Accessibility

Screen reader users move through a page by its headings, so the levels must follow the structure of the page, even when the visual sizes do not.

Nest a title inside a section

A card inside a section needs an h3, though it looks like any other subheading. level={3} changes the tag and keeps the size:

A card inside a section needs an h3, though it looks like any other subheading. :level="3" changes the tag and keeps the size:

Usage this month

API requests

1.2 million of 2 million included in the Scale plan.

<Subheading>Usage this month</Subheading>
<div className="mt-4 rounded-lg border border-zinc-950/10 p-4 dark:border-white/10">
  <Subheading level={3}>API requests</Subheading>
  <Text>1.2 million of 2 million included in the Scale plan.</Text>
</div>
<Subheading>Usage this month</Subheading>
<div class="mt-4 rounded-lg border border-zinc-950/10 p-4 dark:border-white/10">
  <Subheading :level="3">API requests</Subheading>
  <Text>1.2 million of 2 million included in the Scale plan.</Text>
</div>