Skip to content
Download ZIP

Neutral palette · 5.9 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_process_steps_01 · version 1.0.0 · Neutral palette
Using the PageSugar MCP server, fetch component cmp_process_steps_01 version 1.0.0 with variant "neutral", 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
Neutral
Version
1.0.0
Digest
Full digest
sha256-f196fdae3769cfbefc07de81ceba8985dae8cb3b16ba0ce3873dbba78263f9da
ProcessSteps.svelte Svelte · 8.6 KB Raw
<script module lang="ts">
	import type { Snippet } from 'svelte';

	export interface ProcessStep {
		/** Short step name, best led by a verb: "Import your roadmap". */
		title: string;
		/** One or two sentences on what happens in this step. */
		description?: string;
		/** A 16 px inline SVG drawn with currentColor, shown in the marker when numbering is "icons". */
		icon?: Snippet;
	}

	export interface ProcessStepsAction {
		label: string;
		href: string;
	}
</script>

<script lang="ts">
	interface Props {
		/** Steps in order. More than five are always laid out as a vertical list. */
		steps: ProcessStep[];
		/** Short label set small and uppercase above the title, e.g. "How it works". */
		eyebrow?: string;
		/** Section heading. */
		title?: string;
		/** One or two sentences under the title. */
		description?: string;
		/** "auto" runs the steps in a row from 1024 px; "vertical" keeps them in a column. */
		orientation?: 'auto' | 'vertical';
		/** What the markers show: the step's number, or its icon (falling back to the number). */
		numbering?: 'numbers' | 'icons';
		/** Level of the section heading; step titles use the next level down. */
		headingLevel?: 2 | 3;
		/** Optional closing link after the steps, styled as the section's one primary button. */
		action?: ProcessStepsAction;
	}

	let {
		steps,
		eyebrow,
		title,
		description,
		orientation = 'auto',
		numbering = 'numbers',
		headingLevel = 2,
		action
	}: Props = $props();

	const uid = $props.id();
	const sectionHeading = $derived(`h${headingLevel}`);
	const stepHeading = $derived(`h${headingLevel + 1}`);
	const hasIntro = $derived(Boolean(eyebrow || title || description));

	/*
	 * A row holds two to five steps. One step, six or more, or orientation "vertical" keep the
	 * column at every width; below 1024 px every layout is the column.
	 */
	const row = $derived(orientation === 'auto' && steps.length >= 2 && steps.length <= 5);
	const rowClass = $derived(
		steps.length === 2
			? 'lg:grid-cols-2'
			: steps.length === 3
				? 'lg:grid-cols-3'
				: steps.length === 4
					? 'lg:grid-cols-4'
					: 'lg:grid-cols-5'
	);
	/* A column with an introduction sits beside it from 1024 px, so the list never strands on the left. */
	const split = $derived(!row && hasIntro && steps.length > 0);
</script>

<section
	class="process-steps px-4 py-16 sm:px-6 sm:py-24 lg:px-8 lg:py-32"
	aria-labelledby={title ? `${uid}-title` : undefined}
