jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · Effects

effects — attachments, not components

The effect family's one law: an effect is an ATTACHMENT — a factory you mount on an element, never a component you render. The leaf form ({@attach pressEffect(shimmer())}) puts a loop on your own button; the component-tag form (<PressButton {@attach pressEffect(…) }>) reaches a host's root through its rest spread — data-jx-attach names the mounting point, the optional stamp. Every effect is element-level paint: it never owns semantics, never moves the body, and degrades honestly (frost before lens, frozen loops under reduced motion). Scroll-driven motion is a different domain — scroll-run owns it, linked below. Two residents share this home: glass (frost, the liquid lens, the SwiftUI semantic layer) and the press loops (shimmer, pulse, rainbow, ripple).

leaf form · your own element component tag · the host's root fromAction · the action bridge params flow, never mutate data-jx-attach mounts · data-jx-effect paints

install

npx jixoai-ui add effects

install — effects

$npx jixoai-ui add effects

install

npx jixoai-ui add effects/glass — or point shadcn add at the item URL: https://ui.jixoai.com/r/glass.json

install — effects/glass

$npx jixoai-ui add effects/glass

https://ui.jixoai.com/r/glass.json

install

npx jixoai-ui add effects/press-button — or point shadcn add at the item URL: https://ui.jixoai.com/r/press-button.json

install — effects/press-button

$npx jixoai-ui add effects/press-button

https://ui.jixoai.com/r/press-button.json

usage

Usage

Two mount forms and one bridge. Build the effect object with a typed builder, feed it to a factory, mount the factory — the whole {@attach} expression is the attachment, on a leaf element or a host's component tag alike. Never write into a mounted fx; never hand an action-shaped helper to {@attach} raw.

Effects usage
<script lang="ts">
  import PressButton from '@ui/press-button.svelte';
  import Chip from '@ui/chip.svelte';
  import { pressEffect } from '@ui/press-button';
  import { liquid, liquidGlass } from '@ui/glass';
  import { fromAction } from 'svelte/attachments';
  import '@ui/glass/glass.css';
</script>

<!-- the LEAF form — your own element, the factory inline: the whole
     expression is the attachment, teardown on unmount -->
<button {@attach pressEffect(shimmer({ speed: 4000 }))}>deploy</button>
<div style="border-radius: 24px" {@attach liquidGlass(liquid({ bezel: 22 }))}>…</div>

<!-- the COMPONENT-TAG form — the same {@attach} on a host component:
     the attachment compiles to a symbol-keyed prop, the host's rest
     spread lands it on its stamped root (data-jx-attach names the
     mounting point; icon-button chains the same spread one level
     deeper; an undefined value skips — never a crash) -->
<PressButton {@attach pressEffect(ripple({ duration: 800 }))}>deploy</PressButton>
<Chip {@attach pressEffect(shimmer())}>filter</Chip>

<!-- the fromAction bridge — action-shaped helpers (third-party or the
     repo's own internals): update on reference change, destroy on out -->
<div {@attach fromAction(riseIn, () => ({ y: 12 }))}>…</div>

The param-flow law. Params FLOW — derive the fx object ($derived) and let a change replace the factory call; never mutate a mounted fx in place. The channel has no update call: a replaced fx is a fresh closure, an identity remount (old teardown, fresh mount — the glass maps regenerate in single-digit ms). Deep mutation is a two-sided trap: the channel itself never deep-reads the param, but a $state-held fx registers fine-grained deps through the attachment body's own property reads — so mutation re-runs the attachment while the old closure stays captured. Replace, always.

The two-channel law. data-jx-attach is the JS channel — WHERE effects mount: the host stamps it on the mounting-point element ('root' on press-button/chip/icon-button, 'indicator' on tabs). Its value is the point's name, nothing more — an optional, queryable stamp, never the forwarding mechanism (the component tag's rest spread is). data-jx-effect is the CSS channel — WHAT the law sheet paints (the glass frost/lens stamps). The two are orthogonal by design: the tabs liquid indicator carries both, and neither implies the other.

