Skip to content
Palette Inherited colour
Download ZIP

Inherited colour palette · 4.8 KB ZIP File receipt View as Markdown View code

Preview

Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.

Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
cmp_heading_01 · version 1.0.0 · Inherited colour palette
Using the PageSugar MCP server, fetch component cmp_heading_01 version 1.0.0 with variant "default", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.

Not connected yet? Set up the MCP server

Code

Palette
Inherited colour
Version
1.0.0
Digest
Full digest
sha256-224ec32f3f82f0b0a7ed0dd58388040192455f336dc11cc87c15d0f6c2706f0b
Heading.svelte Svelte · 4.9 KB Raw
<script lang="ts" module>
	export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
	export type HeadingSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
	export type HeadingWeight = 'medium' | 'semibold' | 'bold';
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		/** Outline level. Picks the element (h1 to h6) and nothing else. */
		level: HeadingLevel;
		/** Visual size step, independent of level. Defaults to a step that suits the level. */
		size?: HeadingSize;
		/** Font weight. Semibold by default. */
		weight?: HeadingWeight;
		/** Balance the lines of a wrapping heading. Defaults to true from xl up. */
		balance?: boolean;
		/** Short label set above the heading, outside its accessible name. */
		eyebrow?: string;
		/** Id on the heading element, for anchors and aria-labelledby. */
		id?: string;
		/** Extra classes on the outermost element (the hgroup when there is an eyebrow). */
		class?: string;
		/** The heading's content: text, plus inline pieces such as a badge. */
		children: Snippet;
	}

	let {
		level,
		size,
		weight = 'semibold',
		balance,
		eyebrow,
		id,
		class: className,
		children
	}: Props = $props();

	/*
	 * Each step pairs its size with its own leading and tracking: open at the small steps,
	 * tighter as the type grows, so a large heading is set rather than enlarged. Only the three
	 * display steps change size across breakpoints. Complete class names, never interpolated.
	 */
	const SIZES: Record<HeadingSize, string> = {
		xs: 'text-sm leading-[1.4] tracking-normal',
		sm: 'text-base leading-[1.35] tracking-[-0.011em]',
		md: 'text-lg leading-[1.3] tracking-[-0.014em]',
		lg: 'text-xl leading-[1.3] tracking-[-0.016em]',
		xl: 'text-2xl leading-[1.2] tracking-[-0.019em]',
		'2xl': 'text-[1.75rem] leading-[1.15] tracking-[-0.021em] sm:text-3xl lg:text-4xl',
		'3xl': 'text-4xl leading-[1.1] tracking-[-0.022em] sm:text-[2.75rem] lg:text-5xl',
		'4xl': 'text-[2.5rem] leading-[1.05] tracking-[-0.025em] sm:text-6xl lg:text-7xl'
	};

	const WEIGHTS: Record<HeadingWeight, string> = {
		medium: 'font-medium',
		semibold: 'font-semibold',
		bold: 'font-bold'
	};

	/** The size a level gets when no size is given. */
	const LEVEL_SIZES: Record<HeadingLevel, HeadingSize> = {
		1: '3xl',
		2: '2xl',
		3: 'xl',
		4: 'lg',
		5: 'md',
		6: 'sm'
	};

	const DISPLAY: HeadingSize[] = ['xl', '2xl', '3xl', '4xl'];

	// An out-of-range level from untyped data falls back to h2 rather than an invalid element.
	const safeLevel = $derived<HeadingLevel>(
		Number.isInteger(level) && level >= 1 && level <= 6 ? level : 2
	);
	const tag = $derived(`h${safeLevel}` as const);
	const step = $derived(size && size in SIZES ? size : LEVEL_SIZES[safeLevel]);
	const balanced = $derived(balance ?? DISPLAY.includes(step));
</script>