>
	<div
		class={[
			'mx-auto max-w-6xl',
			split &&
				'lg:grid lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] lg:grid-rows-[auto_1fr] lg:gap-x-16'
		]}
	>
		{#if hasIntro}
			<div class="max-w-2xl text-start">
				{#if eyebrow}
					<p
						class="process-steps__tracked text-xs leading-none font-medium tracking-[0.06em] text-balance text-[var(--_subtle)] uppercase"
					>
						{eyebrow}
					</p>
				{/if}
				{#if title}
					<svelte:element
						this={sectionHeading}
						id="{uid}-title"
						class={[
							'process-steps__tracked text-3xl leading-[1.15] font-semibold tracking-tight text-balance break-words text-[var(--_ink)] sm:text-4xl',
							eyebrow && 'mt-2'
						]}
					>
						{title}
					</svelte:element>
				{/if}
				{#if description}
					<p
						class={[
							'max-w-xl text-base leading-6 text-pretty break-words text-[var(--_muted)] sm:text-lg sm:leading-7',
							(eyebrow || title) && 'mt-4'
						]}
					>
						{description}
					</p>
				{/if}
			</div>
		{/if}

		{#if steps.length > 0}
			<!-- role="list" keeps list semantics in Safari, which drops them from a list with no markers. -->
			<ol
				role="list"
				class={[
					'grid grid-cols-1',
					row && ['process-steps__row', rowClass],
					split && 'lg:row-span-2',
					hasIntro && (split ? 'mt-12 sm:mt-16 lg:mt-0' : 'mt-12 sm:mt-16')
				]}
			>
				{#each steps as step, index (index)}
					<li
						class={[
							'process-steps__step relative grid min-w-0 grid-cols-[2rem_minmax(0,1fr)] gap-x-4 pb-8 last:pb-0',
							// Three shared rows from 1024 px (marker, title, description), so descriptions start on one line
							// however many lines each title takes.
							row && 'lg:row-span-3 lg:grid-cols-1 lg:grid-rows-subgrid lg:gap-x-0 lg:pe-8 lg:pb-0'
						]}
					>
						<!-- The marker repeats the list's own numbering, so it is hidden from assistive technology. -->
						<span
							class="process-steps__marker flex size-8 items-center justify-center rounded-full text-sm leading-none font-medium text-[var(--_accent)] tabular-nums ring-1 ring-[var(--_hairline)] ring-inset"
							aria-hidden="true"
						>
							{#if numbering === 'icons' && step.icon}
								{@render step.icon()}
							{:else}
								{index + 1}
							{/if}
						</span>
						<div class={['min-w-0', row && 'lg:row-span-2 lg:grid lg:grid-rows-subgrid']}>
							<svelte:element
								this={stepHeading}
								class={[
									'process-steps__tracked mt-1 text-lg leading-6 font-semibold tracking-[-0.015em] text-balance break-words text-[var(--_ink)]',
									row && 'lg:mt-6'
								]}
							>
								{step.title}
							</svelte:element>
							{#if step.description}
								<p
									class="mt-2 max-w-[30em] text-base leading-6 text-pretty break-words text-[var(--_muted)]"
								>
									{step.description}
								</p>
							{/if}
						</div>
					</li>
				{/each}
			</ol>
		{/if}

		{#if action}
			<a
				href={action.href}
				class={[
					'process-steps__action inline-flex min-h-11 max-w-full items-center rounded-lg bg-[var(--_accent)] px-4 py-2 text-sm leading-5 font-medium break-words text-[var(--_on-accent)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)]',
					steps.length > 0 ? 'mt-12 sm:mt-16' : hasIntro && 'mt-8',
					split && 'lg:mt-8 lg:self-start lg:justify-self-start'
				]}
			>
				<span class="min-w-0 [overflow-wrap:anywhere]"
					>{action.label}&#8288;<svg
						class="process-steps__arrow ms-2 inline-block size-4 align-[-0.1875rem] rtl:-scale-x-100"
						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
					></span
				>
			</a>
		{/if}
	</div>
</section>

<style>
	/* Public tokens: set --process-steps-* on this section or any ancestor to retone it. */
	.process-steps {
		--_accent: var(--process-steps-accent, #18181b);
		--_on-accent: var(--process-steps-on-accent, #ffffff);
		--_ink: var(--process-steps-ink, #18181b);
		--_muted: var(--process-steps-muted, #52525b);
		/* The third text tone, for the eyebrow only: context set quieter than the copy it introduces. */
		--_subtle: var(--process-steps-subtle, #71717a);
		--_hairline: var(--process-steps-hairline, rgb(0 0 0 / 0.12));
	}

	.process-steps__marker :global(svg) {
		width: 1rem;
		height: 1rem;
	}

	/*
	 * The rule that runs through the steps: a one-pixel line from each marker to the next, stopping
	 * 12 px short of both. In the column it runs down the markers' centre line; in the row it runs
	 * along it. Logical insets, so it follows the reading direction under dir="rtl".
	 */
	.process-steps__step:not(:last-child)::after {
		content: '';
		position: absolute;
		inset-inline-start: calc(1rem - 0.5px);
		inset-block: 2.75rem 0.75rem;
		width: 1px;
		background: var(--_hairline);
		pointer-events: none;
	}
	@media (min-width: 64rem) {
		.process-steps__row > .process-steps__step:not(:last-child)::after {
			inset-inline: 2.75rem 0.75rem;
			inset-block: calc(1rem - 0.5px) auto;
			width: auto;
			height: 1px;
		}
	}

	.process-steps__action {
		transition-property: background-color, transform;
		transition-duration: 150ms;
		transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
	}
	.process-steps__arrow {
		transition: translate 150ms cubic-bezier(0.2, 0, 0, 1);
	}
	/* Hover shifts the fill one step towards the label colour instead of fading the button. */
	.process-steps__action:hover {
		background-color: color-mix(in oklab, var(--_accent) 86%, var(--_on-accent));
	}
	.process-steps__action:hover .process-steps__arrow {
		translate: 2px 0;
	}
	.process-steps__action:hover .process-steps__arrow:dir(rtl) {
		translate: -2px 0;
	}
	.process-steps__action:active {
		transform: translateY(1px);
		transition-duration: 80ms;
	}

	/* Arabic and Hebrew are never letter-spaced; tracked text resets under right-to-left. */
	.process-steps__tracked:dir(rtl) {
		letter-spacing: 0;
	}

	@media (prefers-reduced-motion: reduce) {
		.process-steps__action:active {
			transform: none;
		}
		.process-steps__action:hover .process-steps__arrow,
		.process-steps__action:hover .process-steps__arrow:dir(rtl) {
			translate: none;
		}
	}
</style>

Presentational only: renders the steps, copy and link you pass in. It is not a stepper or wizard: nothing is selected, tracked or navigable between steps, and the action is an ordinary link to your own page. No runtime packages beyond Svelte and Tailwind CSS.

Suggested location
src/lib/components/process-steps-01
Required props
steps

Limitations

  • Not interactive: no current step, progress state or navigation between steps. Use a walkthrough or stepper component for that.
  • A row holds at most five steps. Six or more, like a single step, are always laid out as the vertical column.
  • Below 1024 px every layout is the vertical column; there is no two-column tablet row.
  • With an empty steps array the introduction and action still render and the list is omitted.
  • Icons are snippets you supply; with numbering "icons", a step without an icon shows its number.
  • 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
<!-- Illustrative content: replace the steps and link before publishing. -->
<script lang="ts">
	import ProcessSteps, { type ProcessStep } from '$lib/components/process-steps-01/ProcessSteps.svelte';

	const steps: ProcessStep[] = [
		{ title: 'Book online', description: 'Choose a time that suits you.' },
		{ title: 'Fill in your form', description: 'We email it the day before.' },
		{ title: 'Come in', description: 'Your first visit takes about an hour.' }
	];
</script>

<ProcessSteps
	eyebrow="New patients"
	title="Your first visit, step by step"
	{steps}
	action={{ label: 'Book a visit', href: '/book' }}
/>

Process steps#

A "How it works" section: numbered steps in an ordered list, with a one-pixel rule running from each marker to the next. Two to five steps sit in a row from 1024 px; below that, and for one step, six or more, or orientation="vertical", the steps run down a rail, beside the introduction on wide screens.

Icons in the markers#

Pass numbering="icons" and give each step an icon snippet holding a 16 px SVG drawn with currentColor. A step without an icon shows its number, so a mixed list still reads in order.

Svelte
{#snippet calendar()}
	<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><!-- … --></svg>
{/snippet}

<ProcessSteps numbering="icons" steps={[{ title: 'Book online', icon: calendar }]} />

Retoning for a dark band#

Svelte
<div
	class="bg-zinc-950"
	style="--process-steps-ink:#fafafa; --process-steps-muted:#a1a1aa; --process-steps-subtle:#a1a1aa; --process-steps-hairline:rgb(255 255 255 / 0.16); --process-steps-accent:#fafafa; --process-steps-on-accent:#18181b;"
>
	<ProcessSteps {steps} title="How it works" />
</div>

What it does not do#

It is not a stepper. There is no current step, no progress and no navigation between steps; the action is a plain link.

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
stepsProcessStep[]YesNoneSteps in order. Each has a title, an optional description and an optional icon snippet (16 px SVG using currentColor).
eyebrowstringNoNoneShort uppercase label above the title, such as "How it works".
titlestringNoNoneSection heading text.
descriptionstringNoNoneOne or two sentences under the title, held to a readable measure.
orientation'auto' | 'vertical'No'auto'auto lays two to five steps in a row from 1024 px and in a column below; vertical keeps the column at every width, beside the introduction from 1024 px.
numbering'numbers' | 'icons'No'numbers'What each marker shows. icons renders the step's icon snippet and falls back to the number.
headingLevel2 | 3No2Level of the section heading; step titles use the next level down.
actionProcessStepsActionNoNoneOptional closing link { label, href }, shown after the steps as the section's one filled button.

Customization#

On this page

Change content through props, retone the section through six --process-steps-* CSS variables, and edit the source for spacing, the row breakpoint or the marker size (see the guide: the marker and the rule's insets move together).

  • Accent: set --process-steps-accent and --process-steps-on-accent together. The accent colours the marker numbers and icons, fills the action and draws the focus ring; keep on-accent text at 4.5:1 against it. The hover shade is mixed from the pair.
  • Text: --process-steps-ink sets the section and step titles; --process-steps-muted sets the section description and step descriptions; --process-steps-subtle sets the eyebrow, one step quieter. Keep muted and subtle at 4.5:1 against your background.
  • Rule: --process-steps-hairline draws the line between markers and each marker's ring. It is decorative; keep it visible but quiet (8 to 16 percent black on light pages).
  • Dark page retone: ink #fafafa, muted #a1a1aa, subtle #a1a1aa, hairline rgb(255 255 255 / 0.16), accent #fafafa, on-accent #18181b (all --process-steps-*). The section has no background of its own; set one on its parent. usage.md has the snippet.
  • Layout: the row starts at the lg breakpoint (1024 px). Change the lg: classes on the list and steps, and the 64rem media query in the style block, together.
  • Marker size: the 32 px marker (size-8) is tied to the step grid's first track (grid-cols-[2rem_…]) and to the rule's insets in the style block (1rem centre line, 2.75rem start). Change all three together.
  • Copy: lead each step title with a verb and keep descriptions to one or two sentences; the row's columns are narrow at five steps.
  • Action: point action.href at your real signup, booking or contact page. Leave it out when the page already has a primary action nearby.

Public CSS variables

VariableToken
--process-steps-accentaccent
--process-steps-on-accentonAccent
--process-steps-inkink
--process-steps-mutedmuted
--process-steps-subtlesubtle
--process-steps-hairlinehairline

Accessibility#

On this page
  • Steps are an ordered list (<ol role="list">), so screen readers announce the position and count; role="list" keeps that in Safari, which drops list semantics when markers are removed.
  • The visible numbers and icons in the markers are aria-hidden, so a step is not announced as "1, 1, Import your roadmap".
  • The rule between markers is a CSS pseudo-element with no content, so assistive technology ignores it.
  • Step titles are headings one level below the section heading, set with headingLevel.
  • The action is an ordinary link with a two-pixel focus outline in the accent colour, offset by two pixels, on :focus-visible only; it is at least 44 px tall. Its pressed movement is removed under prefers-reduced-motion.
  • Muted text (#52525b) measures 7.7:1 on white and the subtle eyebrow (#71717a) 4.8:1; white on the neutral accent (#18181b) measures 17.7:1 and on the blue accent (#1d4ed8) 6.7:1.
  • Logical properties throughout: under dir="rtl" the markers, the rule and the arrow mirror, and letter-spacing resets to 0.
  • The section is labelled by its heading. Without a title it renders as an unnamed section: wrap it in your own labelled region or add aria-label in the source.

Known limitations

  • Contrast is computed for the shipped palettes only; re-check any changed token.
  • The rule and marker ring are decorative and fall under 3:1 by design; the numbers carry the meaning.

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.