Skip to content
Download ZIP

Amber accent palette · 7.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_hero_split_media_01 · version 1.0.0 · Amber accent palette
Using the PageSugar MCP server, fetch component cmp_hero_split_media_01 version 1.0.0 with variant "amber", 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
Amber accent
Version
1.0.0
Digest
Full digest
sha256-72740cfd1f944d36c71e1f4150a15648d268136f760865fcf72ae781147be55f
SplitMediaHero.svelte Svelte · 12.7 KB Raw
<script lang="ts" module>
	export interface SplitMediaHeroAction {
		label: string;
		href: string;
	}

	/**
	 * Width and height are the file's intrinsic size. They set the frame's aspect ratio before the
	 * file loads, so nothing below the hero moves. Use alt: '' only when the media is decorative.
	 */
	export type SplitMediaHeroMedia =
		| { type: 'image'; src: string; alt: string; width: number; height: number }
		| {
				type: 'video';
				src: string;
				poster: string;
				alt: string;
				width: number;
				height: number;
		  };
</script>

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

	interface Props {
		/** Headline text. */
		title: string;
		/** Short label above the headline. */
		eyebrow?: string;
		/** Supporting paragraph under the headline. */
		description?: string;
		/** The one filled action. */
		primaryAction: SplitMediaHeroAction;
		/** Optional quieter link beside the primary action. */
		secondaryAction?: SplitMediaHeroAction;
		/** Image or muted video beside the copy. Omit it, and mediaSlot, for a text-only hero. */
		media?: SplitMediaHeroMedia;
		/** Which side the media takes from the lg breakpoint. The copy always comes first in the DOM. */
		mediaPosition?: 'start' | 'end';
		/** Replaces the built-in media, for an illustration or a picture element with art direction. */
		mediaSlot?: Snippet;
		/** Short caption set under the media. */
		mediaCaption?: string;
		/** 1 when the hero opens the page, 2 when the page already has an h1. */
		headingLevel?: 1 | 2;
		/** Accessible names for the video's pause and play button. */
		videoLabels?: { pause: string; play: string };
	}

	let {
		title,
		eyebrow,
		description,
		primaryAction,
		secondaryAction,
		media,
		mediaPosition = 'end',
		mediaSlot,
		mediaCaption,
		headingLevel = 1,
		videoLabels = { pause: 'Pause video', play: 'Play video' }
	}: Props = $props();

	const uid = $props.id();

	const hasMedia = $derived(Boolean(mediaSlot || media));
	const start = $derived(mediaPosition === 'start');
	/* Reserve the file's shape before it loads; a missing or zero size falls back to 4:3. */
	const ratio = $derived(
		media && media.width > 0 && media.height > 0 ? `${media.width} / ${media.height}` : '4 / 3'
	);

	let video = $state<HTMLVideoElement>();
	let playing = $state(false);
	/* The pause control only appears once script runs: without it, the video never starts. */
	let ready = $state(false);

	/* A refused or superseded play() leaves the label on whatever the element is actually doing. */
	function play(el: HTMLVideoElement) {
		el.play().catch(() => {
			if (video === el) playing = !el.paused;
		});
	}

	// Runs once per video element; a new src renders a new element (see the key block below).
	$effect(() => {
		const el = video;
		if (!el) return;
		ready = true;
		playing = !el.paused;
		// Safari only autoplays when the muted property itself is set, not just the attribute.
		el.muted = true;
		const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
		// Autoplay is decided here rather than with the autoplay attribute, so reduced motion never
		// sees a first frame of movement.
		if (!reduce.matches) play(el);
		// Switching reduced motion on mid-play stops the clip and returns it to its poster.
		const onChange = () => {
			if (!reduce.matches || el.paused) return;
			el.pause();
			el.load();
		};
		reduce.addEventListener('change', onChange);
		return () => {
			reduce.removeEventListener('change', onChange);
			playing = false;
		};
	});

	function toggle() {
		if (!video) return;
		if (video.paused) play(video);
		else video.pause();
	}
</script>

