Image banners, focal points, scrims, and textures

An Espalier banner is one responsive image covered into a ratio-defined frame. The focal point preserves the meaningful region as that frame changes. A scrim provides text legibility; an optional texture provides style. These are separate layers so decoration can never be mistaken for the contrast mechanism.

This guide is also the committed visual contract for the banner presets. The component implementation, examples, and browser review use the exact values below.

Layer order

From back to front, banner mode renders the media, scrim, texture, the optional flush edge, and real overlay content. The scrim, texture, and flush edge ignore pointer input and accessibility APIs. Overlay content stays semantic and interactive.

banner-scheme selects the banner's polarity and defaults to the active scheme. A dark banner darkens the image behind light ink; a light banner lifts the image toward paper behind dark ink. The default scrim, texture, and overlay inks derive from the theme's semantic tokens (--esp-color-background for surfaces, --esp-color-headings for overlay text) with the lightness pinned per polarity, so a surrounding context re-tints them and an explicit banner-scheme still reads on any page scheme:

Polarity Scrim/texture ink Overlay ink
dark Background hue pinned to oklch lightness 0.13/0.12 Headings hue pinned to lightness 0.97
light Background hue pinned to lightness 0.97/0.98 Headings hue pinned to lightness 0.2

When the banner polarity matches the page scheme the theme's colors already have the right polarity, so the defaults use them verbatim: the scrim is exactly --esp-color-background and the overlay ink exactly --esp-color-headings. The pinned derivations in the table apply only to the cross-scheme override case.

Overlay text also carries a soft polarity-matched text-shadow halo by default — dark ink gets a light halo, light ink a dark one — overridable (or removable with none) through --esp-image-overlay-text-shadow.

On light banners the darkening texture blends flip to their lightening counterparts: halftone and grunge render with screen instead of multiply.

Scrim recipes

--esp-image-scrim-color defaults to the polarity ink above (with a oklch(0.12 0.015 255) fallback outside a theme root). Each recipe uses the selected strength as the opacity of the whole scrim layer:

Strength Opacity
soft 0.36
medium 0.56
strong 0.76

The public --esp-image-scrim-opacity token overrides those values. Each anchor has a base recipe:

Scrim Base recipe
flat Solid scrim color across the frame.
top to bottom: color 0% 20%, 68% color mix at 48%, transparent at 78%.
bottom to top: color 0% 20%, 68% color mix at 48%, transparent at 78%.
left to right: color 0% 20%, 68% color mix at 48%, transparent at 78%.
right to left: color 0% 20%, 68% color mix at 48%, transparent at 78%.
radial Circle at center: color 0%, 72% mix at 34%, 28% mix at 64%, transparent at 82%.

The scrim follows its content

The base recipe is a minimum. The component measures the overlay content against the frame and stretches the full-strength region until it covers the content box, so a three-line heading on a short card cover sits on the scrim at full strength instead of running up into the fade:

  • A directional scrim holds full strength from its anchored edge to the far edge of the content (padding included), or to 20%, whichever is further. The fade keeps its proportions in the space left: the 68% stop sits 35% and the transparent stop 72.5% of the way from the end of that plateau to the frame's opposite edge. With a plateau of exactly 20% that is the base recipe, so an overlay that already fits the band looks as it always has.
  • A radial scrim grows an opaque core out to the content box's farthest corner, measured along the center-to-corner ray, and scales its three fade stops into the rest of that ray. A zero core is the base recipe. On a wide banner a long centered heading can push that core out close to the frame's edges.
  • A directional scrim only follows content that starts in its own half of the frame, and a radial scrim only content that covers the frame's center. An explicit scrim="top" over bottom-start content, or scrim="radial" over it, keeps the base recipe rather than turning into a veil over the whole photograph.

The measurement tracks the banner's size, its compact-ratio, the overlay padding, content-position, and every element slotted into the overlay, so fonts loading or the text changing re-measure it. It watches boxes, not styles, so two changes wait for the next resize of anything it watches: redistributing the overlay padding without changing its total (for example 24px 12px 0 to 0 12px 24px), and content changing inside a display: contents wrapper, whose own children are what it watches. Slot the heading itself, rather than a wrapper, when its content will change. It runs from a ResizeObserver after layout and only moves gradient stops, which never feeds back into layout. Before the first measurement, the base recipe renders. prefers-reduced-transparency, forced-colors, and print handle the scrim exactly as before.

