Skip to content
Download ZIP

Blue eyebrow palette · 5.5 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_section_heading_01 · version 1.0.0 · Blue eyebrow palette
Using the PageSugar MCP server, fetch component cmp_section_heading_01 version 1.0.0 with variant "blue", 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
Blue eyebrow
Version
1.0.0
Digest
Full digest
sha256-c0610165b2bb2899c3c6d7eebdcc55f53000cabf47bde60786edb0b1efc6f3ef
SectionHeading.svelte Svelte · 7.4 KB Raw
<script module lang="ts">
	export type SectionHeadingLevel = 1 | 2 | 3 | 4;
	export type SectionHeadingSize = 'sm' | 'md' | 'lg' | 'xl';

	export interface SectionHeadingLink {
		/** Specific link text, such as "Browse templates"; it is read out of context. */
		label: string;
		href: string;
	}
</script>

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

	interface Props {
		/** Heading text. */
		title: string;
		/** Semantic heading level. It never changes the visual size; `size` does. */
		level?: SectionHeadingLevel;
		/** Visual size preset, independent of `level`. */
		size?: SectionHeadingSize;
		/** Short label set small and uppercase above the title, such as "Pricing". */
		eyebrow?: string;
		/** One or two sentences under the title, at most 48ch wide, under 70 characters a line. */
		description?: string;
		/** Start-aligned by default; centre only a short introduction. */
		align?: 'start' | 'center';
		/** Your own link or button. Takes precedence over `actionLink`. */
		action?: Snippet;
		/** A ready-made outline link with an arrow, used when no `action` snippet is given. */
		actionLink?: SectionHeadingLink;
		/** Heading id, so your section can point at it with aria-labelledby. Generated when omitted. */
		id?: string;
	}

	let {
		title,
		level = 2,
		size = 'md',
		eyebrow,
		description,
		align = 'start',
		action,
		actionLink,
		id
	}: Props = $props();

	const uid = $props.id();
	const headingId = $derived(id ?? `${uid}-title`);
	const tag = $derived(`h${level}`);
	const centered = $derived(align === 'center');
	const hasAction = $derived(Boolean(action || actionLink));

	/*
	 * One type ramp per size, from DESIGN.md §3.1. Leading and tracking tighten as the title grows,
	 * and the description steps up to
	 * the 18 px lead size from md. `level` never touches these.
	 */
	const titleClass = {
		sm: 'text-2xl/[1.2] tracking-[-0.02em]',
		md: 'text-3xl/[1.15] tracking-[-0.02em] sm:text-4xl/[1.15]',
		lg: 'text-4xl/[1.1] tracking-[-0.022em] sm:text-5xl/[1.1]',
		xl: 'text-4xl/[1.1] tracking-[-0.025em] sm:text-5xl/[1.05] lg:text-6xl/[1.05]'
	} as const;
	const descriptionClass = {
		sm: 'mt-3 text-base/6',
		md: 'mt-4 text-base/6 sm:text-lg/7',
		lg: 'mt-4 text-lg/7',
		xl: 'mt-4 text-lg/7'
	} as const;
</script>

<div
	class={[
		'section-heading flex flex-col gap-6',
		centered ? 'items-center text-center' : 'items-start text-start',
		!centered && hasAction && 'section-heading--split md:flex-row md:justify-between md:gap-12'
	]}
