Skip to main content
xriptxr

Docs

Effects

A token is a value. A component is a thing. An effect is a verb — a behavior applied to an element under a condition — and neither of the other two can hold one.

The shape

A token cannot say "on hover"; a component cannot decorate an element that already exists without wrapping it. So an effect is addressed the way an icon name and a theme recipe already are: a name that is its own spec.

<span   data-fx="throb">                      <!-- no condition: ambient -->
<a      data-fx="glow@hover">
<button data-fx="glare@hover lift@active">
<div    data-fx="tint@checked frost@disabled">

<!-- re-point one effect's color inline, like the toolbar above does -->
<a data-fx="glow@hover" style="--fx-color: var(--pink)">

<!-- named parameters, any subset, any order -->
<span data-fx="throb?rate:3s,colors:[accent,accent-2]">
<div  data-fx="glow@hover?spread:18">
It emits plain attribute-selector CSS, so nothing here needs a runtime — the whole cross product of the shipped set is about 3 KB gzipped.

The library

Each effect gets the stage that actually shows it, because one shared stage cannot show them all. A halo is light spilling onto its surroundings, so glow and throb need a dark surface and no clipping. A filter acts on the element's own content, so saturate needs color inside the target; a backdrop-filter acts on what is behind, so frost needs the opposite. The spec under each tile is the one it is running, parameters and all.

glow

glow@hover?spread:18

a halo, the accent bloom

throb

throb?rate:5s,colors:[accent,accent-2],spread:20

a slow pulse that drifts between the two accent hues

glare

glare@hover?angle:105,rate:900ms

an animated sheen that sweeps across the box

lift

lift@hover?distance:7

a small rise with a shadow under it

tint

tint@hover?amount:55,color:accent

a color wash over the surface

frost

frost@hover?blur:7,saturation:90

a backdrop blur and saturation bump

reveal

reveal?distance:14,rate:400ms

fades and slides in once armed

shake

shake@hover?distance:6

a short nudge, for an error

flash

flash@fired?amount:80

a hard blink of color: impact, rejection, that one

float

float@fired?distance:34,rate:900ms

a rise and fade out; a readout that appears, travels, and dies

pop

pop@fired?scale:0.3

a scale in past the resting size and back; something arriving

wobble

wobble@fired?angle:9

a rotational jitter, where shake is a translational one

saturate@hover?amount:200 · switch → saturate@armed?amount:0

hover boosts to 200; the switch overrides to grayscale, hover and all

reveal is the one that cannot be shown by hovering — it waits to be armed, which is precisely what an observer does. Its tile has the button.

Intensity is the algorithm's policy, not a per-component guess. Switch the theme in the toolbar and the whole layer follows; on xtyle-hc it flattens to nothing, because a halo, a lift, and a wash all spend the edge contrast a high-contrast taste exists to protect.

Parameters

An effect's tunables are named, after a ?: throb?rate:3s,colors:[accent,accent-2], glare@hover?angle:315,rate:100ms. A bare number takes the parameter's own unit (so 315 is degrees and 3 is seconds), a hex is a fixed color, and a bare name resolves to that theme token — which keeps it theme-reactive rather than freezing whatever color it happened to be when you typed it.

hover

glow@hover?spread:20,color:pink

hover

throb?rate:1.2s,colors:[green,blue]

hover

glare@hover?angle:315,rate:700ms,color:#ffffff

hover

lift@hover?distance:10,color:purple

hover

tint@hover?amount:60,color:red

EffectParameters
glow spread (px), color
throb rate (s), color, color-alt, colors, spread (px)
glare angle (deg), rate (ms), color
lift distance (px), color
tint amount (%), color
frost blur (px), saturation (%)
reveal distance (px), rate (ms)
shake distance (px), rate (ms)
flash color, amount (%), rate (ms)
float distance (px), rate (ms)
pop scale, rate (ms)
wobble angle (deg), rate (ms)
saturate amount (%)

Conditions

ConditionSelector
nonealways — ambient
hover:hover
focus:focus-visible
active:active
checked:checked
disabled:disabled,[aria-disabled="true"]
open[open],[aria-expanded="true"]
invalid:invalid,[aria-invalid="true"]
armed[data-fx-armed]
selected[aria-selected="true"],[data-selected]
current[aria-current],[data-current]
busy[aria-busy="true"],[data-busy]
fired[data-fx-fired]

The optional runtime

The layer is a stylesheet and needs nothing running. But a parameter cannot live in a selector, so writing the attribute by hand gets you the effect and the defaults; the ?… tail is understood and then dropped. applyEffect is the pairing done correctly, and it clears only the properties it wrote, so swapping specs can't leave a stale parameter retuning the next one.

// the effect fires, and its parameters are silently dropped
el.dataset.fx = "throb?rate:1.4s";

// both halves, together
applyEffect(el, "throb?rate:1.4s");

A transient (flash, float, pop, wobble, shake) is a verb about an event rather than a state, so it needs a trigger. fireEffect sets the fired condition and clears it once the animation lands; a call that arrives mid-flight restarts it rather than being swallowed.

import { applyEffect, fireEffect, armInView } from "@xtyle/core/fx";

// set the spec AND write the properties its parameters resolve to
applyEffect(el, "glow@hover?spread:20,color:danger");

// fire a transient: sets `data-fx-fired`, clears it when the animation lands
await fireEffect(toast, "pop@fired");

// arm every `reveal` under a root, disarming each as it scrolls in
const stop = armInView(document.body);
applyEffects(root) does the same in one pass for markup that already carries the attribute, like an SSR page or a template. And a spec naming an effect, condition, or parameter nothing has registered warns rather than sitting there inert; unknownSpecs(spec) is the same check as a value, and it says which half was missing.

Tokens

TokenDerived from
--fx-intensitythe algorithm's taste; 0 disables the whole layer
--fx-color--accent
--fx-color-alt--accent-2 — the hue a throb travels toward
--fx-duration--duration-base
--fx-ease--ease-standard

These are ordinary tokens on the open register, so a theme dials them with an override and needs no knob. Four shared tokens rather than a family per effect is deliberate: a per-effect family would grow with the library and lock a third-party effect out of deriving at all, whereas a shared dial means an addon's effect answers to the theme the day it lands.

Adding your own

The registries are last-wins on the name, and the built-ins register first — so an addon replaces one entry or adds a new one without restating the rest. Same contract fills use.

import { registerEffect, registerCondition } from "@xtyle/core";

// last-wins on the name: this replaces the shipped glow
registerEffect({ name: "glow", active: "filter:blur(1px)" });

// ...and this adds one the library never had
registerEffect({ name: "sparkle", active: "outline:1px dashed var(--fx-color)" });
registerCondition({ name: "dragging", selector: "[data-dragging]" });

// `sparkle@dragging` now works — and so does `glow@dragging`, because the
// cross product is regenerated, not hand-listed.

Reduced motion

Every effect that moves is wrapped in a prefers-reduced-motion: reduce guard that returns it to stillness. This is a large part of why the layer is worth having: the policy is decided once, correctly, instead of by every adopter who happens to remember.