glass

The lens over a visual band — gradient text and a 24px grid give refraction something to refract (the approved prototype's backdrop recipe). The dock covers BOTH layers: the physical sliders feed liquid() directly, the variant row compiles liquid.apple(); the source lane emits the exact builder snippet. Drag the sliders — the searchbox, card and wide pill all ride ONE effect object through the identity remount.

Search ⌘K

liquid() — kube.io's generator, ported

surface convex-squircle · bezel 22px · thickness 30px · scale 55px · blur 0.2px · rimSaturate 4× · specular 0.2

wide pill · uniform bezel

blur() — the zero-JS frost member

css-only: the stamp + vars, no mount — this is also liquid's fallback paint

regular
clear
tint
identity (= frost)

engine probe…

glass-usage.svelte svelte
<script lang="ts">
  import { liquid, liquidGlass } from '@ui/glass';
  import '@ui/glass/glass.css';
</script>

<!-- the physical layer — live values from the playground dock -->
const fx = liquid({
  surface: 'convex-squircle',
  bezel: 22,
  thickness: 30,
  scale: 55,
  blur: 0.2,
  specular: 0.2,
  rimSaturate: 4,
});
<div style="border-radius: 28px" {@attach liquidGlass(fx)}>…</div>

<!-- the semantic layer — the dock's variant, compiled down -->
const fx = liquid.apple({ variant: 'regular', shape: 'capsule' });
<button style="border-radius: 999px" {@attach liquidGlass(fx)}>…</button>

the model

Two layers, one stamp channel

The physical layer is the objective fact; the semantic layer is the iOS standard compiled down and exposes no physics. Both land on the same data-jx-effect stamp with the same tuning vars — the law sheet owns the paint, the mount owns the lens.

liquid({...}) carries kube.io's generator: the surface profile d(s) = T.x/T.y · (H(s)·thickness + bezel) — a 1-D ray trace with the glass-slab path term — normalized by max displacement and swept along the border as a rounded-rect SDF, rastered at 2× dpr under a hard 512² area cap. The mount canvas-encodes lens + specular maps, appends the 9-primitive SVG chain (in-chain frost → displacement → rim saturate → the saturated-copy / faded-ring specular pair) and only THEN writes the --jx-glass-filter pointer: a set pointer never precedes its fragment. liquid.apple({...}) compiles SwiftUI's parameter standard onto that — see the compile table in the API section.

the glass builders
liquid({ surface, bezel, thickness, scale, blur, specular, rimSaturate,
        radius, saturate, fill, brightness })   // the physical layer
liquid.apple({ variant, tint, interactive, shape, isEnabled })  // the semantic
blur({ radius, saturate, fill, brightness })   // the frost member, zero JS

environments

Degradation is the design, not the fallback

The frost base paints unconditionally — for liquid it IS the no-JS, pre-hydration and unsupported-engine paint. The lens is an enhancement, never a dependency: frost → lens on mount, never unfiltered → lens.

environmentpaint
Chromium, JS onthe lens, from the first painted frame after mount (the pointer-only @supports branch)
Chromium, JS off / pre-hydrationfrost — the unconditional base paint holds (attachments are SSR-inert; disable JS on this page: the band stays frosted)
Safari / Firefoxfrost — the @supports branch never applies; same geometry, honest paint
no 2D canvas / zero-size boxthe mount stamps vars and returns — frost stands, no crash; a ResizeObserver rebuilds when the element appears
prefers-reduced-transparencysolid fill (--jx-glass-solid-fill over the background token), no filters
prefers-reduced-motionevery press loop and the band scenery freeze; the interactive press choreography drops (motion is an enhancement too)
printfilters off, the fill stays readable on paper
forced-colorsno law block by design — consumers carry their own Canvas grounds

The lens's host svg is a zero-size, aria-hidden fragment appended to the document body — pure scenery, never in the accessibility tree. The element itself keeps whatever role it had: the effect never owns semantics.

recipe

Element chrome is consumer-owned

kube's own component css, not law: consumers ship the tint fill, the shadow and the border-radius. The mount measures the element's border-radius (or the shape override) and builds the lens to fit — the chrome never fights the effect.

The tint layer rides an inset child (so the law sheet's fill paint stays untouched), the shadow and the radius ride the host. The semantic layer's tint option compiles to the same shape: a 30% color-mix fill written into the stamp vars — no extra element needed.

the chrome recipe
<!-- the kube recipe: the EFFECT is the item's; the CHROME is yours.
     The mount owns refraction; tint fill, shadow and border-radius
     stay consumer-owned styling. -->
<div style="border-radius: 26px" {@attach liquidGlass(fx)}>
  <div
    aria-hidden="true"
    style="position: absolute; inset: 0; border-radius: inherit;
           background: rgba(255, 255, 255, 0.05);
           box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16)"
  ></div>
  <div style="position: relative">…content…</div>
</div>

shimmer

press family · shimmer — the shine arc walks the BORDER BAND, painted ON THE HOST ITSELF (no child layer, no inset — the Owner's r11 ruling): the host's own border is the ring's geometry (width = ringW, color forced transparent, image forced hidden), and the double background — a fill layer clipped to the padding box over a rotating conic — clips through --shimmer-clip: border-area where Chrome 139+ answers (the TRUE cutout — a transparent face shows the band behind it), border-box elsewhere. The fill channel: auto (omitted) resolves the theme scope's --background token — the same basis as text/border, never a measured ancestor; a 0xRRGGBB number (solidFill() mints these from any CSS color against that same scope token — try it over the animated band), or null for transparent (the blend emulation — white + darken in light contexts, black + lighten in dark — where border-area is missing). The hosts are NEUTRAL plain elements; drag the dock and every host rides ONE fx object through the identity remount.

shimmer-usage.svelte svelte
<script lang="ts">
  import { pressEffect, solidFill } from '@ui/press-button';
  import { shimmer } from '@ui/press-button/press-button.svelte';
</script>

<!-- the host itself carries the ring — its border IS the band. shine defaults
     to the primary token, ring-color to currentColor, and fill: an opaque
     number from solidFill(cssColor) against the theme scope's --background
     token (the auto basis — the same one omitted fill rides), or
     null for transparent (the border-area cutout where the engine supports
     it, the blend emulation elsewhere) -->
<button {@attach pressEffect(shimmer({ shine: 'var(--primary)', ringColor: 'currentColor', shineWidth: '30deg', speed: 3000, ringW: 1, fill: solidFill('rgba(255, 255, 255, 0.35)') }))}>
  deploy
</button>

pulse

press family · pulse — sonar rings breathe outward from the body's own silhouette: a negative-z copy of the body casts expanding box-shadows, and the variant picks the breathing curve — slow expands and fades, ring holds then dissolves at its extent and re-condenses, ripple eases out on a settling curve. Neutral plain hosts, one fx object, identity remounts.

pulse-usage.svelte svelte
<script lang="ts">
  import { pressEffect } from '@ui/press-button';
  import { pulse } from '@ui/press-button/press-button.svelte';
</script>

<!-- the leaf form — a plain element, the factory inline; the dock's live params -->
<button {@attach pressEffect(pulse({ color: "var(--primary)", duration: 2500, distance: '0.7em', variant: 'slow' }))}>
  deploy
</button>

rainbow

press family · rainbow — shimmer's SIBLING on the same host-channel technique (r13): the host's own border IS the flowing ring (width = ringW, forced transparent + image hidden), the wrap-stop train rides background layer 2 through the same border-area gate and fill channel (auto = the theme scope's --background token — the same basis as text/border / null = the true cutout / a solidFill() number), and the registered shift's one animation on the host drives the unchanged blurred under-glow below through inheritance. Neutral plain hosts, one fx object, identity remounts.

rainbow-usage.svelte svelte
<script lang="ts">
  import { pressEffect } from '@ui/press-button';
  import { rainbow } from '@ui/press-button/press-button.svelte';
</script>

<!-- the leaf form — the host's own border IS the flowing ring (ring-w);
     fill: solidFill(cssColor) → opaque number, null → the cutout, omitted → the theme scope's --background token -->
<button {@attach pressEffect(rainbow({ speed: 2000, colors: ["hsl(0 100% 63%)", "hsl(270 100% 63%)", "hsl(210 100% 63%)", "hsl(195 100% 63%)", "hsl(90 100% 63%)"], ringW: 1, fill: undefined }))}>
  upgrade
</button>

ripple

press family · ripple — ink expands from the exact press point: the factory listens for its own pointerdown (Enter / Space spawn centered ink, no-op while disabled or aria-disabled), a css-animated svg dot scales to the body's long side, its soft edge an optional feGaussianBlur (the soft param — 0 disables the filter), and the seat inherits the host's own border-radius with overflow hidden so the ink stays inside the rim — and the node leaves the DOM on animationend. round pins against the site-wide bevel law; bevel cuts the corners into a diamond. PRESS THE HOSTS — this is the family's one gesture-driven loop.

ripple-usage.svelte svelte
<script lang="ts">
  import { pressEffect } from '@ui/press-button';
  import { ripple } from '@ui/press-button/press-button.svelte';
</script>

<!-- the leaf form — a plain element, the factory inline; the dock's live params -->
<button {@attach pressEffect(ripple({ color: "currentColor", duration: 600, shape: 'round', soft: 1.5 }))}>
  deploy — press me
</button>

theming

Tokens and the stamp channel

Selectors read the data-jx-effect stamp, the --jx-glass-* vars carry the tuning — consumers override VALUES, never names. No density footprint: the effect is dimensionless paint.

TokenDefaultSource
--jx-glass-radius'14px' (blur) · '2px' (liquid)component
--jx-glass-saturate1.35 (blur) · 1.6 (liquid)component
--jx-glass-fillcolor-mix(in oklab, var(--background, Canvas) 68%, transparent)component
--jx-glass-brightness1component
--jx-glass-filtermount-writtencomponent
--jx-glass-interactiveflagcomponent
--jx-glass-solid-fillvar(--background, Canvas)component

api

API

Builders build typed fx objects (pure — they never touch the document); factories turn an fx into an attachment; hosts expose named hooks. Glass numeric args clamp into range, NaN / ±Infinity / non-finite throws TypeError at construction, surface validates against the enum.

liquid(o) — the physical layer (LiquidGlassOptions)

PropertyTypeDefaultDescription
surface 'convex-circle' | 'convex-squircle' | 'concave' | 'lip''convex-squircle'The dome cross-section (Apple’s preferred profile is the default). lip is a smoothstep blend of squircle + concave; concave flips the rim pull by physics — a diverging lens samples outward at the border.
bezel number22Refraction band width in element px (border → inward). Clamp [4, 80].
thickness number30The ray-traced glass-slab thickness — the path term H(s)·thickness in the profile. Clamp [2, 160].
scale number55feDisplacementMap scale in element px — the displacement magnitude. ABSOLUTE px: kube pins it per element (56px searchbox → 55, 92px thumb → 22, 150px circle → 134); there is no universal fraction, tune per surface. Clamp [0, 160].
blur number0.2The in-chain frost (feGaussianBlur stdDeviation, the first primitive). Clamp [0, 4].
specular number0.2The faded-ring opacity (feFuncA slope). Clamp [0, 1].
rimSaturate number4The rim’s saturation boost (feColorMatrix saturate on the ring-clipped copy). Clamp [1, 12].
radius string'2px'The frost fallback’s blur (no-JS / engines without url() backdrop-filters) — tabs liquid’s retired values.
saturate number1.6The frost fallback’s saturation. Clamp [0, 3].
fill stringcolor-mix(in oklab, var(--background, Canvas) 68%, transparent)The frost fallback’s translucent fill.
brightness number1The frost fallback’s brightness. Clamp [0, 3].

liquid.apple(o) — the semantic layer (AppleLiquidOptions)

PropertyTypeDefaultDescription
variant 'regular' | 'clear' | 'identity''regular'Glass.regular / .clear / .identity. Compiles per the table below — regular is the physical defaults verbatim; identity (or isEnabled false) returns the FROST member, Apple’s no-op mapped to the law’s own degradation at zero lens cost.
tint string—Glass.tint(_:) — a color; compiles to fill = color-mix(in oklab, <tint> 30%, transparent).
interactive booleanfalseGlass.interactive() — stamps --jx-glass-interactive:1; the law sheet’s motion css carries the press scale/brighten (off under prefers-reduced-motion). Composes with the consumer’s own press choreography.
shape 'capsule' | numberthe element’s border-radiusA Shape override for the mount’s radius: capsule = min(w,h)/2 (the full pill), number = px clamped into [0, min(w,h)/2]. Unset reads the element’s computed border-radius.
isEnabled booleantruefalse → identity behavior (the frost member).

blur(o) — the frost member (BlurOptions)

PropertyTypeDefaultDescription
radius string'14px'The frost blur radius — the retired .jx-glass value.
saturate number1.35The frost saturation. Clamp [0, 3].
fill stringcolor-mix(in oklab, var(--background, Canvas) 68%, transparent)The translucent frost fill.
brightness number1The frost brightness. Clamp [0, 3].

the press builders — shimmer / pulse / rainbow / ripple

PropertyTypeDefaultDescription
shimmer(o) ShimmerEffectsee belowThe shine arc walks the host’s OWN border — the effect paints the HOST ITSELF (no child layer, no inset): border-width = ringW with color forced transparent and image forced hidden, the double background (a fill layer clipped to the padding box over a rotating conic) clipped through --shimmer-clip — border-area where Chrome 139+ answers (the TRUE cutout), border-box elsewhere. Options: shine? (any CSS color, default var(--primary)), ringColor? (the ring’s REST color — the band the arc walks on, default currentColor), fill? (the FACE: an opaque 0xRRGGBB number — solidFill(cssColor) mints these by compositing over the theme scope’s --background token — or null for transparent; default = the theme scope’s --background token — the same basis as text/border, never a measured ancestor, following the scope live), shineWidth? (the arc’s angular width, default 30deg), speed? (one full revolution, in ms, default 3000), ringW? (number = px, or any CSS length string, default 4). A null fill where border-area is missing rides the blend emulation: light context → white + mix-blend-mode darken, dark → black + lighten. Inheritable: --shimmer-shine-start (the arc’s head angle, default 280deg).
pulse(o) PulseEffectsee belowSonar rings breathe outward from the body’s silhouette. Options: color? (default var(--primary)), duration? (default 2500ms), distance? (default 0.7em), variant? — slow | ring | ripple (default slow).
rainbow(o) RainbowEffectsee belowShimmer’s sibling on the SAME host-channel technique (r13): the host’s own border IS the flowing ring (border-width = ringW, color forced transparent, image hidden); the wrap-stop train (a registered --jx-rainbow-shift pans the stops through one seamless 200% cycle — top, bottom, both sides alike) rides background layer 2 through the same border-area gate and fill channel as shimmer (fill: an opaque 0xRRGGBB number from solidFill(), null for the true cutout / blend emulation, default = the theme scope’s --background token — the same basis as text/border); the blurred under-glow below is unchanged and inherits the shift. Options: speed? (the flow pace in ms, default 2000), colors? (a non-empty array, default five hsl primes), ringW? (number = px, or any CSS length string, default 4), fill? (number | null).
ripple(o) RippleEffectsee belowInk expands from the exact press point, centered on keyboard activation — a css-animated svg dot with an optional feGaussianBlur soft edge (soft, default 0 — 0 disables the filter), riding a seat that inherits the host’s border-radius with overflow hidden, removed on animationend. Options: color? (default currentColor), duration? (default 600ms), soft? (default 0), shape? — round | bevel (bevel cuts the corners into a diamond).

the semantic compile table — Apple parameter → physical compilation

variant: 'regular'the physical defaults verbatim (liquid() ≡ liquid.apple() — deep-equal, pinned by tests)
variant: 'clear'{ blur: 0, fill: background 22% mix, rimSaturate: 2.5, specular: 0.12 } — less frosting, more transparency, a quieter rim
variant: 'identity' / isEnabled: falsethe frost member — blur({ radius: 2px, saturate: 1.6 }); Apple’s no-op at zero lens cost
tintfill = color-mix(in oklab, <tint> 30%, transparent)
interactivethe effect flag → --jx-glass-interactive:1 → the law sheet’s press motion
shapethe mount’s radius override ('capsule' = min(w,h)/2; number = px, clamped); unset = the element’s border-radius

the attachment factories + the bridge

PropertyTypeDefaultDescription
liquidGlass(fx) Attachment<HTMLElement>—The glass factory: {@attach liquidGlass(fx)} is the whole mount — stamps vars, measures, canvas-encodes the maps, appends the chain, writes the pointer LAST. A replaced fx re-runs the factory (identity remount — teardown + rebuild, maps regenerate in single-digit ms; frost stands during the swap). ResizeObserver rAF-coalesced regeneration; the no-canvas and zero-box guards leave frost standing.
pressEffect(fx) Attachment<HTMLElement>—The press factory — SELF-LISTENING (Owner ruling): pointerdown/pointerup + keydown Enter/Space, no-op while the element matches :disabled / [aria-disabled="true"] (re-checked per event), reduced-motion gated inside; teardown cancels WAAPI + listeners and strips the stamped layers/vars/classes. Documented price: the ripple ink fires on POINTERDOWN and cannot see a host’s loading lock — mounting consumers that need the lock wrap the factory with their own gating.
fromAction(action, fn?) from 'svelte/attachments'—The bridge for ACTION-shaped helpers (update/destroy objects — third-party or the repo’s own internals): calls action(element, fn()) once, update(arg) when the getter’s value changes (reference-level), destroy on teardown. Never hand an action to {@attach} raw — the object return is silently dropped (the leak counter-example in the spec battery).
attachLiquidGlass(el, fx) (HTMLElement, LiquidGlassEffect) => { update, destroy }—The framework-agnostic glass kernel — what the factory wraps (the ripple precedent: effects may own runtime JS).

the mounting points — data-jx-attach, the optional named stamps

PropertyTypeDefaultDescription
press-button 'root'—The root element carries data-jx-attach='root'; attach through the component tag — <PressButton {@attach pressEffect(shimmer())}> — the rest spread lands it on the button/anchor, inside the press law.
chip 'root'—The root element carries data-jx-attach='root'; attach through the component tag — the compact rung of the press family takes the same attachment shape.
icon-button 'root'—The chain: this component's rest spread forwards into the wrapped PressButton's own spread — one attachment surface, two hops, landing at the same root button.
tabs 'indicator'—The shared indicator element carries data-jx-attach='indicator' as its OPTIONAL named stamp; the mount itself is the component's OWN material business (indicator='liquid' derives it — no consumer reach onto the engine-owned element). The element can carry BOTH channels: the stamp (the JS naming) and data-jx-effect (the paint stamp).

the stamp helpers

PropertyTypeDefaultDescription
glassAttrs(fx) Record<string, string>spreadStamps data-jx-effect + the vars style. For liquid it carries the tuning vars ONLY — the --jx-glass-filter pointer belongs to the mount exclusively, never to markup.
glassVars(fx) string—The vars string alone (the style attribute’s glass payload, semicolon-joined).

PressButton — the component-tag form's first host (public contract)

PropertyTypeDefaultDescription
variant 'fill' | 'tonal' | 'outline' | 'ghost' | 'link''outline' · ambient zoneSelects the ladder rung; link is the interaction exception. Omitted → the ambient paint zone (ButtonGroup / variant scope), else the frozen own. Semantic hue injects through --jx-fill/--jx-fill-ink, --jx-tonal, --jx-outline classes at the call site.
{@attach …} (component tag) Attachment<HTMLElement>—The effect mount (r4): <PressButton {@attach pressEffect(shimmer())}> — the tag compiles to a symbol-keyed prop, the rest spread lands it on the stamped root. An undefined value skips (never a crash); a replaced fx is an identity remount.
…rest HTMLAttributes<HTMLElement>—The rest lane: arbitrary attributes land verbatim on the root (button or anchor) — the same lane the component-tag attachment rides.
href string—Renders an anchor and navigates to the target; hrefs not starting with / open a new tab with noreferrer automatically.
loading booleanfalseThe async pose: aria-disabled=true (focusable), pointer AND keyboard activation suppressed, href navigation blocked, spinner glyph in the leading lane. Press law holds unchanged. Pair with the one-shot flash() helper (bind:this) on settle.
raised boolean'true' · ambient zoneThe physics axis, orthogonal to paint: false is the FLAT texture — no rest/hover shadow, the body never moves, an engrave-tier inset alone creates the press. Inert on link.
onclick () => void—Runs for button activation.
children * Snippet—Button label and optional inline icon content — the label is a snippet, so icons compose inline with the component’s own gap.

migration

Migrating to attachments

The effect-attachments migration (2026-09-09; r4 2026-09-10) retired the action-directive mounts, the builder-prop form and the attachments record: every effect now mounts through a factory — the leaf form on your own elements, the component-tag form through hosts.

use:pressEffect(fx)→ {@attach pressEffect(fx)} — the same factory, the attachment channel. The element-level action directive is retired repo-wide; the canary in the spec battery holds it at zero.
<PressButton effect={…}>→ <PressButton {@attach pressEffect(shimmer())}> — the builder-prop retired with the host’s effect branches, and the r4 review retired the record that briefly replaced it: the component tag is the one uniform syntax, the host’s rest spread lands it at the stamped root. Chip’s default ripple died with its prop — attach through the tag.
<PressButton attachments={{ root: … }}>→ <PressButton {@attach pressEffect(…)}> — the attachments record (r2’s interim shape) retired with r4: the same mount through the tag, one less prop, the same stamped root.
use:liquidGlass(fx)→ {@attach liquidGlass(fx)} — liquidGlass IS the factory now; the kernel (attachLiquidGlass) stays exported for framework-agnostic use.
use:someAction(el, p)→ {@attach fromAction(someAction, () => p)} — action-shaped helpers (update/destroy objects) MUST ride the bridge: a bare attach silently drops the object return and leaks (the spec battery’s counter-example). This is also how the repo’s own internals mounted.
toast’s ToastEffectNOT this family — a material stamp (the toast surface’s own paint), untouched by the migration; its battery stays green, canary-guarded.

The glass channel’s earlier migration rides the same page: the retired hand-tuned .jx-glass class is a stamp-channel member now — blur() defaults ARE the retired values verbatim (14px · 1.35 · background 68%), so an unconfigured migration is computed-equivalent by construction; a migration canary source-scans for stray jx-glass references so the old class cannot quietly return. Tuned surfaces name their values through the same builder — toast 12px / saturate 1 / popover 55%, tabs glass 10px / 1.5 / 40%, tabs liquid the lens itself at frost 2px / 1.6.

the frost migration
<!-- the frost channel, BEFORE the class retired:
     <div class="jx-glass …">…</div> — NOW the stamp channel -->
<div {...glassAttrs(blur())} class="frosted-panel">…</div>

<!-- a tuned surface names its values through the same builder -->
<div {...glassAttrs(blur({ radius: '12px', saturate: 1 }))} class="toast-ground">…</div>
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h