>
	<div class={['w-full min-w-0', size === 'xl' ? 'max-w-4xl' : 'max-w-3xl']}>
		{#if eyebrow}
			<p
				class={[
					'section-heading__eyebrow mb-2 text-xs/none font-semibold tracking-[0.06em] text-balance break-words text-[var(--_eyebrow)] uppercase'
				]}
			>
				{eyebrow}
			</p>
		{/if}
		<svelte:element
			this={tag}
			id={headingId}
			class={[
				'section-heading__title font-semibold text-balance break-words text-[var(--_ink)]',
				titleClass[size]
			]}
		>
			{title}
		</svelte:element>
		{#if description}
			<p
				class={[
					'section-heading__description max-w-[48ch] text-pretty break-words text-[var(--_muted)]',
					centered && 'mx-auto',
					descriptionClass[size]
				]}
			>
				{description}
			</p>
		{/if}
	</div>

	{#if action}
		<div class="max-w-full min-w-0 md:max-w-xs md:shrink-0">{@render action()}</div>
	{:else if actionLink}
		<div class="max-w-full min-w-0 md:max-w-xs md:shrink-0">
			<!--
				A block-level grid, not inline-flex: an inline box offers the row its first baseline, so a
				label that wraps would align its first line. Here the label's last line is the baseline,
				and the arrow sits level with the label's first line.
			-->
			<a
				href={actionLink.href}
				class="section-heading__link grid min-h-11 grid-cols-[auto_auto] content-center items-start justify-start gap-2 rounded-lg px-4 py-2 text-sm/5 font-medium break-words text-[var(--_ink)] ring-1 ring-[var(--_hairline)] ring-inset focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)]"
			>
				<span class="section-heading__label min-w-0 self-baseline-last md:max-w-[24ch]"
					>{actionLink.label}</span
				>
				<svg
					class="section-heading__arrow h-lh w-4 shrink-0"
					viewBox="0 0 16 16"
					fill="none"
					aria-hidden="true"
				>
					<path
						d="M3 8h9.5M8.5 4l4 4-4 4"
						stroke="currentColor"
						stroke-width="1.75"
						stroke-linecap="round"
						stroke-linejoin="round"
					/>
				</svg>
			</a>
		</div>
	{/if}
</div>

<style>
	/* Public tokens: set --section-heading-* on this element or any ancestor to retone it. */
	.section-heading {
		--_accent: var(--section-heading-accent, #1d4ed8);
		--_ink: var(--section-heading-ink, #18181b);
		--_muted: var(--section-heading-muted, #52525b);
		--_eyebrow: var(--section-heading-eyebrow, #1d4ed8);
		--_hairline: var(--section-heading-hairline, rgb(0 0 0 / 0.12));
	}

	/*
	 * Beside the text from md, the action sits on the text's last baseline: the description's last
	 * line, or the title's when there is no description. Browsers without `last baseline` fall back
	 * to bottom alignment.
	 */
	@media (min-width: 48rem) {
		.section-heading--split {
			align-items: flex-end;
			align-items: last baseline;
		}
	}

	.section-heading__link {
		transition-property: background-color, transform;
		transition-duration: 150ms;
		transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
	}
	/* Hover adds one step of ink behind the label instead of fading the link. */
	.section-heading__link:hover {
		background-color: color-mix(in oklab, var(--_ink) 5%, transparent);
	}
	.section-heading__link:active {
		transform: scale(0.98);
		transition-duration: 80ms;
	}

	.section-heading__arrow {
		transition: transform 150ms cubic-bezier(0.2, 0, 0, 1);
	}
	.section-heading__link:hover .section-heading__arrow {
		transform: translateX(2px);
	}
	/* The arrow points along the reading direction, and nudges that way on hover. */
	.section-heading__arrow:dir(rtl) {
		transform: scaleX(-1);
	}
	.section-heading__link:hover .section-heading__arrow:dir(rtl) {
		transform: scaleX(-1) translateX(2px);
	}

	/* Arabic and Hebrew are never letter-spaced and have no case; tracked text resets under RTL. */
	.section-heading__eyebrow:dir(rtl) {
		letter-spacing: 0;
		text-transform: none;
	}
	.section-heading__title:dir(rtl) {
		letter-spacing: 0;
	}
	/* Arabic sets optically smaller than Latin at the same size; the small text steps up one size. */
	.section-heading:is(:lang(ar), :lang(fa), :lang(ur)) .section-heading__eyebrow {
		font-size: 0.875rem;
		line-height: 1.25rem;
	}
	/* Arabic runs wider than Latin at the same measure, so its description line is shorter. */
	.section-heading:is(:lang(ar), :lang(fa), :lang(ur)) .section-heading__description {
		max-width: 40ch;
	}
	.section-heading:is(:lang(ar), :lang(fa), :lang(ur)) .section-heading__label {
		font-size: 1rem;
		line-height: 1.5rem;
	}
	/* CJK has no spaces to balance on: keep words whole and drop the Latin tracking. */
	.section-heading:is(:lang(ja), :lang(zh), :lang(ko)) :is(.section-heading__title, p) {
		letter-spacing: 0;
		word-break: keep-all;
		line-break: strict;
	}

	@media (prefers-reduced-motion: reduce) {
		.section-heading__link {
			transition-property: background-color;
		}
		.section-heading__link:active,
		.section-heading__link:hover .section-heading__arrow {
			transform: none;
		}
		.section-heading__link:hover .section-heading__arrow:dir(rtl) {
			transform: scaleX(-1);
		}
	}
</style>

Presentational only: it renders the text and action you pass and adds no section spacing, background or container, so put it inside your own section and give that section aria-labelledby pointing at the heading id. The built-in link is an ordinary anchor; anything else, such as a button that opens a dialog, goes through the action snippet.

Suggested location
src/lib/components/section-heading-01
Required props
title

Limitations

  • Adds no section padding, background or max width; the parent section owns those.
  • Heading levels 1 to 4 only. Pick the level that fits your page outline; the size preset never changes it.
  • One action. For a pair of buttons, render both inside the action snippet.
  • Beside the text, the action column is at most 20rem wide, so a long label or a pair of buttons wraps inside it.
  • Centred alignment always places the action below the description, never beside it.
  • Light appearance by default. The tokens retone it for a dark or tinted page, but no dark mode is declared or selected automatically.

Example

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

<section aria-labelledby="templates-heading" class="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24">
	<SectionHeading
		id="templates-heading"
		eyebrow="Templates"
		title="Start with a board, not a blank page"
		description="Launch plans, hiring pipelines and sprint boards, set up and ready to copy."
		actionLink={{ label: 'Browse templates', href: '/templates' }}
	/>
</section>

Placing it in a section#

Copy SectionHeading.svelte into src/lib/components/section-heading-01/. The heading draws text and an optional action and nothing else: no padding, no background, no container. Put it at the top of your own section and name the section after it:

Svelte
<section aria-labelledby="faq-heading" class="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24">
	<SectionHeading id="faq-heading" eyebrow="Help" title="Questions before you switch" />
	<!-- the section's content -->
</section>

Level and size#

level sets the tag and size sets the look, and neither affects the other. An h1 page introduction can use xl, a section md, a subsection inside a long page level={3} size="sm".

Size Title (phone → wide) Eyebrow gap Description
sm 24 px 8 px 16 px
md 30 → 36 px 8 px 16 → 18 px
lg 36 → 48 px 8 px 18 px
xl 36 → 48 → 60 px 8 px 18 px

Leading and tracking tighten as the title grows. All of it lives in three maps at the top of the script.

The action#

Pass actionLink={{ label, href }} for the built-in outline link with an arrow. For anything else (a filled button, a button that opens a dialog, two links) pass a snippet; it takes precedence:

Svelte
<SectionHeading title="Check-ups, cleans and everything after">
	{#snippet action()}
		<a href="/book" class="rounded-lg bg-zinc-900 px-5 py-3 text-sm font-medium text-white">
			Book a check-up
		</a>
	{/snippet}
</SectionHeading>

Start-aligned, the action sits beside the text from the md breakpoint, on the text's last baseline (the description's last line, or the title's when there is no description), and drops below the text on phones. Centred, it always sits below.

Retoning#

Variable Default Draws
--section-heading-ink #18181b Title, link label
--section-heading-muted #52525b Description
--section-heading-eyebrow #52525b Eyebrow
--section-heading-hairline rgb(0 0 0/.12) Built-in link outline
--section-heading-accent #18181b Built-in link focus ring

On a dark band:

Svelte
<div
	class="bg-zinc-950"
	style="--section-heading-ink: #fafafa; --section-heading-muted: #a1a1aa; --section-heading-eyebrow: #a1a1aa; --section-heading-hairline: rgb(255 255 255 / 0.16); --section-heading-accent: #fafafa;"
>
	<SectionHeading title="Plan the quarter where the work happens" />
</div>

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
titlestringYesNoneHeading text.
level1 | 2 | 3 | 4No2Semantic heading level. It changes the tag only, never the visual size.
size'sm' | 'md' | 'lg' | 'xl'No'md'Visual size preset: 24, 36, 48 and 60 px titles on wide screens, with leading, tracking and description size stepping with them.
eyebrowstringNoNoneShort label set small and uppercase in a paragraph before the heading, so the heading text stays clean.
descriptionstringNoNoneOne or two sentences under the title, held to 48ch, under 70 characters a line (40ch in Arabic).
align'start' | 'center'No'start'Text alignment. Centred places the action below the description.
actionSnippetNoNoneYour own link or button. Start-aligned, it sits beside the text from md on the text's last baseline, at most 20rem wide, and below it on phones. Takes precedence over actionLink.
idstringNoNoneHeading id, for aria-labelledby on your section. Generated with $props.id() when omitted.

Customization#

On this page

Change content through props, retone it through five --section-heading-* CSS variables (accent, ink, muted, eyebrow and hairline), and edit the size map at the top of the script for a different type ramp.

  • Eyebrow: --section-heading-eyebrow colours the eyebrow on its own, so a brand colour can mark it without tinting the title. Keep it at 4.5:1 against the page.
  • Text: --section-heading-ink sets the title and the link label; --section-heading-muted sets the description.
  • Link: --section-heading-hairline outlines the built-in link and --section-heading-accent draws its focus ring. The hover fill is mixed from ink, so it follows.
  • Dark page retone: ink #fafafa, muted #a1a1aa, eyebrow #a1a1aa (or #93c5fd for blue), hairline rgb(255 255 255 / 0.16), accent #fafafa (all --section-heading-*). usage.md has the snippet.
  • Sizes: the titleClass, eyebrowClass and descriptionClass maps at the top of the script hold every size decision. Change a row to change a preset.
  • Levels: set level to fit the page outline (an h1 for the page intro, h2 for sections, h3 for subsections) and size for how it should look.
  • Section: wrap it in your own section with padding and a container, and point aria-labelledby at the id you pass.
  • Action: pass actionLink for a single link, or an action snippet for a button, a filled primary or two links.

Public CSS variables

VariableToken
--section-heading-accentaccent
--section-heading-inkink
--section-heading-mutedmuted
--section-heading-eyebroweyebrow
--section-heading-hairlinehairline

Accessibility#

On this page
  • The heading is a real h1 to h4 set by level; size only changes how it looks, so the page outline stays correct at any size.
  • The eyebrow is a paragraph before the heading, not part of it, so the heading's accessible name is the title alone.
  • Pass id and point your section's aria-labelledby at it to name the section after the heading.
  • The built-in link's accessible name is its label alone, so write a label that makes sense out of context, such as "Browse templates" rather than "Learn more".
  • The link is at least 44 px tall and shows a two-pixel accent outline, offset by two pixels, on :focus-visible only. Its press scale and arrow nudge are removed under prefers-reduced-motion.
  • The arrow is decorative, hidden from assistive technology, and points along the reading direction under dir="rtl".
  • Muted text (#52525b) measures 7.7:1 on white and the blue eyebrow (#1d4ed8) 6.7:1; the title (#18181b) 17.7:1.
  • Letter-spacing and the uppercase eyebrow reset under right-to-left text. Japanese, Chinese and Korean text drops the Latin tracking and uses word-break: keep-all with strict line breaking, so lines break at spaces and punctuation rather than between any two characters; an unbroken run longer than the line still wraps.

Known limitations

  • Contrast is computed for the shipped palettes on white; re-check any retoned token (4.5:1 for text, 3:1 for the focus ring).
  • The link's outline (12% black) is a decorative boundary under 3:1; the link is identified by its label and arrow.
  • Content passed through the action snippet is yours to make accessible.

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.