With scrim="auto", an empty overlay resolves to none; top and bottom content positions resolve to the matching anchor; middle-start and middle-end resolve to the directional scrim on that inline edge (left and right in left-to-right text, mirrored in right-to-left); center resolves to radial; and copy in the compact band (below) resolves to none. An explicit scrim always wins. The measurement covers every piece of overlay copy, so a long supporting sentence or the action sits on full-strength scrim, not just the headline.

Overlay copy

Positions

content-position places the copy with logical directions, so start and end mirror in right-to-left text:

start center end
top top-start top top-end
middle middle-start center middle-end
bottom bottom-start (default) bottom bottom-end

The copy stack

The overlay takes three slots, rendered in this order whatever the light-DOM order:

Slot Content
overlay The headline: a heading, or the span an editor emits.
overlay-supporting One optional supporting sentence.
overlay-action At most one native a or button. A second logs a console warning.

A headline on its own lays out exactly as it always has. Once supporting copy or an action is present the overlay lays the copy out as one stack:

  • Grouped spacing. The stack owns the space between its pieces: the slotted copy's block margins are zeroed, supporting copy sits --esp-image-overlay-gap (default var(--esp-size-small)) under the headline, and the action sits twice that under the copy above it. Spacing only separates pieces, so supporting copy or an action with nothing above it starts at the overlay padding.
  • Headline prominence. Supporting copy defaults to the lead type role (--esp-type-lead-font-size and --esp-type-lead-font-weight, body font, line height 1.4); the headline keeps its own size. Page styles on the slotted elements win over these defaults.
  • A measure. The stack's line length is bounded by --esp-image-overlay-measure, 36rem by default, so long copy does not run the width of a wide banner. Set the token on a headline-only banner to bound that too (its default there is none).
  • One accessible action. The action stays the consumer's native link or button, drawn in the overlay ink: a 2px border, a 2.75rem minimum target, and a two-tone :focus-visible ring (an ink outline over a halo of the scrim ink) that reads over any photograph. Page styles win here too, so an application can restyle it; keep a visible focus indicator if you do.
  • Contrast for the whole stack. The scrim measurement and the overlay halo cover every piece of copy.

Compact placement

compact-placement decides where the copy goes below the compact width (--esp-image-compact-width, 40rem by default):

Value Effect
overlay (default) The copy stays over the image.
below The copy moves into a band under the image.

A crop that keeps its subject at narrow widths often loses the quiet region the copy sat on; below is the fallback for that. The image keeps the compact ratio (or ratio) in its own box and the band follows it, painted with the banner scheme's scrim ink as its surface and the overlay ink as its text, with no halo. An auto scrim resolves to none there; an explicit scrim still paints over the image. With an explicit host block size the band keeps its natural height at the bottom and the image takes the rest.

Growth, enlarged text, and long labels

A banner's ratio sizes it exactly as before while its copy is not clipped. When the copy's text would be cut off by the ratio's frame (enlarged text at 200%, a long localized label, a narrow column), the banner grows to fit the copy with its spacing, and the image covers the grown frame; once the copy fits again with its spacing, the ratio takes back over. Copy whose padding or margins overflow while its text still shows only sits closer to the edge, as it always has. Words too long for a line wrap inside the copy rather than overflowing it.

Only a frame the ratio sizes grows, and only when growing takes effect. An explicit block size, a max-block-size or min-block-size that decides the height, or a grid or flex container that stretches or shrinks the banner still wins, as it always has. So does a cap the grown copy would exceed, and a banner whose width follows its content (growing would only widen it): the banner keeps its ratio, and copy past the frame is clipped. A cap is checked whenever the copy or the banner resizes and on every viewport resize, so a cap that tightens on a grown banner (a viewport-height cap as the window shrinks, or one a script changes) hands the growth back. A banner a cap held back tries again once a viewport resize settles, whatever kind of cap it is, and when its copy gets shorter. Cap a banner only where you know the copy fits. Hidden overlay elements and display: contents wrappers count by their visible contents.

Soft height

A real max-block-size or block-size is a hard limit: copy that does not fit is clipped. When the space a banner should fill is known but the copy is not (a generator capping banners to the first fold, say), use the soft properties instead:

