Components

Thirteen components. Six wrap the fluid engine; seven are interface primitives on a shared WebGL2 context.

<Fluid>

The core component. Renders a WebGL fluid simulation on a canvas that fills its parent container.

Input: Mouse, touch and pen drag stroke dye and velocity into the canvas (pointerInput, on by default); mouse and pen hover splat with splatOnHover; pen pressure scales force. Keyboard has no interactive role: the canvas is decorative (aria-hidden) and the content stays fully keyboard-operable.

<script lang="ts">
  import { Fluid } from 'svelte-fluid';
  import type { FluidHandle } from 'svelte-fluid';

  let fluidRef = $state<{ handle: FluidHandle } | undefined>();
</script>

<button onclick={() => fluidRef?.handle.randomSplats(8)}>Splat</button>

<div style="height: 400px">
  <Fluid bind:this={fluidRef} curl={30} bloom shading colorful />
</div>

Accepts all FluidConfig props plus:

PropTypeDefaultDescription
widthnumber—Fixed width in CSS px. Omit to fill parent.
heightnumber—Fixed height in CSS px. Omit to fill parent.
maxPixelRationumber | nullnullCap physical pixels per CSS pixel. The default, null, uses native device DPR; pass e.g. 2 to cap on DPR 3+ displays. CSS quality tiers are unchanged.
classstring—Class on wrapper div.
stylestring—Inline style on wrapper div.
lazybooleanfalseDefer engine until the element enters the viewport. Frees the WebGL context slot. Recommended when you have 6+ instances on one page.
autoPausebooleantruePause the RAF loop when not visible or tab is hidden.
fallbackSnippet<[{ reason }]>—Custom UI rendered when WebGL is permanently unavailable. Receives the typed failure reason. Takes precedence over poster.
posterstring—Static image (object-fit: cover) shown when WebGL is unavailable and no fallback is given. A graceful still of the animation.
posterAltstring''Alt text for the poster image. Defaults to empty (decorative) — set it when the poster conveys meaning.
fallbackTextstringsee belowVisually-hidden message for the default fallback, discoverable by assistive tech (rendered alongside a poster too). Set '' to suppress for purely decorative instances.
onReady() => void—Called after the engine is constructed and its first frame is scheduled; fires again when a lazy instance rebuilds. Throwing callbacks are caught and logged.
onError(error: Error) => void—Called when engine construction fails — a WebGLUnavailableError (check .reason) or another init error. Throwing callbacks are caught and logged.
WebGL fallback (ADR-0041): when WebGL is permanently unavailable — no WebGL, or no half-float texture support — <Fluid> keeps the page intact. It renders, in order of preference, your fallback snippet, then a poster image, else it fills the box with backColor and exposes a visually-hidden fallbackText message (default: "This animation requires WebGL, which isn't available in your browser."). Short-lived failures, such as hitting the browser's live-context cap on a dense lazy page, stay blank and retry on the next scroll. The fill matches the mode: transparent stays see-through, and reveal uses the cover color and masks on any failure so hidden content is never exposed. For distortion, pass poster={yourImageUrl} so the still image stands in for the warp. Set requireHardwareAcceleration to also treat a software-only renderer as unavailable. The helper isWebGLAvailable() is exported if you want to gate rendering yourself. A frame that throws at runtime (ADR-0085) is also terminal: the engine is evicted, onError is called once, and the same fallback appears with reason 'render-failed'. It is never retried.
Reduced motion (ADR-0085): under prefers-reduced-motion: reduce every component settles the opening scene for a fixed 60 steps at 1/60 s, renders one finished frame, and stays still with no animation loop and no pointer response. FluidReveal drops its cover so content shows in full; auto-animation (autoReveal, autoDistort, autoAnimate) is skipped. The preference is followed live in both directions.

Exposes a FluidHandle via bind:this for programmatic control (splats, pause, resume, performance state).

<FluidBackground>

Full-viewport fluid behind page content. Use this as a page or screen wrapper; the canvas stays fixed to the viewport and your content is stacked above it.

Input: Pointer events are read from the window, so mouse, touch and pen movement over the page stirs the fluid while the canvas never intercepts clicks or touch scrolling. Keyboard has no interactive role: the canvas is decorative (aria-hidden) and the content stays fully keyboard-operable.

<script lang="ts">
  import { FluidBackground } from 'svelte-fluid';
</script>

<FluidBackground exclude=".card, .sidebar" splatOnHover colorful shading bloom>
  <!-- your page content -->