{#snippet heading(extra?: string)}
	<svelte:element
		this={tag}
		{id}
		class={[
			'heading__title [overflow-wrap:anywhere] text-[var(--_ink)]',
			SIZES[step],
			WEIGHTS[weight] ?? WEIGHTS.semibold,
			balanced ? 'text-balance' : 'text-pretty',
			extra
		]}
	>
		{@render children()}
	</svelte:element>
{/snippet}

{#if eyebrow}
	<hgroup class={['heading', className]}>
		<p
			class="heading__eyebrow text-xs leading-none font-medium tracking-[0.06em] text-balance [overflow-wrap:anywhere] text-[var(--_muted)] uppercase"
		>
			{eyebrow}
		</p>
		{@render heading('mt-2')}
	</hgroup>
{:else}
	{@render heading(['heading', className].filter(Boolean).join(' '))}
{/if}

<style>
	/*
	 * Public tokens: set --heading-ink or --heading-muted on the heading or any ancestor. By
	 * default the heading takes the surrounding text colour, and the eyebrow a softer mix of it,
	 * so it sits on a light page, a dark band or a tinted card without a change.
	 */
	.heading__title {
		--_ink: var(--heading-ink, currentColor);
	}

	.heading__eyebrow {
		--_muted: var(--heading-muted, color-mix(in srgb, currentColor 72%, transparent));
	}

	/* Arabic, Hebrew and other right-to-left scripts are never letter-spaced or set in caps. */
	.heading__title:dir(rtl),
	.heading__eyebrow:dir(rtl) {
		letter-spacing: 0;
		text-transform: none;
	}

	/* Arabic, Hebrew and CJK glyphs set small at 12 px; their eyebrow steps up to 13 px. */
	.heading__eyebrow:dir(rtl),
	.heading__eyebrow:lang(zh),
	.heading__eyebrow:lang(ja),
	.heading__eyebrow:lang(ko) {
		font-size: 0.8125rem;
	}

	/*
	 * Chinese, Japanese and Korean: no Latin tracking, and lines break at sensible points.
	 * Korean keeps its words whole; Japanese breaks at phrases where the browser can find them.
	 */
	.heading__title:lang(zh),
	.heading__title:lang(ja),
	.heading__title:lang(ko),
	.heading__eyebrow:lang(zh),
	.heading__eyebrow:lang(ja),
	.heading__eyebrow:lang(ko) {
		letter-spacing: 0;
	}

	.heading__title:lang(zh),
	.heading__title:lang(ja),
	.heading__title:lang(ko) {
		line-break: strict;
	}

	.heading__title:lang(ko) {
		word-break: keep-all;
	}

	@supports (word-break: auto-phrase) {
		.heading__title:lang(ja) {
			word-break: auto-phrase;
		}
	}
</style>

Pass a level for the outline and, when the default does not suit, a size for the look; the content goes in as children. It renders one native h1 to h6 (inside an hgroup when there is an eyebrow). It does not choose the level from its nesting, set a font family, add anchor links or build a table of contents.

Suggested location
src/lib/components/heading-01
Required props
levelchildren

Limitations

  • The level is always yours to choose: the component cannot see where it sits in the page, so it never infers or corrects the outline.
  • No font family is set; the heading inherits font-sans from the page. The leading and tracking are tuned for a neo-grotesque such as Inter and may want adjusting for a serif or a condensed face.
  • Colour is inherited from the surrounding text unless --heading-ink is set; there is no separate dark mode because the heading follows whatever colour its container sets.
  • The eyebrow is plain text. For a pill, an icon or a link above the heading, drop the eyebrow prop and render your own element before the component.
  • text-wrap: balance and pretty are progressive: browsers without them wrap normally. Phrase breaking for Japanese uses word-break: auto-phrase where supported (Chromium) and ordinary CJK breaking elsewhere.

Example

Svelte
<script lang="ts">
	import Heading from '$lib/components/heading-01/Heading.svelte';
</script>

<Heading level={2} size="3xl" eyebrow="Timelines" id="timelines">
	Plan the quarter on one timeline.
</Heading>

<Heading level={3} size="md">Guest access</Heading>

Heading#

level is the outline and size is the look. Pick the level from where the heading sits in the page and the size from the design, and never let one decide the other.

Svelte
<script lang="ts">
	import Heading from '$lib/components/heading-01/Heading.svelte';
</script>

<!-- A landing page section: an h2 set large, with an eyebrow. -->
<Heading level={2} size="3xl" eyebrow="Timelines" id="timelines" class="max-w-[22ch]">
	Plan the quarter on one timeline.
</Heading>

<!-- A card inside that section: an h3 set small. -->
<Heading level={3} size="md">Guest access</Heading>

The scale#

Size Font size (base · sm · lg) Leading Tracking Default for
4xl 40 · 60 · 72 px 1.05 −0.025 em
3xl 36 · 44 · 48 px 1.1 −0.022 em h1
2xl 28 · 30 · 36 px 1.15 −0.021 em h2
xl 24 px 1.2 −0.019 em h3
lg 20 px 1.3 −0.016 em h4
md 18 px 1.3 −0.014 em h5
sm 16 px 1.35 −0.011 em h6
xs 14 px 1.4 0

Headings from xl up balance their lines by default; smaller ones use text-wrap: pretty.

A badge in the heading#

Children can hold inline content. Set a badge as an inline block raised about 0.3 em so it sits level with the first line of text however the heading wraps:

Svelte
<Heading level={3} size="xl">
	Guest access
	<span
		class="ms-2 inline-block rounded-full border border-black/10 px-2 align-[0.3em] text-xs/5 font-medium tracking-normal text-zinc-600"
	>
		New
	</span>
</Heading>

The badge text becomes part of the heading's accessible name ("Guest access New"). If it should not, give it aria-hidden="true" and say the same thing elsewhere.

Colour#

The heading takes the surrounding text colour and the eyebrow a 72% mix of it. On a dark band, set the band's text colour and leave the tokens alone. To tint only the heading:

Svelte
<div style="--heading-ink: #7c2d12">
	<Heading level={2}>Sourdough out of the oven at 7</Heading>
</div>

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
level1 | 2 | 3 | 4 | 5 | 6YesNoneOutline level. Picks the element (h1 to h6) and never the size. An out-of-range value renders an h2.
size'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'NoNoneVisual size step. Omitted, the level decides: h1 3xl, h2 2xl, h3 xl, h4 lg, h5 md, h6 sm. Changing it never changes the element.
weight'medium' | 'semibold' | 'bold'No'semibold'Font weight. Semibold suits most headings; medium reads calmer at display sizes.
balancebooleanNoNoneBalance the lengths of wrapped lines. Defaults to true for xl and larger, where a lone last word shows most; false uses text-wrap: pretty.
eyebrowstringNoNoneShort label set small and uppercase above the heading. It is a separate paragraph inside an hgroup, so it is not part of the heading's accessible name.
idstringNoNoneId on the heading element itself (not the hgroup), for anchor links and aria-labelledby.
classstringNoNoneExtra classes on the outermost element: the heading, or the hgroup when there is an eyebrow. Use it for margins and max-width.
childrenSnippetYesNoneThe heading's content: its text, plus inline pieces such as a badge.

Customization#

On this page

Choose level, size and weight through props; retone the heading and eyebrow with --heading-ink and --heading-muted, or leave them to inherit the surrounding text colour. Edit the SIZES map in the source to change the scale.

  • Colour: by default the heading is the surrounding text colour and the eyebrow is a 72% mix of it, so the pair works on white, on a dark band and on a tinted card without changes. Set --heading-ink for a brand or muted heading and --heading-muted for the eyebrow, on the component or any ancestor.
  • Dark or tinted page: set the container's text colour (for example text-zinc-50 on bg-zinc-950) and the heading follows. Only set --heading-ink when the heading should differ from the body text.
  • Level and size: pick the level from the outline (one h1 per page, no skipped levels) and the size from the design. A card title is often level 3 at size md or lg; a hero is level 1 at 4xl.
  • Scale: the SIZES map at the top of the script holds each step's font size, leading and tracking as complete Tailwind classes. Edit a step there to change it everywhere; keep the leading and tracking tightening as size grows.
  • Eyebrow: it sits 8 px above the heading in the heading's own hgroup. For a coloured eyebrow set --heading-muted; keep it at 4.5:1 against the background.
  • Badges and other inline content: put them in children after the text, as an inline-block with vertical-align about 0.3em and an 8 px gap so they sit level with the first line. usage.md has an example.
  • Width: pass class="max-w-[20ch]" or similar for display headings so balanced lines stay short.

Public CSS variables

VariableToken
--heading-inkink
--heading-mutedmuted

Accessibility#

On this page
  • Renders a native h1 to h6 chosen by the required level prop, so assistive technology reads the level from the element; no role or aria-level is added.
  • Changing size or weight never changes the element, so a visual choice cannot break the outline. Choosing a level that fits the page (no skipped levels, one h1) is the consumer's responsibility.
  • The eyebrow is a paragraph before the heading inside an hgroup. It is read as ordinary text and is not part of the heading's accessible name; put it in children instead if it should be.
  • The id prop lands on the heading element, so aria-labelledby on a section names it by the heading text alone.
  • The default colour is inherited, so contrast is whatever the page sets for its text. The eyebrow mixes that colour at 72%: about 8:1 for zinc-950 on white and 10:1 for zinc-50 on zinc-950. Re-check both if you set --heading-ink or --heading-muted.
  • Long words and addresses wrap with overflow-wrap: anywhere, so a heading never forces horizontal scrolling at 320 px.
  • Letter-spacing and uppercase are removed under dir="rtl", and letter-spacing is removed for Chinese, Japanese and Korean.

Known limitations

  • An eyebrow inside hgroup is exposed as generic text in current browsers; some older screen readers announce the hgroup as a group.
  • Contrast is not checked by the component; it follows the colour of the surrounding text.

Release details#

On this page
Integration
  • Presentational
  • Works without client-side JavaScript
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 30 September 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.