Property Effect
--esp-image-banner-max-block-size A soft cap. The banner is the smaller of its ratio height and the cap.
--esp-image-banner-block-size A soft target. It replaces the ratio as the banner height (a viewport-fill banner). With both set, the cap limits the target.

While the copy fits, a soft property lays the banner out exactly like the matching hard one, focal crop and image motion included. When the copy, with its spacing, would not fit, the banner grows to fit it instead of clipping it, and gives the soft height back once the copy fits again. In the compact band (compact-placement="below") the soft properties size only the image box, and the band adds below it, so band copy is never clipped either. A real max-block-size, block-size, or min-block-size keeps its hard meaning, alongside a soft one too, and a banner with neither soft property behaves exactly as before. To switch a soft property off (in a media query, say), set it to none, auto, or initial. A hard limit beside a soft height assumes no border or block padding on the banner element (banners have none by default); with them, the frame takes the host's limit as its own.

/* Cap every banner at the first fold without ever clipping its copy. */
esp-image[banner] {
  --esp-image-banner-max-block-size: calc(100svh - var(--site-header-height));
}

Use lengths (including viewport units and calc()), not percentages: the soft height sizes a box inside the banner, where a percentage has nothing to resolve against. The element reads both properties from computed style, so a media query or a script can set them; a change takes effect on the next resize of the banner, its copy, or the viewport. Lengths only, for hard limits beside them too: a percentage max-block-size or min-block-size on a soft-height banner has no definite block to resolve against inside it. The static stylesheet (below) reads the same properties.

Image motion

Copy never moves with the image. The overlay is a sibling of the media box, not inside it, so a transform or animation on the projected img/picture or on ::part(image) (parallax, drift, slow zoom) leaves the copy where it is, and the media box clips its own overscan so a zooming image never paints over the compact band. Under prefers-reduced-motion: reduce, animations and transitions on the projected banner media and on ::part(image) stop. Motion a script drives, such as a scroll-linked fallback, must check the preference itself.

Flush edge

The strength above is the opacity of the whole scrim layer, so a directional scrim's anchored stop is capped by it: even strong leaves 24% of the photograph showing at the very edge the scrim is anchored to.

scrim-edge lifts that cap for the edge alone.

Value Effect
none (default) The scrim is capped by its strength everywhere.
flush The anchored edge paints the scrim ink at full alpha and relaxes to the ordinary strength over a short band.

flush applies to the four directional anchors — top, bottom, left, right — and is keyed off the resolved scrim, so an auto scrim that resolves to a direction picks it up. It is inert for none, flat, and radial, where a full-alpha edge would mean a solid block over the whole photograph or its centre.

Composited alpha is b(x) + s·g(x)·(1 − b(x)): the edge layer's own mask b over the scrim, where s is the strength and g the gradient's alpha profile at that point. Inside the gradient's opaque plateau — at least the first 20% of the axis for all four anchors, and further when the content reaches past it — g is 1 and the expression is a straight line from 1 at the edge to s at the band's end. The 12% default sits inside that plateau. A band set deeper than the plateau still relaxes monotonically, but it follows the gradient's own falloff rather than a straight line. Above the band the scrim is untouched either way, so flush is an edge treatment and never a heavier veil over the subject.

The flush edge paints as a separate layer above the texture. Keeping the existing scrim layer preserves its rendering: folding opacity into gradient stops produced differences of up to 1/255 in measured pixels. Those differences would fail the existing visual baselines.

Token Default Effect
--esp-image-scrim-edge-depth 12% How far the edge takes to relax to the ordinary strength, along the gradient axis. Past the gradient's opaque plateau (at least 20%) the relax is no longer a straight line.
--esp-image-scrim-edge-image none A background image painted over the flush edge and faded out with it.
--esp-image-scrim-edge-size auto Background size for that image.

The join is exact because both sides paint the same token: when a banner's polarity matches the page scheme its scrim ink is --esp-color-background, which is what esp-section paints its band with. A banner pinned to the opposite polarity with banner-scheme keeps the lightness-pinned derivation, which is not the band's colour, so its flush edge reads as a deliberate colour break rather than a dissolve.

The image token exists so a textured band can carry across the join. The component anchors its own side of the tile to the scrim's edge, so the banner's last tile row ends exactly at the join; the adjoining band has to match the background-size and anchor its own background to the meeting edge, with background-origin: border-box so the phase does not depend on the section's padding.