</FluidBackground>

DOM elements matching the exclude selector become physical "holes" — the fluid pools around them. The component observes scroll, resize, and DOM mutations to keep exclusion zones accurate.

Composition: Slot content defaults to pointer-events: none so window-level pointer input keeps feeding the fluid. Add pointer-events: auto back to interactive descendants, and keep nav/cards inside the slot when they should also be included in exclude.
<script lang="ts">
  import { FluidBackground } from 'svelte-fluid';
</script>

<FluidBackground class="fluid-screen" exclude=".site-nav, .panel" splatOnHover>
  <nav class="site-nav">...</nav>
  <main class="hero-copy">...</main>
  <section class="panel">...</section>
</FluidBackground>

<style>
  :global(.fluid-screen) {
    min-height: 100vh;
    isolation: isolate;
  }

  .site-nav,
  .panel {
    position: relative;
    z-index: 2;
    pointer-events: auto;
  }

  .hero-copy {
    position: relative;
    z-index: 1;
  }
</style>
PropTypeDefaultDescription
excludestring—CSS selector for elements to exclude. Example: ".card, .nav"
excludeRadiusnumber16Border radius of exclusion zones in CSS px.
excludePadnumber4Padding around exclusion zones in CSS px.
classstring—Class on wrapper div.
stylestring—Inline style on wrapper div.
minContrastnumber0 (off)WCAG ratio the fluid must keep against contrastColor. Use 4.5 for body text.
contrastColorRGBcontent's computed text colourText colour (0–255) to guarantee contrast against. Set it when text colours vary or change at runtime.
Contrast: Off by default so the 0.8.0 look is unchanged. With minContrast={4.5} every fluid pixel is lightened or darkened (hue kept) until it meets AA against the text colour; pixels that already pass are untouched. Glass presets are excluded because reflections composite after this pass. The text colour is read once on mount; pass contrastColor for theme switches or per-section colours (ADR-0086).

Defaults to pointerTarget='window' and splatOnHover=true. All FluidConfig props are accepted.

<FluidReveal>

The fluid acts as a reveal mask — cursor movement uncovers content behind a solid cover. Great for scratch-to-reveal effects and interactive hero sections.

Input: Mouse, touch and pen movement over the wrapper uncovers content; touch is forced harder than mouse; the first pointer or touch interaction stops auto-reveal. Keyboard has no interactive role: the canvas is decorative (aria-hidden) and the content stays fully keyboard-operable.

<script lang="ts">
  import { FluidReveal } from 'svelte-fluid';
</script>

<FluidReveal
  sensitivity={0.24}
  coverColor={{ r: 0.15, g: 0.15, b: 0.18 }}
  fringeColor={{ r: 0.6, g: 0.7, b: 0.85 }}
  accentColor={{ r: 0.2, g: 0.35, b: 0.7 }}
>
  <div>Your hidden content here</div>
</FluidReveal>
PropTypeDefaultDescription
sensitivitynumber0.1How easily areas reveal. Higher = less dye needed.
curvenumber0.5Power exponent for reveal alpha. Higher = crisper edge.
coverColorRGB{ r: 1, g: 1, b: 1 }Solid color of the cover layer (0–1 linear).
accentColorRGB{ r: 0.2, g: 0.35, b: 0.7 }Fringe accent color at reveal edges (0–1 linear).
fringeColorRGB{ r: 0.6, g: 0.7, b: 0.85 }Outer fringe color between cover and accent (0–1 linear).
fadeBackbooleantrueWhether revealed areas gradually fade back to covered.
fadeSpeednumber—Explicit dissipation value. 1.0 = permanent, 0.99 = slow fade. Overrides fadeBack.
autoRevealbooleanfalseAuto-animate a smooth looping path to reveal content before user interaction.
autoRevealSpeednumber1.0Speed of the auto-reveal animation.

Also accepts width, height, class, style, lazy, autoPause, fallback, poster, posterAlt, fallbackText, onReady, onError, and all FluidConfig props.

Note: The canvas sits on top of children for alpha compositing. Interactive elements (links, buttons) inside the slot will not receive pointer events.
Contrast: Revealed content is your own DOM, so its contrast is the text colour you choose against the page behind it. Fully revealed pixels (cover alpha under 0.1) keep at least 80% of that contrast (measured, 5th percentile). The cover and its fringe are a deliberate partial state and are not AA-guaranteed: do not put essential text only reachable through a half-revealed cover. Reduced-motion users get the content with no cover. Pick coverColor and text colour as a pair with at least 4.5:1 (ADR-0086).

