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 polarity
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 exactly20%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"overbottom-startcontent, orscrim="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(defaultvar(--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-sizeand--esp-type-lead-font-weight, body font, line height1.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,36remby 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 isnone). - One accessible action. The action stays the consumer's native link or
button, drawn in the overlay ink: a
2pxborder, a2.75remminimum target, and a two-tone:focus-visiblering (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.


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 ablock-size,min-block-size, ormax-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-widthcannot 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-paddingmust 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-optionchildren orlocal-imagehave no image until upgrade; the sheet is for projectedimg/picturebanners.
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.