The strong preset is intended to clear APCA Lc 75 for body text and Lc 60 for headings over a typical mid-key image. No fixed overlay can guarantee contrast over arbitrary photography, so the consumer must review its actual image and may override color or opacity.

Procedural texture recipes

CSS-gradient textures use a shared pitch selected by texture-scale. The public --esp-image-texture-scale length overrides the selected pitch per instance.

Scale Pitch
fine 4px
medium 8px
coarse 14px

Dot and line geometry scales with the pitch so every scale reads as a screen rather than vanishing specks:

Texture Exact recipe Default opacity Blend mode
dots Tiled radial dot: ink through pitch × 0.17, transparent 0.35px later. 0.8 normal
halftone Larger dot tile (ink through pitch × 0.22), always masked — from opaque at the active scrim anchor (bottom, by default) to transparent at 82%. 0.9 multiply (screen on light banners)
scanlines Repeating line along the block axis: ink through pitch × 0.3, transparent through the selected pitch. 0.35 normal
duotone 135deg gradient from the shadow token at 0% 42% to the highlight token at 100%. 0.9 color

--esp-image-texture-color defaults to the polarity ink. Duotone defaults derive from the theme: the shadow stop is the primary hue pinned to oklch lightness 0.32, the highlight stop is the complementary hue pinned to 0.84.

SVG textures use discrete shared filters; CSS lengths cannot change an SVG filter's frequency. Because CSS filter: url(#id) only resolves inside the referencing element's own tree scope, each banner's shadow root receives one copy of these definitions (installed lazily when an SVG texture is active):

The three SVG presets are deliberately pushed to opposite ends so they read distinctly: paper is a low-frequency, many-octave mottle with a contrast stretch (a soft cloudy surface); grain is a very-high-frequency, one-octave speckle (fine film grain); grunge thresholds the noise into sparse distressed marks.

Texture Scale feTurbulence baseFrequency numOctaves Post-processing Opacity Blend
paper fine fractalNoise 0.016 6 saturation 0 + contrast 2.6 0.9 overlay
paper medium fractalNoise 0.011 6 saturation 0 + contrast 2.6 0.9 overlay
paper coarse fractalNoise 0.007 6 saturation 0 + contrast 2.6 0.9 overlay
grain fine fractalNoise 0.65 1 saturation 0 + contrast 1.7 0.72 overlay
grain medium fractalNoise 0.5 1 saturation 0 + contrast 1.7 0.72 overlay
grain coarse fractalNoise 0.38 1 saturation 0 + contrast 1.7 0.72 overlay
grunge fine fractalNoise 0.14 2 threshold matrix below 0.42 multiply (screen on light banners)
grunge medium fractalNoise 0.09 3 threshold matrix below 0.42 multiply (screen on light banners)
grunge coarse fractalNoise 0.05 4 threshold matrix below 0.42 multiply (screen on light banners)

Both grain and paper stretch contrast with a per-channel linear transfer (slope from the table, intercept = (1 − slope) / 2 so mid-gray stays fixed while deviations widen). Without it the desaturated noise clusters near mid-gray and washes out under the blend — grain barely reads and paper looks like faint grain instead of a distressed surface. Grain also stays at a visible ~2px grain size: sub-pixel frequencies average out to nothing.

The SVG texture layer paints an opaque white source; the referenced filter replaces that paint with generated noise, so the layer never renders as a solid fill.

Blend modes only composite with the photo when no ancestor between the texture layer and the image creates a stacking context; the banner frame isolates the whole stack instead, so blends reach the photo but never the page behind the component.