<FluidDistortion>

The fluid velocity field warps an underlying image like liquid glass. Move your cursor to create ripples.

Input: Mouse, touch and pen movement over the wrapper ripples the image; the first pointer or touch interaction stops auto-distortion. Keyboard has no interactive role: the canvas is decorative (aria-hidden) and the content stays fully keyboard-operable.

<script lang="ts">
  import { FluidDistortion } from 'svelte-fluid';
</script>

<FluidDistortion src="/hero.jpg" width={640} height={360} strength={0.4} intensity={24} />
PropTypeDefaultDescription
srcstring—URL of the image to distort. Required.
strengthnumber0.4How strongly velocity warps the image. 0–1.
intensitynumber24How much dye each interaction injects.
fit'cover' | 'contain''cover'How the image fits the canvas.
scalenumber1.0Image scale. >1 zooms out, <1 zooms in.
autoDistortbooleanfalseAuto-animate distortion along a smooth looping path before user interaction.
autoDistortSpeednumber1.0Speed of auto-distort animation.
initialSplatsnumber20Random splats at startup. Creates a chaotic distortion that settles. 0 to start clean.
bleednumber60Extra canvas pixels beyond visible edges. Prevents velocity bounce at boundaries.

Also accepts width, height, class, style, lazy, autoPause, fallback, poster, posterAlt, fallbackText, onReady, onError, and all FluidConfig props.

<FluidStick>

Dye clings to text or SVG paths. The mask makes dye last longer on the shape, while a small pressure push moves fluid around it.

Input: Mouse, touch and pen drag and hover deposit dye on the canvas; the first pointer activity stops auto-animation. Keyboard has no interactive role: the canvas is decorative (aria-hidden) and the content stays fully keyboard-operable.

<script lang="ts">
  import { FluidStick } from 'svelte-fluid';
</script>

<!-- Text mode -->
<FluidStick text="FLUID" font="900 120px sans-serif" />

<!-- SVG path mode -->
<FluidStick d="M55 5 L25 45 L45 45 L20 95 L75 50 L55 50 L80 5 Z" />
PropTypeDefaultDescription
textstring—Text to render as the sticky mask.
fontstring'bold 72px sans-serif'CSS font string for text mode.
dstring—SVG path data. Takes precedence over text if both are set.
maskViewBox[n, n, n, n][0,0,100,100]viewBox for path mode.
maskFillRule'nonzero' | 'evenodd''nonzero'Fill rule for path mode.
maskResolutionnumber512Mask rasterization resolution.
maskBlurnumber4Blur radius in mask pixels. Softens edges.
maskPaddingnumber0.9How much of the texture the text fills (text mode only).
strengthnumber0.95How strongly dissipation is reduced on the mask. 1 = dye never fades.
stickyPressureAmountnumber0.15Artificial pressure on the mask to push fluid around it.
amplifynumber2.0Splat intensity multiplier on the mask.
autoAnimatebooleantrueAuto-animate a smooth looping path to deposit dye before user interaction.
autoAnimateSpeednumber2.0Speed of auto-animation.
autoAnimateDurationnumber5.0Seconds before auto-animation stops. 0 = indefinite.

Also accepts width, height, class, style, lazy, autoPause, fallback, poster, posterAlt, fallbackText, onReady, onError, and all FluidConfig props.

<FluidText>

Fluid confined inside text. Wraps <Fluid> with an svgPath container shape in text mode. Automatically measures the text so the font keeps a consistent visual height regardless of length.

Input: Inherits <Fluid> pointer input: mouse, touch and pen drag stroke the fluid inside the letterforms, and hover splats with splatOnHover. The wrapper is role="img" with the text as its label. Keyboard has no interactive role: the canvas is decorative (aria-hidden) and the content stays fully keyboard-operable.

<script lang="ts">
  import { FluidText } from 'svelte-fluid';
</script>

<FluidText
  text="SVELTE"
  height={100}
  seed={42}
  splatOnHover
  shading
  colorful
