Skip to content
Download ZIP

Neutral palette · 5.4 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_pricing_grid_001 · version 1.1.2 · Neutral palette
Using the PageSugar MCP server, fetch component cmp_pricing_grid_001 version 1.1.2 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.1.2
Digest
Full digest
sha256-0e78b00483eed7aafd03831830355b6b540cad1bb30abf6e411068ec076b1d73
PricingGrid.svelte Svelte · 7.2 KB Raw
<script module lang="ts">
	export interface PricingPlan {
		/** Plan name shown as the card heading. */
		label: string;
		/** Display-only price string, e.g. "$12" or "Custom". Never parsed or charged. */
		price: string;
		/** Billing period shown after the price, e.g. "per seat per month". */
		period?: string;
		/** One line on who the plan is for. */
		description?: string;
		/** Included features; an empty list renders no list. */
		features: string[];
		cta: { label: string; href: string };
		/** Lift this plan with the accent hairline, the featured shadow and a filled button. */
		recommended?: boolean;
	}
</script>

<script lang="ts">
	interface Props {
		plans: PricingPlan[];
		title: string;
		description?: string;
		/** Short label set small and uppercase above the title, e.g. "Pricing". */
		eyebrow?: string;
		headingLevel?: 2 | 3 | 4 | 5;
		recommendedLabel?: string;
	}

	let {
		plans,
		title,
		description,
		eyebrow,
		headingLevel = 2,
		recommendedLabel = 'Recommended'
	}: Props = $props();

	const uid = $props.id();
	const sectionHeading = $derived(`h${headingLevel}`);
	const planHeading = $derived(`h${headingLevel + 1}`);

	/*
	 * At most three plans per row. Two or four plans sit in two columns so no row is left with a
	 * single orphan; one plan keeps a single card's width. Three plans stack below lg rather than
	 * leave one card alone on a tablet row; five or more pair up from sm, then run three a row.
	 */
	const columns = $derived(
		plans.length === 1
			? 'max-w-md'
			: plans.length === 2 || plans.length === 4
				? 'max-w-md sm:max-w-4xl sm:grid-cols-2'
				: plans.length === 3
					? 'max-w-md lg:max-w-none lg:grid-cols-3'
					: 'max-w-md sm:max-w-4xl sm:grid-cols-2 lg:max-w-none lg:grid-cols-3'
	);
</script>

<section
	class="pricing-grid px-4 py-16 sm:px-6 sm:py-24 lg:px-8 lg:py-32"
	aria-labelledby="{uid}-title"