The grunge threshold matrix derives its marks from the noise alpha channel (alpha' = 0.95 − 1.4·A, keeping only the sparse low-alpha tail — an RGB-sum threshold passes almost nothing over real noise output), with the mark color a constant offset L per polarity variant. Because the filter paints the marks itself, grunge ships two variant sets: grunge-* with black marks (L = 0, for multiply on dark banners) and grunge-light-* with white marks (L = 1, for screen on light banners — screen with black marks would be a no-op):

0 0 0 0 L
0 0 0 0 L
0 0 0 0 L
0 0 0 -1.4 0.95

It is followed by a discrete alpha transfer with table values 0 0 0 1 1. All filters use seed="17", stitchTiles="stitch", and x/y/width/height of -10%/-10%/120%/120%.

Reference sheets

The sheets below are captured by node scripts/capture-banner-reference.js from scripts/fixtures/esp-image-banner-reference.browser.html, which renders real esp-image banners against the current build; the same run pixel-asserts the SVG-filter textures over flat media before writing the sheets. Each row shows the fine, medium, and coarse variation over the repository's existing, provenance-cleared bright and dark canvas images.

Bright-photo banner preset reference

Dark-photo banner preset reference

Focal point, ratios, and server rendering

focus is a normalized point. focus="0 0" is the logical top-left of the image, focus="1 1" is the bottom-right, and the default is 0.5 0.5. The public --esp-image-object-position token has final precedence.

In JavaScript the normalized object is .focusPoint, not .focus: preserving the platform's callable HTMLElement.focus() method keeps the component focusable through ordinary DOM APIs. The HTML and storage contract remains the compact focus="x y" serialization.

ratio accepts a positive number or <number> / <number>. compact-ratio replaces it when the component's own width is below --esp-image-compact-width (default 40rem). An explicit host block size wins over either ratio.

Before the component upgrades, a server renderer that needs focal positioning must set the equivalent CSS itself, for example:

<esp-image banner ratio="3/1" focus="0.7 0.35" style="--esp-image-static-ratio: 3 / 1; --esp-image-object-position: 70% 35%;">
  <img src="/banner.jpg" alt="A person standing beside a greenhouse" />
</esp-image>

Before the component upgrades

Server-rendered banner markup paints with page styles alone until Espalier's module defines esp-image. The static stylesheet lays the same markup out in that window, so nothing shifts when the element upgrades:

<link rel="stylesheet" href="/node_modules/@taprootio/espalier/css/image/banner-static.css" />
<!-- or, through a bundler: import "@taprootio/espalier/image/banner-static.css"; -->

<esp-image
  banner
  ratio="3/1"
  compact-ratio="3/2"
  compact-placement="below"
  content-position="middle-start"
  focus="0.65 0.4"
  style="--esp-image-static-ratio: 3 / 1; --esp-image-static-compact-ratio: 3 / 2; --esp-image-object-position: 65% 40%;"
>
  <img src="/banner.jpg" alt="A person standing beside a greenhouse" />
  <h2 slot="overlay">Grow somewhere wonderful</h2>
  <p slot="overlay-supporting">Plan around the light you already have.</p>
  <a slot="overlay-action" href="/start">Start planning</a>
</esp-image>

Every rule is scoped to esp-image[banner]:not(:defined), so it stops applying the moment the element upgrades. It honors the soft height properties (above) the same way the element does where container style queries exist (Chrome, Safari 18+); elsewhere a soft height applies at upgrade, and a soft cap on a banner whose copy fits lands then. A soft cap needs --esp-image-static-ratio to apply before upgrade, as the element needs a ratio. It reproduces the frame ratio, focal cropping, all nine positions, the copy stack and its measure, growth, and the compact band, from the same attributes. CSS cannot read a ratio out of an attribute, so the server also sets:

Property Value
--esp-image-static-ratio The ratio value.
--esp-image-static-compact-ratio The compact-ratio value, if any.
--esp-image-object-position The focal point as a percentage pair, as above.

Its limits:

  • Parity assumes a banner with a definite inline size laid out as a block (width: 100%, as a site generator usually emits it). The upgraded banner keeps its ratio on the element, so a banner whose width follows its height (an auto width with a block-size, min-block-size, or max-block-size), or one a grid or flex container stretches or shrinks, can lay out differently before upgrade. The sheet also makes the banner a size container, so a banner that sizes to its content (inline-block, fit-content, a flex item with an auto basis) has no width until upgrade.
  • The compact switch uses a container query at the default 40rem; a custom --esp-image-compact-width cannot apply before upgrade.
  • No scrim or texture paints until upgrade. Hold the page until Espalier loads (see Build a site) if unscrimmed copy over the photo would be unreadable.
  • Emit the copy in stack order (headline, supporting, action). A headline on its own takes the overlay padding as its own padding before upgrade, so a background on that headline would show the padding until upgrade.
  • Styling through ::part(overlay) does not reach light-DOM markup; mirror such a rule onto the static copy. For the compact band, a custom --esp-image-overlay-padding must be a single length.
  • The sheet grows a banner whenever its copy and spacing outgrow the ratio, while the upgraded banner grows only when its text would be clipped. Copy that fits but whose padding or margins do not makes the banner taller before upgrade than after.
  • The sheet sizes the image box by the ratio, while the upgraded banner applies its ratio to its own border box, so a border or padding set directly on the banner element makes it taller before upgrade by that amount. Banners have no border by default.
  • With an explicit host block size and compact-placement="below", the image keeps its compact ratio before upgrade and the band's surface takes the extra height; after upgrade the image takes it. Content-sized banners match exactly.
  • Banners built from esp-image-option children or local-image have no image until upgrade; the sheet is for projected img/picture banners.

Use <esp-focus-picker> to author that value visually. Its esp-focus-picker-changed event detail is the same {x, y} object assigned to esp-image.focusPoint. Per-breakpoint art direction remains a <picture> concern; use alternate <source media> images when one focal point cannot describe the intended composition.

<esp-focus-picker
  src="/assets/focus-picker-unsplash.jpg"
  alt="A woman standing in a sunlit field with her back to the camera"
  focus="0.65 0.55"
  ratio="3/1"
  compact-ratio="3/2"
></esp-focus-picker>

The demo photograph is by Sasha Freemind on Unsplash.

Shipped image textures

The optional package stylesheet maps three generated, first-party 256×256 tiles onto the same consumer-image tokens. The tiles are inlined as data URIs, so importing the stylesheet is the only step — the classes work without serving or configuring any asset paths:

<link rel="stylesheet" href="/css/textures/banner-textures.css">
<esp-image banner class="esp-texture-paper" ratio="3/1">
  <img src="/assets/canvas-bg.jpg" alt="A colorful floral pattern">
</esp-image>

Use .esp-texture-paper, .esp-texture-grain, or .esp-texture-grunge. Applications can instead set --esp-image-texture-image and its repeat, position, size, opacity, and blend-mode tokens directly. Both paths render through the shared texture image layer. The committed assets regenerate with node scripts/generate-banner-textures.js and verify with --check. Published-package consumers import the same stylesheet through the stable @taprootio/espalier/textures/banner-textures.css subpath.

Registered textures

Hand-wiring the consumer tokens on every banner is verbose and not reusable. The application's texture vocabulary is owned by esp-root: EspalierRoot.registerTexture(name, definition) turns an image-backed look into a named preset any banner under the root selects with texture="<name>":

import { EspalierRoot } from "@taprootio/espalier";

EspalierRoot.registerTexture("pressed-paper", {
  image: "repeating-linear-gradient(-45deg, oklch(1 0 0 / 0.3) 0 3px, transparent 3px 9px)",
  blendMode: "soft-light",
  opacity: 0.9,
});

image accepts any CSS background-image value — a gradient as above, a data URI, or a url() the application serves itself. Espalier's own defaults never depend on a served asset.

The plain registerImageTexture / registeredImageTextures module functions remain available from the package root, which also registers the full component catalog, and from a dedicated side-effect-free subpath for code that must not register components:

import {
  BUILT_IN_IMAGE_TEXTURES,
  isBuiltInImageTexture,
  registerImageTexture,
  registeredImageTextures,
} from "@taprootio/espalier/shared/image-texture-registry";

Importing that subpath defines no custom element and touches no DOM, so a generator, a server-rendered shell, or a build script can enumerate and validate the texture vocabulary in Node without a DOM shim. It is the same registry the components read — the root entry re-exports BUILT_IN_IMAGE_TEXTURES, registerImageTexture, and registeredImageTextures from this same module — so nothing can drift between the two.

Definitions carry image (required) plus optional size, repeat, position, opacity, and blendMode, mapping one-to-one onto the consumer tokens — and explicit --esp-image-texture-* tokens still override a registered preset per banner. Names are lowercase kebab-case and may not shadow a built-in preset; re-registering a name replaces it. Live banners re-render on registration, so a banner may reference a name before its registration script runs. registeredImageTextures() returns a snapshot of the registry for building editor preset pickers, and script-only contexts (such as doc demos) can use the registerTexture / registeredTextures statics on the esp-root class instead of module imports.

Resilience

In forced colors, decorative layers are removed and overlay content receives a solid Canvas background with CanvasText. Reduced transparency strengthens the scrim and removes texture. Print removes texture and gives overlay content a solid paper-safe background. All presets are static; under reduced motion the banner also stops any animation or transition applied to its media (see Image motion), and copy never moves either way.

Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O