/>
PropTypeDefaultDescription
textstring—The text to render as fluid-filled letterforms. Required.
fontstring'bold 100px "Helvetica Neue", Arial, sans-serif'CSS font string for mask rasterization.
maskResolutionnumber512Mask rasterization resolution.
heightnumber—Fixed height in CSS px.
minContrastnumber4.5WCAG AA ratio the glyph outline keeps against the page at every text size. Explicit 3 is valid for large text; use 1 to disable (0.8.0 look).
contrastColorRGBnearest opaque ancestor backgroundPage colour (0–255) to guarantee contrast against. Measured once on mount; set it for gradients, images or theme switches.
Contrast: The canvas is transparent, so dim dye was indistinguishable from the page (measured 1.0:1 on every preset). A thin ~1.5 CSS px outline supplies at least 4.5:1 contrast against the page by default (WCAG halo technique); interior dye stays bit-identical. The existing jump-flood SDF keeps the band anti-aliased at native DPR. WebGL1 builds the same outline from the coverage mask. Page colour is measured on mount (any CSS colour syntax, alpha composited); consumer responsibility: page backgrounds that are images or gradients need an explicit contrastColor (ADR-0086).

Also accepts class, style, lazy, autoPause, fallback, poster, posterAlt, fallbackText, onReady, onError, and all FluidConfig props. Defaults to transparent=true.

Interface primitives

Six components for interactive UI rather than backgrounds. Each is a real native element (button, radio group, file input, switch or div) with a decorative WebGL2 surface behind or over it. They share one WebGL2 context per page (ADR-0088/0093), so a dense page does not hit the browser's context cap. Without WebGL2 each renders a plain, fully styled native control that behaves identically. They are not <Fluid> wrappers and take no FluidConfig props.

<InkPaper>

A paper-textured panel that takes watercolour from pointer, touch and pen strokes, for editorial and note-taking surfaces.

Field notes

Drag across the paper to lay colour. Pigment mixes subtractively and dries into the grain.

<script lang="ts">
  import { InkPaper } from 'svelte-fluid';
</script>

<InkPaper paper="#f4ecdc" pigments={['#2549a8', '#e7b112']} brush={{ size: 28 }} style="height: 260px; padding: 1.5rem">
  <h3 data-ink-resist>Field notes</h3>
  <button data-ink-resist data-ink-wick="0">Keep dry</button>
</InkPaper>
PropTypeDefaultDescription
paperstring#f4ecdcPaper colour, any CSS colour (normalised to opaque sRGB). Also the fallback background.
pigmentsstring[]4 built-in coloursUp to four #rgb/#rrggbb pigments, mixed subtractively. Invalid entries are dropped.
brushInkBrush{ size: 23, water: 1, pigment: null }size in CSS px (2–120), water relative load (0.1–3), pigment index or null to rotate after each pause. Omitted fields keep their value.
seednumber1Seeds the paper grain and opening wash.
childrenSnippet—Content above the paper. Descendants with data-ink-resist stay dry; focusing or hovering a data-ink-wick descendant blooms pigment (an integer value picks the pigment).
...restHTMLAttributes<HTMLDivElement>—All other div attributes are forwarded.
Input: mouse, touch and pen strokes paint (pen pressure scales the brush). Vertical touch drags still scroll the page. Keyboard: the paper takes no input; focusing a data-ink-wick control blooms pigment around it, and your children keep their own native keyboard behaviour.
Reduced motion: no painting and no animation; the paper settles to one finished still.
Contrast: children are your own DOM drawn above the paper, so text contrast is the colour you choose against paper. Pigment never covers a data-ink-resist box. Keep body text at 4.5:1 against the paper and the darkest pigment mix.
No WebGL2: the div is a flat paper colour and every child works. Shares one WebGL2 context per page with the other interface primitives (ADR-0088/0093), so many on one page do not hit the context cap.

<LiquidButton>

A button for primary actions, with a lit liquid surface that ripples when pressed.

<script lang="ts">
  import { LiquidButton } from 'svelte-fluid';
</script>

<LiquidButton tone="auto" onclick={save}>Save changes</LiquidButton>
PropTypeDefaultDescription
tone'light' | 'dark' | 'auto''auto'Palette. 'auto' measures the page colour behind the control once per change.
type'button' | 'submit' | 'reset''button'Native button type.
childrenSnippet—Label, drawn by the DOM above the liquid.
...restHTMLButtonAttributes—Every native attribute and handler (onclick, disabled, aria-*) is forwarded to the <button>.
classstring—Class on the root element.
Input: a real <button>. Click, tap, pen, Enter and Space all activate it and send one ripple (pen pressure scales it). The focus ring appears on :focus-visible and follows the shape. Minimum height 2.75rem.
Reduced motion: presses add no ripple; the surface is drawn once as a still.
Contrast: the label rides above the liquid; light added under it is clamped so the label keeps at least 4.5:1 in both tones (ADR-0092).
No WebGL2 / forced colours: a gradient-filled native button with a native outline; under forced-colors it uses system colours. Shares one WebGL2 context per page with the other interface primitives (ADR-0088/0093), so many on one page do not hit the context cap.