>
	<div class="mx-auto max-w-6xl">
		<div class="mx-auto max-w-2xl text-center">
			{#if eyebrow}
				<p
					class="pricing-grid__tracked mb-2 text-xs/none font-semibold tracking-[0.06em] text-balance text-(--_muted) uppercase"
				>
					{eyebrow}
				</p>
			{/if}
			<svelte:element
				this={sectionHeading}
				id="{uid}-title"
				class="pricing-grid__tight text-3xl/tight font-semibold tracking-[-0.02em] text-balance break-words text-(--_ink) sm:text-4xl/tight"
			>
				{title}
			</svelte:element>
			{#if description}
				<p
					class="mx-auto mt-4 max-w-xl text-base/relaxed text-pretty text-(--_muted) sm:text-lg/relaxed"
				>
					{description}
				</p>
			{/if}
		</div>

		{#if plans.length > 0}
			<ul class={['mx-auto mt-12 grid grid-cols-1 gap-6 sm:mt-16', columns]} role="list">
				{#each plans as plan, index (index)}
					<!-- Four subgrid rows (header, price, features, action) so prices and buttons line up across a row of cards. -->
					<li
						class={[
							'pricing-grid__plan row-span-4 grid min-w-0 grid-rows-subgrid gap-y-0 rounded-2xl bg-(--_surface) p-6 text-start sm:p-8',
							plan.recommended
								? 'pricing-grid__plan--featured shadow-(--_shadow-featured)'
								: 'ring-1 ring-(--_hairline)'
						]}
					>
						<div class="min-w-0">
							<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
								<svelte:element
									this={planHeading}
									id="{uid}-plan-{index}"
									class="pricing-grid__tight min-w-0 text-lg/snug font-semibold tracking-[-0.015em] break-words text-(--_ink)"
								>
									{plan.label}
								</svelte:element>
								{#if plan.recommended}
									<p
										class="rounded-full px-2 py-1 text-xs/4 font-medium break-words text-(--_ink) ring-1 ring-(--_hairline) ring-inset"
									>
										{recommendedLabel}
									</p>
								{/if}
							</div>
							{#if plan.description}
								<p class="mt-2 text-sm/relaxed text-pretty break-words text-(--_muted)">
									{plan.description}
								</p>
							{/if}
						</div>

						<p class="mt-6 flex min-w-0 flex-wrap content-start items-baseline gap-x-2 gap-y-1">
							<span
								class="pricing-grid__tight text-5xl/none font-semibold tracking-[-0.03em] break-words text-(--_ink) tabular-nums"
								>{plan.price}</span
							>
							{#if plan.period}
								<span class="min-w-0 text-sm/snug text-pretty break-words text-(--_muted)"
									>{plan.period}</span
								>
							{/if}
						</p>

						<div class="min-w-0">
							{#if plan.features.length > 0}
								<ul class="mt-8 space-y-3 text-sm/relaxed text-(--_muted)" role="list">
									{#each plan.features as feature, featureIndex (featureIndex)}
										<li class="flex gap-3">
											<svg
												class="h-lh w-4 shrink-0 text-(--_accent)"
												viewBox="0 0 16 16"
												fill="none"
												aria-hidden="true"
											>
												<path
													d="M3.5 8.5l3 3 6-7"
													stroke="currentColor"
													stroke-width="1.75"
													stroke-linecap="round"
													stroke-linejoin="round"
												/>
											</svg>
											<span class="min-w-0 break-words">{feature}</span>
										</li>
									{/each}
								</ul>
							{/if}
						</div>

						<a
							href={plan.cta.href}
							aria-describedby="{uid}-plan-{index}"
							class={[
								'pricing-grid__cta mt-8 flex min-h-11 items-center justify-center self-end rounded-lg px-4 py-2 text-center text-sm/5 font-medium break-words focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--_accent)',
								'transition-[color,background-color,box-shadow,translate] duration-150 ease-[cubic-bezier(0.2,0,0,1)] active:translate-y-px active:duration-80 motion-reduce:transition-[color,background-color,box-shadow] motion-reduce:active:translate-y-0',
								plan.recommended
									? 'pricing-grid__cta--primary bg-(--_accent) text-(--_on-accent) hover:bg-(--_primary-hover)'
									: 'pricing-grid__cta--secondary text-(--_ink) ring-1 ring-(--_control-border) ring-inset hover:bg-(--_secondary-hover)'
							]}
						>
							{plan.cta.label}
						</a>
					</li>
				{/each}
			</ul>
		{/if}
	</div>
</section>

<style>
	.pricing-grid {
		--_accent: var(--pricing-grid-accent, #18181b);
		--_on-accent: var(--pricing-grid-on-accent, #ffffff);
		--_ink: var(--pricing-grid-ink, #18181b);
		--_muted: var(--pricing-grid-muted, #52525b);
		--_hairline: var(--pricing-grid-hairline, #e4e4e7);
		--_control-border: var(--pricing-grid-control-border, #d4d4d8);
		--_surface: var(--pricing-grid-surface, #ffffff);

		/*
		 * Formulas, read by utilities in the markup. The one elevation: the recommended plan, lit
		 * from above. Its 1px accent hairline is the first layer of the same shadow, so the
		 * elevation never paints over the boundary. Button hover shifts the tone one step towards
		 * the text colour instead of fading the button.
		 */
		--_shadow-featured:
			0 0 0 1px var(--_accent), 0 1px 2px rgb(0 0 0 / 0.05), 0 12px 40px rgb(0 0 0 / 0.08);
		--_primary-hover: color-mix(in oklab, var(--_accent) 86%, var(--_on-accent));
		--_secondary-hover: color-mix(in oklab, var(--_ink) 5%, transparent);
	}

	/*
	 * :lang() has no utility. Arabic-script text is cursive; negative tracking pulls its joins
	 * apart, so it sets at 0.
	 */
	.pricing-grid:is(:lang(ar), :lang(fa), :lang(ur))
		:is(.pricing-grid__tight, .pricing-grid__tracked) {
		letter-spacing: 0;
	}
</style>

Presentational only: every label, price and feature is displayed exactly as supplied by your data, and each plan button is an ordinary link. No additional runtime packages beyond Svelte and Tailwind CSS and no backend. Point each link at your own checkout, signup or contact flow.

Suggested location
src/lib/components/pricing-grid-01
Required props
planstitle

Limitations

  • Does not create subscriptions, calculate totals, apply tax or switch currencies; prices are display strings.
  • No billing-period toggle; render a second grid or edit the source if you need one.
  • At most three plans a row. Four plans are laid out two by two; three stack below the lg breakpoint; with five, the last row holds two cards aligned to the start.
  • With an empty plans array the heading and description still render and the plan list is omitted; render your own message or hide the section if that is not what you want.
  • 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 example claims, prices and links before publishing. -->
<script lang="ts">
	import PricingGrid, { type PricingPlan } from '$lib/components/pricing-grid-01/PricingGrid.svelte';

	const plans: PricingPlan[] = [
		{
			label: 'Starter',
			price: '$9',
			period: 'per month',
			features: ['One project', 'Email support'],
			cta: { label: 'Choose Starter', href: '/signup?plan=starter' }
		},
		{
			label: 'Team',
			price: '$29',
			period: 'per month',
			features: ['Unlimited projects', 'Shared workspaces'],
			cta: { label: 'Choose Team', href: '/signup?plan=team' },
			recommended: true
		}
	];
</script>

<PricingGrid eyebrow="Pricing" title="Plans and pricing" {plans} />

The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.

Copy PricingGrid.svelte into src/lib/components/pricing-grid-01/. Replace the example plans, prices, features and destinations with your real offer. Each cta.href is an ordinary link; the grid does not create a subscription or process a payment.

PricingPlan is exported from the component’s module script. Its shape is { label, price, period?, description?, features, cta: { label, href }, recommended? }.

Layout#

The grid never holds more than three plans a row:

Plans Layout
0 The heading and description render; the plan list is omitted.
1 One card, max-w-md, centred.
2 or 4 Two columns from sm, so four plans sit two by two rather than three and one.
3 Three columns from lg; stacked below it, so no tablet row holds one card.
5, 6, … Two columns from sm, three from lg. Five plans leave two on the last row.

Below those breakpoints the cards stack at max-w-md. Each card is a four-row subgrid (header, price, features, button), so across one row of cards the prices start at the same height and the buttons share a baseline, even when descriptions or feature lists differ in length.

Retoning#

Seven variables, set on any ancestor, cover every colour the component draws:

Variable Default Draws
--pricing-grid-accent #18181b Recommended button, recommended hairline, checks, focus
--pricing-grid-on-accent #ffffff Text on the recommended button
--pricing-grid-ink #18181b Title, plan names, prices, badge text
--pricing-grid-muted #52525b Description, periods, features, eyebrow
--pricing-grid-hairline #e4e4e7 Card and badge outlines
--pricing-grid-control-border #d4d4d8 Secondary button outline
--pricing-grid-surface #ffffff Card fill

A brand accent only needs the first two:

Svelte
<div style="--pricing-grid-accent: #1d4ed8; --pricing-grid-on-accent: #ffffff;">
	<PricingGrid title="Plans and pricing" {plans} />
</div>

A worked retone for a dark page:

Svelte
<div
	class="bg-zinc-950"
	style="
		--pricing-grid-surface: #18181b;
		--pricing-grid-ink: #fafafa;
		--pricing-grid-muted: #a1a1aa;
		--pricing-grid-hairline: rgb(255 255 255 / 0.1);
		--pricing-grid-control-border: rgb(255 255 255 / 0.2);
		--pricing-grid-accent: #fafafa;
		--pricing-grid-on-accent: #18181b;
	"
>
	<PricingGrid title="Plans and pricing" {plans} />
</div>

The featured shadow is black, so on a dark page it disappears and the accent hairline carries the recommended plan on its own. That is intended; add an inset top highlight to --_shadow-featured if you want more.

White on-accent text measures about 17.7:1 on the neutral accent (#18181b) and 6.7:1 on the blue accent (#1d4ed8), using the WCAG relative-luminance formula. These figures describe those pairs only. Re-check contrast after any colour changes.

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
plansPricingPlan[]YesNonePlans in display order. Each has label, price (display string), optional period and description, features (string[], may be empty), cta { label, href } and an optional recommended flag.
titlestringYesNoneSection heading text.
descriptionstringNoNoneOptional introductory paragraph under the heading.
eyebrowstringNoNoneOptional short label set small and uppercase above the heading, such as "Pricing".
headingLevel2 | 3 | 4 | 5No2Level of the section heading; plan names use the next level down.
recommendedLabelstringNo'Recommended'Badge text shown on plans with recommended: true.

Customization#

On this page

Change content through props, retone the section through seven CSS variables (accent, on-accent, ink, muted, hairline, control border and card surface), and edit Tailwind classes in the source for spacing, radius or layout.

  • Accent: set --pricing-grid-accent and --pricing-grid-on-accent together. The accent fills the recommended button and draws the recommended card's hairline, the check marks and the focus ring; keep the on-accent text at 4.5:1 or better against it. The button's hover shade is mixed from the pair, so it follows.
  • Text: --pricing-grid-ink sets the title, plan names and prices; --pricing-grid-muted sets the description, periods and features. Keep muted at 4.5:1 against the card surface.
  • Boundaries: --pricing-grid-hairline outlines ordinary cards and the badge; --pricing-grid-control-border outlines the secondary buttons.
  • Surface: --pricing-grid-surface is the card fill. The section itself has no background; set one on its parent.
  • Dark page retone: surface #18181b, ink #fafafa, muted #a1a1aa, hairline rgb(255 255 255 / 0.1), control-border rgb(255 255 255 / 0.2), accent #fafafa, on-accent #18181b (all --pricing-grid-*). The black featured shadow disappears on dark; the accent hairline carries the emphasis. usage.md has the snippet.
  • Links: replace each cta.href with your real checkout, signup or contact route and make cta.label specific (e.g. "Choose Team").
  • Headings: set headingLevel so the section heading fits the page outline; plan names automatically use the next level.
  • Layout: the columns value at the top of the script decides the grid: one plan keeps max-w-md; two or four use two columns from sm; three use three columns from lg; five or more use two from sm and three from lg. Change those classes to alter the column count.
  • Recommended plan: mark at most one plan recommended so the emphasis stays meaningful.

Public CSS variables

VariableToken
--pricing-grid-accentaccent
--pricing-grid-on-accentonAccent
--pricing-grid-inkink
--pricing-grid-mutedmuted
--pricing-grid-hairlinehairline
--pricing-grid-control-bordercontrolBorder
--pricing-grid-surfacesurface

Accessibility#

On this page
  • Plans are a semantic list; each plan name is a heading one level below the section heading, which is set with headingLevel.
  • Each plan link is described by its plan heading via aria-describedby, so repeated labels such as "Get started" remain distinguishable.
  • The recommended plan is identified by visible badge text, not by colour or elevation alone.
  • White (#ffffff) on-accent text measures about 17.7:1 against the neutral accent (#18181b) and about 6.7:1 against the blue accent (#1d4ed8); the hover shades stay above 11:1 and 4.8:1. Muted text (#52525b) measures 7.7:1 on white. Ratios use the WCAG relative-luminance formula.
  • Links show a two-pixel focus outline in the accent colour, offset by two pixels, on :focus-visible only. The pressed state moves the button one pixel and is removed under prefers-reduced-motion.
  • Buttons are at least 44 px tall at every width, so they clear the coarse-pointer target size.
  • Checkmark icons are decorative and hidden from assistive technology.
  • The layout uses logical properties and text-start, so it mirrors under dir="rtl"; negative tracking is removed for Arabic, Persian and Urdu.
  • Element IDs come from $props.id(), so multiple grids on one page stay unique and hydrate deterministically.

Known limitations

  • Contrast ratios are computed for the shipped palettes only; any changed token must be re-checked (4.5:1 for text, 3:1 for the focus ring against the surface).
  • The secondary button's outline (#d4d4d8) is a decorative boundary under 3:1; the button is identified by its label 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.1.2 (Published) Current release · 16 September 2026

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