<section
	class="hero-split-media px-4 py-16 sm:px-6 sm:py-24 lg:px-8 lg:py-32"
	aria-labelledby="{uid}-title"
>
	<div
		class={[
			'mx-auto grid items-center gap-12 lg:gap-16',
			hasMedia
				? start
					? 'max-w-7xl lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]'
					: 'max-w-7xl lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]'
				: 'max-w-7xl'
		]}
	>
		<div class={['min-w-0 text-start', !hasMedia && 'max-w-4xl']}>
			{#if eyebrow}
				<p
					class="hero-split-media__eyebrow hero-split-media__tracked mb-2 text-xs/none font-medium tracking-[0.06em] text-balance break-words text-[var(--_muted)] uppercase"
				>
					{eyebrow}
				</p>
			{/if}
			<svelte:element
				this={headingLevel === 2 ? 'h2' : 'h1'}
				id="{uid}-title"
				class={[
					'hero-split-media__title hero-split-media__tracked text-4xl/[1.1] font-semibold tracking-[-0.022em] text-balance break-words text-[var(--_ink)] sm:text-5xl/[1.05] xl:text-6xl/[1.05]',
					// Without media the headline carries the section alone, so it takes the next step up.
					!hasMedia && 'lg:text-6xl/[1.05] xl:text-7xl/[1.05]'
				]}
			>
				{title}
			</svelte:element>
			{#if description}
				<p
					class="hero-split-media__prose mt-6 max-w-xl text-base/6 text-pretty break-words text-[var(--_muted)] sm:text-lg/7"
				>
					{description}
				</p>
			{/if}
			<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap">
				<a
					href={primaryAction.href}
					class="hero-split-media__action hero-split-media__primary inline-flex min-h-12 max-w-full items-center justify-center rounded-lg bg-[var(--_accent)] px-6 py-3 text-center text-base/6 font-medium text-[var(--_on-accent)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)]"
				>
					<span class="min-w-0 [overflow-wrap:anywhere]">{primaryAction.label}</span>
				</a>
				{#if secondaryAction}
					<a
						href={secondaryAction.href}
						class="hero-split-media__action hero-split-media__secondary inline-flex min-h-12 max-w-full items-center justify-center rounded-lg px-6 py-3 text-center text-base/6 font-medium text-[var(--_ink)] ring-1 ring-[var(--_control-border)] ring-inset focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)] sm:ring-0"
					>
						<!-- The arrow is joined to the last word (U+2060) so it follows a wrapped label. -->
						<span class="min-w-0 [overflow-wrap:anywhere]"
							>{secondaryAction.label}&#8288;<svg
								class="ms-2 inline-block size-4 align-[-0.1875em] rtl:-scale-x-100"
								viewBox="0 0 16 16"
								fill="none"
								stroke="currentColor"
								stroke-width="1.75"
								stroke-linecap="round"
								stroke-linejoin="round"
								aria-hidden="true"
							>
								<path d="M3 8h10M9 4l4 4-4 4" />
							</svg></span
						>
					</a>
				{/if}
			</div>
		</div>

		{#if hasMedia}
			<figure class={['m-0 min-w-0', start && 'lg:order-first']}>
				<div
					class="hero-split-media__frame relative overflow-hidden rounded-[var(--_frame-radius)] bg-[var(--_placeholder)]"
					style:aspect-ratio={!mediaSlot && media ? ratio : undefined}
				>
					{#if mediaSlot}
						{@render mediaSlot()}
					{:else if media?.type === 'video'}
						{#key media.src}
							<video
								bind:this={video}
								id="{uid}-video"
								class="absolute inset-0 size-full object-cover"
								src={media.src}
								poster={media.poster}
								width={media.width}
								height={media.height}
								muted
								loop
								playsinline
								preload="metadata"
								disablepictureinpicture
								aria-label={media.alt || undefined}
								aria-hidden={media.alt ? undefined : 'true'}
								onplay={() => (playing = true)}
								onpause={() => (playing = false)}
							></video>
						{/key}
					{:else if media}
						<img
							class="absolute inset-0 size-full object-cover"
							src={media.src}
							alt={media.alt}
							width={media.width}
							height={media.height}
							loading="eager"
							fetchpriority="high"
							decoding="async"
						/>
					{/if}
					<!-- The hairline sits over the media so a pale image still has an edge. -->
					<span
						class="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-[var(--_hairline)] ring-inset"
						aria-hidden="true"
					></span>
					{#if !mediaSlot && media?.type === 'video' && ready}
						<button
							type="button"
							class="hero-split-media__control absolute end-3 bottom-3 grid size-11 place-items-center rounded-full bg-zinc-950/60 text-white ring-1 ring-white/15 backdrop-blur-md"
							aria-controls="{uid}-video"
							aria-label={playing ? videoLabels.pause : videoLabels.play}
							onclick={toggle}
						>
							{#if playing}
								<svg class="size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
									<rect x="3.5" y="2.5" width="3" height="11" rx="1" />
									<rect x="9.5" y="2.5" width="3" height="11" rx="1" />
								</svg>
							{:else}
								<!-- Nudged a pixel towards the point so the triangle looks centred. -->
								<svg
									class="size-4 translate-x-px"
									viewBox="0 0 16 16"
									fill="currentColor"
									aria-hidden="true"
								>
									<path
										d="M4.5 2.9v10.2a.9.9 0 0 0 1.36.77l8.3-5.1a.9.9 0 0 0 0-1.54l-8.3-5.1a.9.9 0 0 0-1.36.77Z"
									/>
								</svg>
							{/if}
						</button>
					{/if}
				</div>
				{#if mediaCaption}
					<figcaption
						class="hero-split-media__prose mt-3 text-[13px]/5 text-balance break-words text-[var(--_muted)]"
					>
						{mediaCaption}
					</figcaption>
				{/if}
			</figure>
		{/if}
	</div>
</section>

<style>
	/* Public tokens: set --hero-split-media-* on this section or any ancestor to retone it. */
	.hero-split-media {
		--_accent: var(--hero-split-media-accent, #b45309);
		--_on-accent: var(--hero-split-media-on-accent, #ffffff);
		--_ink: var(--hero-split-media-ink, #18181b);
		--_muted: var(--hero-split-media-muted, #52525b);
		--_hairline: var(--hero-split-media-hairline, rgb(0 0 0 / 0.08));
		--_frame-radius: var(--hero-split-media-frame-radius, 16px);
		/* The frame's fill before the media paints, one step off the page in the ink colour. */
		--_placeholder: color-mix(in oklab, var(--_ink) 4%, transparent);
		/*
		 * The primary's hover mixes 12% of this colour into the accent. White lightens a near-black
		 * accent; a mid-tone accent needs black, since lightening it drops white text's contrast.
		 */
		--_accent-hover-mix: var(--hero-split-media-accent-hover-mix, #000000);
		/* The outlined secondary on phones: a control boundary at 3:1, stronger than a hairline. */
		--_control-border: color-mix(in oklab, var(--_ink) 50%, transparent);
	}

	/* The one elevation: the media frame, lit from above. */
	.hero-split-media__frame {
		box-shadow:
			0 1px 2px rgb(0 0 0 / 0.05),
			0 12px 40px rgb(0 0 0 / 0.08);
	}

	.hero-split-media__action,
	.hero-split-media__control {
		transition-property: color, background-color, box-shadow, transform, opacity;
		transition-duration: 150ms;
		transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
	}
	.hero-split-media__action:active,
	.hero-split-media__control:active {
		transform: scale(0.98);
		transition-duration: 80ms;
	}
	/* Hover shifts the tone one step instead of fading the button. */
	.hero-split-media__primary:hover {
		background-color: color-mix(in oklab, var(--_accent) 88%, var(--_accent-hover-mix));
	}
	.hero-split-media__secondary:hover {
		background-color: color-mix(in oklab, var(--_ink) 5%, transparent);
	}
	.hero-split-media__control:hover {
		background-color: rgb(9 9 11 / 0.75);
	}
	/* White fills the gap under the accent ring, so focus shows over any image. */
	.hero-split-media__control:focus-visible {
		outline: 2px solid var(--_accent);
		outline-offset: 2px;
		box-shadow: 0 0 0 2px #ffffff;
	}
	/* The control fades in once script runs; it sits over the media, so nothing moves. */
	@starting-style {
		.hero-split-media__control {
			opacity: 0;
		}
	}

	/* Arabic and Hebrew are never letter-spaced, and CJK is not tightened. */
	.hero-split-media__tracked:dir(rtl),
	.hero-split-media__tracked:is(:lang(ja), :lang(zh), :lang(ko)) {
		letter-spacing: 0;
	}
	/* Uppercase does nothing for Hebrew or Arabic, so the eyebrow takes a step up in size instead. */
	.hero-split-media__eyebrow:dir(rtl) {
		font-size: 0.8125rem;
	}
	/*
	 * Japanese and Chinese have no spaces: break between phrases where the browser can
	 * (auto-phrase), keep closing punctuation on its line (strict), and never mid-word.
	 * Korean spaces its words, so it keeps them whole.
	 */
	:is(.hero-split-media__title, .hero-split-media__prose):is(:lang(ja), :lang(zh)) {
		word-break: normal;
		word-break: auto-phrase;
		line-break: strict;
	}
	/* Every Japanese or Chinese character is a full em wide, so the display size steps down to keep
	   each clause on one line. */
	.hero-split-media__title:is(:lang(ja), :lang(zh)) {
		font-size: 2rem;
	}
	@media (min-width: 40rem) {
		.hero-split-media__title:is(:lang(ja), :lang(zh)) {
			font-size: 3rem;
		}
	}
	:is(.hero-split-media__title, .hero-split-media__prose):lang(ko) {
		word-break: keep-all;
	}

	@media (prefers-reduced-motion: reduce) {
		.hero-split-media__action:active,
		.hero-split-media__control:active {
			transform: none;
		}
	}
</style>

Presentational: renders your headline, copy, links and media exactly as supplied. It does not host, resize, transcode or optimise media, and it ships no images; supply files sized for the column (about 1400 px wide covers a 2x screen). Both actions are ordinary links. Without JavaScript the video shows its poster and does not play.

Suggested location
src/lib/components/hero-split-media-01
Required props
titleprimaryAction

Limitations

  • Ships no media. The preview's illustrations and clip are preview-only and are not part of the export.
  • One image or one video. For art direction (a different crop per breakpoint) pass a picture element through mediaSlot; the slot then owns its own sizing and aspect ratio.
  • The image loads eagerly with fetchpriority="high" because a hero image is usually the page's largest paint. Below the fold, pass the image through mediaSlot with loading="lazy" instead.
  • The video is muted and has no controls beyond pause and play. It is for short ambient loops, not for anything with speech or sound; use a video player for those.
  • Media with a white background may need its own dark version on a dark page; the component cannot recolour an image.
  • 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 SplitMediaHero from '$lib/components/hero-split-media-01/SplitMediaHero.svelte';
</script>

<SplitMediaHero
	eyebrow="Riverside Lido"
	title="Fifty metres of heated water, open all year."
	description="Lane swimming from 6:30 every morning and lessons for children from four."
	primaryAction={{ label: 'Book a lane', href: '/book' }}
	secondaryAction={{ label: 'See the timetable', href: '/timetable' }}
	media={{
		type: 'image',
		src: '/images/pool.jpg',
		alt: 'The main pool from the gallery, eight lanes with lane ropes.',
		width: 1600,
		height: 1200
	}}
/>

Split-media hero#

An opening section with the headline, copy and actions on one side and an image or a short muted video on the other. From the lg breakpoint the copy takes five parts of the width and the media seven; below it, the copy comes first and the media follows at full width.

Media#

Pass the file's own width and height. They set the frame's aspect ratio before anything loads, so the page never jumps. The image is loaded eagerly with fetchpriority="high", because a hero image is usually the largest thing on the first screen.

Svelte
<SplitMediaHero
	title="Fifty metres of heated water, open all year."
	primaryAction={{ label: 'Book a lane', href: '/book' }}
	media={{
		type: 'image',
		src: '/images/pool.jpg',
		alt: 'The main pool from the gallery, eight lanes with lane ropes.',
		width: 1600,
		height: 1200
	}}
/>

When alt can be empty#

Write alt text that says what the image shows whenever it adds something the copy does not: a product screen, the actual venue, a menu. Use alt: '' only when the image is atmosphere and the headline and paragraph already say everything; screen readers then skip it.

Video#

Svelte
media={{
	type: 'video',
	src: '/media/timeline.mp4',
	poster: '/media/timeline-poster.webp',
	alt: 'The timeline bar for Guest seats sliding two weeks later.',
	width: 960,
	height: 720
}}

The clip is always muted and loops. It starts only when the visitor has not asked for reduced motion; otherwise the poster stays until they press play. A 44 px pause and play button sits in the frame's end corner. Translate its names with videoLabels:

Svelte
videoLabels={{ pause: 'השהיית הסרטון', play: 'הפעלת הסרטון' }}

Keep clips short and silent, encode them as H.264 MP4 for every browser, and make the poster work on its own.

Art direction#

For a different crop on phones, pass a picture element through mediaSlot. It renders inside the same frame (radius, hairline, shadow), and it sets its own aspect ratio:

Svelte
<SplitMediaHero
	title="Swimming lessons for every age"
	primaryAction={{ label: 'Find a class', href: '/lessons' }}
>
	{#snippet mediaSlot()}
		<picture>
			<source
				media="(min-width: 1024px)"
				srcset="/images/pool-wide.jpg"
				width="1600"
				height="1000"
			/>
			<img
				class="block aspect-square h-auto w-full object-cover lg:aspect-[8/5]"
				src="/images/pool-square.jpg"
				alt="The learner pool from above."
				width="1000"
				height="1000"
			/>
		</picture>
	{/snippet}
</SplitMediaHero>

No media#

Omit both media and mediaSlot and the hero becomes a text-only introduction, on the same start edge as the rest of the page, with the copy held to a readable width.

Retoning for a dark page#

CSS
.my-dark-band {
	--hero-split-media-ink: #fafafa;
	--hero-split-media-muted: #a1a1aa;
	--hero-split-media-hairline: rgb(255 255 255 / 0.1);
	--hero-split-media-accent: #fafafa;
	--hero-split-media-on-accent: #18181b;
}

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
titlestringYesNoneHeadline text, set at display size.
primaryActionSplitMediaHeroActionYesNoneThe one filled action: { label, href }.
eyebrowstringNoNoneShort label set small and uppercase above the headline. Omitted, no eyebrow renders.
descriptionstringNoNoneSupporting paragraph under the headline, held to a readable measure.
secondaryActionSplitMediaHeroActionNoNoneOptional quieter link beside the primary action: { label, href }.
mediaSplitMediaHeroMediaNoNone{ type: 'image', src, alt, width, height } or { type: 'video', src, poster, alt, width, height }. Width and height are the file's intrinsic size and reserve its aspect ratio. alt may be empty only when the media is decorative. Omit media and mediaSlot for a text-only hero.
mediaPosition'start' | 'end'No'end'Which side the media takes from the lg breakpoint. Start and end follow the text direction, and the copy always comes first in the DOM.
mediaSlotSnippetNoNoneReplaces the built-in media inside the same frame, for an illustration or a picture element with art direction. The snippet sets its own size.
mediaCaptionstringNoNoneShort caption set under the media.
headingLevel1 | 2No11 when the hero opens the page; 2 when the page already has an h1.
videoLabels{ pause: string; play: string }No{ pause: 'Pause video', play: 'Play video' }Accessible names for the video's pause and play button. Translate them with the rest of the page.

Customization#

On this page

Change content through props, retone the section through seven --hero-split-media-* CSS variables, and edit Tailwind classes in the source for the column split, spacing or type scale.

  • Accent: set --hero-split-media-accent and --hero-split-media-on-accent together, keeping on-accent at 4.5:1 or better. The accent fills the primary action and draws focus rings.
  • Hover: the primary's hover fill mixes 12% of --hero-split-media-accent-hover-mix into the accent. White (the default) lightens a near-black accent; set #000000 for a mid-tone accent such as the blue and amber palettes, because lightening it lowers contrast with white text.
  • Text: --hero-split-media-ink sets the headline and the secondary link; --hero-split-media-muted sets the eyebrow, paragraph and caption. Keep muted at 4.5:1 against your page.
  • Media frame: --hero-split-media-frame-radius rounds the frame (16px by default; 0 for square corners) and --hero-split-media-hairline draws the one-pixel edge over the media. The frame's resting fill is mixed from the ink colour, so it follows a retone.
  • Dark page retone: ink #fafafa, muted #a1a1aa, hairline rgb(255 255 255 / 0.1), accent #fafafa, on-accent #18181b (all --hero-split-media-*). The black shadow under the frame disappears on dark; the hairline carries the edge.
  • Column split: the grid's lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] gives the media the larger share. Change both fractions (and the mirrored pair for mediaPosition start) to 1fr for an even split.
  • Media: supply width and height from the file itself so the frame reserves the right space. Use object-cover crops; for a different crop on phones, pass a picture element through mediaSlot.
  • Video: keep clips short, silent and under a few megabytes, with a poster that works on its own, because reduced-motion visitors see the poster unless they press play.
  • Headings: set headingLevel to 2 when the page already has an h1.

Public CSS variables

VariableToken
--hero-split-media-accentaccent
--hero-split-media-on-accentonAccent
--hero-split-media-accent-hover-mixaccentHoverMix
--hero-split-media-inkink
--hero-split-media-mutedmuted
--hero-split-media-hairlinehairline
--hero-split-media-frame-radiusframeRadius

Accessibility#

On this page
  • The section is labelled by its headline, which is an h1 by default; set headingLevel to 2 when the page already has one.
  • The copy comes before the media in the DOM at every width, so reading and tab order put the headline and actions first. mediaPosition moves the media with CSS only.
  • Image alt text comes from you. Describe what the image shows when it carries meaning; pass alt: '' only when it is decoration and the copy already says everything.
  • A video with alt text takes it as its accessible name (aria-label); with alt: '' it is hidden from assistive technology. It has no audio track expectation and is always muted.
  • The video autoplays only when prefers-reduced-motion is not set, and stops and returns to its poster if the setting is switched on while it plays. With reduced motion, the poster stays until the visitor presses play (WCAG 2.2.2).
  • The pause and play button is a 44 px native button whose accessible name swaps between videoLabels.pause and videoLabels.play; it names the video through aria-controls. It appears once JavaScript runs, since without it the video never moves.
  • Links and the video button show a two-pixel accent focus ring on :focus-visible only; over media the ring sits on a white band so it shows against any image.
  • White on-accent text measures 17.7:1 on the neutral accent (#18181b), 6.7:1 on blue (#1d4ed8) and 5.0:1 on amber (#b45309). The hover fills keep it above 4.5:1: the neutral hover lightens (about 12:1), blue and amber darken (about 7.9:1 and 6.1:1). Muted text (#52525b) measures 7.7:1 on white. Ratios use the WCAG relative-luminance formula.
  • The layout uses logical properties and text-start, so it mirrors under dir="rtl"; tracking resets to 0 for right-to-left and CJK text, and CJK headlines keep words whole.
  • Element IDs come from $props.id(), so two heroes on one page stay unique.

Known limitations

  • Contrast ratios are computed for the shipped palettes only; re-check any changed token (4.5:1 for text, 3:1 for the focus ring).
  • The video button sits over the media on a translucent dark fill; over a very dark clip its boundary is the white/15 hairline and the white icon, not the fill.
  • The component cannot check that alt text is accurate or that a video is free of flashing content; both are the consumer's responsibility.

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.