<LiquidSegmented>

A single-choice selector (time range, view mode) where the selected option sits on a liquid lens that sloshes across on change.

Time range

Selected: week

<script lang="ts">
  import { LiquidSegmented } from 'svelte-fluid';
  let range = $state('week');
</script>

<LiquidSegmented
  name="range"
  legend="Time range"
  options={[
    { value: 'day', label: 'Day' },
    { value: 'week', label: 'Week' },
    { value: 'month', label: 'Month' }
  ]}
  bind:value={range}
/>
PropTypeDefaultDescription
options{ value: string; label: string }[]—Required. The choices.
valuestring—Selected value; bind:value.
namestring—Required. Radio group name; unique on the page.
legendstring—Required. Visible group label (<legend>).
tone'light' | 'dark' | 'auto''auto'Palette. 'auto' measures the page colour behind the control once per change.
disabledbooleanfalseDisable the group.
classstring—Class on the <fieldset>.
Input: a <fieldset> of native radios. Tab enters the group, arrow keys move and select, and it submits in forms; click, tap and pen select an option. Focus is shown by a ring around the selected option.
Reduced motion: the lens snaps to the selection with no slosh.
Contrast: labels keep at least 4.5:1 against the surface in both tones (ADR-0092).
No WebGL2 / forced colours: a plain styled segmented control with a checked tint; system colours under forced-colors. Shares one WebGL2 context per page with the other interface primitives (ADR-0088/0093), so many on one page do not hit the context cap.

<LiquidDropZone>

A file picker that doubles as a drop target, for uploads: the zone raises a meniscus toward the pointer as a file is dragged in.

Last pick: none yet

<script lang="ts">
  import { LiquidDropZone } from 'svelte-fluid';
</script>

<LiquidDropZone
  accept="image/*"
  multiple
  label="Choose images or drop them here"
  onfiles={(files) => upload(files)}
  announce={(files) => files.length + ' images ready'}
/>
PropTypeDefaultDescription
acceptstring—Native accept list; dropped files are filtered by it too.
multiplebooleanfalseAllow several files; otherwise a drop keeps the first match.
labelstring'Choose a file or drop it here'Visible text; children replaces it.
childrenSnippet—Custom content instead of label.
onfiles(files: File[]) => void—Picked or dropped files (never empty). Exceptions are caught and logged.
announce(files: File[]) => stringcount messagePolite live-region text for a result.
tone'light' | 'dark' | 'auto''auto'Palette. 'auto' measures the page colour behind the control once per change.
disabledbooleanfalseDisable the zone.
namestring—Native input name, for form submission.
...restHTMLLabelAttributes—Other attributes go to the <label>.
Input: a <label> around a visually hidden but focusable <input type="file">. Click, tap, pen, Enter and Space open the browser's picker; drag and drop works with a mouse or pen. Results are announced in a polite live region.
Reduced motion: the meniscus snaps to its target and drops send no ripple.
Contrast: the label keeps at least 4.5:1 in both tones (ADR-0094).
No WebGL2 / forced colours: a plain styled native control; system colours under forced-colors. Shares one WebGL2 context per page with the other interface primitives (ADR-0088/0093), so many on one page do not hit the context cap.

<LiquidCaustics>

A wrapper that lays caustic light over live content where the user acts, for emphasis on cards and callouts.

Tide tables

Move the pointer over this block, or tab into it, to send a ripple of light across the text. The text stays selectable and crisp.

A focusable link
<script lang="ts">
  import { LiquidCaustics } from 'svelte-fluid';
</script>

<LiquidCaustics tone="dark" intensity={0.75} style="padding: 1.5rem; background: #14181f; color: #e8ecf4">
  <h3>Tide tables</h3>
  <p>Move the pointer or tab into the block.</p>
</LiquidCaustics>
PropTypeDefaultDescription
tone'light' | 'dark' | 'auto''auto'Palette. 'auto' measures the page colour behind the control once per change.
intensitynumber0.75Strength 0–1. Always clamped so body text keeps at least 4.5:1 against its measured background.
childrenSnippet—The live content. It is never resampled.
...restHTMLAttributes<HTMLDivElement>—All other div attributes are forwarded.
Input: pointer moves and focus send a ripple that spreads and fades over about a second. At rest nothing is drawn or scheduled. The overlay ignores pointer events, so the content stays fully interactive, selectable and zoomable.
Reduced motion: no ripples and nothing drawn; the content alone.
Contrast: dark tone adds light, light tone adds soft shade. Every visible native text run (including links and labels) limits the peak against its solid ancestor backgrounds, preserving passing 4.5:1 text (ADR-0094). Authored failing text stays unchanged, with no AA claim; the whole overlay is disabled. Gradients/images, filters, blending, opacity, shadows, styled first-letter/first-line paint, generated/embedded/shadow content, animated styling display: contents, out-of-flow/shifted descendants, negative margins, grid and list-item layouts also show plain content. An opaque root ancestor background is required; browser Canvas colours are not guessed. The static-solid contract is bounded to 128 text runs, 512 elements, 1024 nodes and 64 ancestors; overflow disables the overlay. Class/style, content, font, pointer/focus and resize changes remeasure; arbitrary CSSOM edits and external overlapping layers are not supported.
No WebGL2 / forced colours: the content alone. Shares one WebGL2 context per page with the other interface primitives (ADR-0088/0093), so many on one page do not hit the context cap.

<LiquidToggle>

A native checkbox switch for settings. Its shallow liquid lens slides between Off and On, releasing a brief wake through the existing height-field surface. Space, label activation, form submission and reset remain native. Supply a visible children label (recommended) or an explicit aria-label; no generic accessible name is supplied.

State: off

<script lang="ts">
  import { LiquidToggle } from 'svelte-fluid';
  let notifications = $state(false);
</script>

<LiquidToggle bind:checked={notifications} onchange={(on) => save(on)}>Notifications</LiquidToggle>
PropTypeDefaultDescription
checkedbooleanfalseOn/off; bind:checked.
onchange(checked: boolean) => void—Called with the new state after a user toggle. Exceptions are caught and logged.
tone'light' | 'dark' | 'auto''auto'Liquid palette and native fallback.
disabledbooleanfalseDisable the switch.
childrenSnippet—Visible label; part of the accessible name.
...restHTMLInputAttributes—Native input attributes including name, value and form are forwarded. type, role and aria-checked are fixed.
Input: a native <input type="checkbox" role="switch"> inside its visible label. Space, click, tap and pen toggle immediately. Checked inputs submit their native name/value; form reset restores the initial state. The 96×48 CSS px track is the hit area, without an additional border or button shell. Keyboard focus draws a 2 px ring.
Reduced motion: the lens snaps to the checked endpoint; Off/On remains visible.
Contrast: the Off/On label boxes use the existing 4.5:1 surface budget; the DOM label uses the inherited page colour. The focus ring uses the existing tone palette (3:1 on its reference page).
No WebGL2: the native checkbox still works; CSS shows the selected endpoint. Forced colours: a plain visible native checkbox and label, without canvas. The liquid uses the existing shared surface renderer, not a new solver.

<EnamelText>

A display heading in glazed enamel: molded, rounded relief that gives under a press and relaxes. Use it for one short headline, not body copy. Place it inside your own heading element; the heading supplies the role and size.

Harbour

<script lang="ts">
  import { EnamelText } from 'svelte-fluid';
</script>

<h2 style="font-size: 4rem; color: #d9462b">
  <EnamelText text="Harbour" />
</h2>
PropTypeDefaultDescription
textstringrequiredDisplay text: short and plain, one line or a few wrapped lines.
tone'light' | 'dark' | 'auto''auto'Studio lighting. 'auto' follows the page colour behind the text.
colorstringinherited text colourEnamel body colour, any CSS colour.
...restHTMLAttributes<HTMLSpanElement>—All other span attributes are forwarded (class, style, ...).
Input: a pointer, touch or pen press dents the enamel under the contact and it relaxes on release; pen pressure scales the dent. It is not focusable and adds no tab stop. Keyboard users see the full static relief. The text is a real <span> in the heading, so it stays selectable, findable and zoomable; presses are observed, never captured or prevented.
Reduced motion: the static relief is drawn and presses are ignored.
Contrast: glyph pixels are clamped to at least 3:1 against the page for large text (24 px, or bold 18.66 px, and up) and at least 4.5:1 otherwise (ADR-0097).
No WebGL2 / EXT_color_buffer_float: plain text in the inherited colour; the DOM text only turns transparent once the first enamel frame is on screen. Shares one WebGL2 context per page with the other interface primitives (ADR-0088/0093).