Iridescent border glow for any element - a perimeter-mapped rainbow flash/lights engine with zero dependencies.
- 🌈 The full effect: flashing rainbow wedge, travelling beams, flowing rainbow, pulse, draw-on reveal, stay-lit hold - with an inner glow, a soft outer halo and a crisp 1px ring, all composable.
- 📦 Zero runtime dependencies. No Tailwind, no CSS framework, no asset files. Everything is generated CSS (conic gradients + SVG data-URI masks). Works next to shadcn/ui, plain HTML, anything.
- 🧩 Works on any element - a card, a button, a section, an input, a circle avatar. Corner radius is auto-detected.
- ⚛️ Framework-free core (
irisify) + optional React bindings (irisify/react). - ♿ Decoration only: overlays are
aria-hidden,pointer-events: none, andprefers-reduced-motionis respected by default. - ⚡ Engineered for 60fps: colour is mapped to border arc-length (not angle, so nothing whips around corners), crisp layers get a high-res gradient while blurred halos get a cheap low-res one, and a static glow costs zero per-frame work.
npm i irisify
# or
pnpm add irisifyimport { Irisify } from "irisify/react";
<Irisify>
<div className="card">Ask anything…</div>
</Irisify>That's the original flash - a rainbow wedge that blooms from the bottom and converges at the top. Pick another preset and a couple of knobs:
<Irisify preset="beam" speed={5} colors={["#22d3ee", "#818cf8", "#e879f9"]}>
<button className="btn">Generate ✨</button>
</Irisify>Or attach to your own element with no wrapper:
import { useIrisify } from "irisify/react";
const ref = useIrisify<HTMLDivElement>({ preset: "rainbow" });
return <div ref={ref} className="card">…</div>;import { irisify } from "irisify";
const instance = irisify(document.querySelector(".card"), { preset: "hold" });
instance.dismiss(); // hold preset: sweep the glow away
instance.update({ speed: 2 });
instance.destroy();The element gets two sibling overlays sandwiching it (halo behind, ring + inner glow on top) so an opaque background keeps your content perfectly readable - the same layer order as the original design.
| Option | Type / default | What it does |
|---|---|---|
preset |
"flash" (default) · "hold" · "beam" · "rainbow" · "pulse" · "glow" · "reveal" |
What the light does (see below) |
colors |
palette name or string[] - default "iris" |
The gradient - see Colors |
speed |
number (seconds) |
One cycle: one flash, one lap, one breath |
glow |
"inside" | "outside" | "both" (default "both") |
Where the bloom lives relative to the border |
intensity |
number 0–2 (default 1) |
Overall glow strength |
border |
boolean (default true) |
The crisp 1px rainbow ring |
placement |
"all" · Side[] · { start, sweep } (degrees) |
Which part of the border is lit |
direction |
"clockwise" | "counterclockwise" |
Travel direction |
gap |
number (seconds) |
Rest between cycles. Negative (flash/hold): the next flash launches while the previous one is still fading |
paused |
boolean |
Freeze on the current frame |
dismissed |
boolean |
hold preset: sweep the held glow away / play it back in |
radius |
number (px) |
Corner radius override (auto-detected from CSS otherwise) |
reducedMotion |
"static" (default) · "none" · "ignore" |
What prefers-reduced-motion users see |
enabled |
boolean (default true) |
Master switch - false removes the effect entirely (no DOM, no work). Great for "glow while loading" |
flash- the original. A rainbow wedge fades in, sweeps bottom → top along both sides, and dissolves as the arms converge.hold- the flash travels in, then stays lit with the colours flowing. Flipdismissedto sweep it away.beam- one comet band travelling around the border (border-beam style).rainbow- the whole border lit, colours flowing around it.pulse- the lit border breathing in and out.glow- a static lit border. No animation loop at all - zero per-frame cost.reveal- the border draws itself on as one continuous piece, flashes off, repeats.
The simple options compose with the preset: placement confines any preset to sides or an exact arc, colors/speed/intensity re-skin it, and so on. Each preset is plain data - import IRISIFY_PRESETS to see exactly what one sets and build on it.
Three levels of control, lowest effort first:
1. Pick a built-in palette by name:
<Irisify preset="rainbow" colors="sunset"> … </Irisify>iris (the signature rainbow - default) · aurora · ocean · sunset · candy · ember · neon · mono. They're exported as plain CSS strings (IRISIFY_PALETTES), so copy one and tweak it.
2. Pass your own CSS colours - hex, rgb, hsl or oklch, in the order they appear along the gradient:
<Irisify colors={["#22d3ee", "#818cf8", "#e879f9"]}> … </Irisify>Flowing modes sample the list cyclically - the last colour blends back into the first, so a loop never shows a seam. Alpha is honoured ("rgb(255 0 234 / 60%)" gives a softer stop, like the original's translucent pink tail).
3. Exact engine control - advanced.stops takes OKLCH stops directly ({ l, c, h, a }[], what the engine natively blends in), and advanced.stopAngles sets where each colour sits inside the flash wedge (the original's authored 16°/30°/43°/59°/72°/84° spacing). Use these when you need the flash to reproduce a brand gradient exactly.
<Irisify advanced={{
stops: [
{ l: 0.452, c: 0.249, h: 264.1, a: 1 }, // blue
{ l: 0.843, c: 0.195, h: 87.0, a: 1 }, // yellow
{ l: 0.7, c: 0.322, h: 316.0, a: 0.8 }, // magenta, 80% alpha
],
}}> … </Irisify>Everything the engine can do is reachable under advanced. It wins over the simple options.
<Irisify
advanced={{
motion: {
mode: "flash",
flashOriginAngle: 270, // start at the left edge centre (auto-switches engine)
flashDestAngle: 90, // converge on the right edge centre
flashTrail: 0.7, // longer comet trail
flashTrailFade: 0.5, // dim the trail behind the head
flashOverlap: 0.3, // arms land brighter/earlier on the destination
},
easing: "cubic-bezier(0.26, 0.94, 0.6, 1)",
fadeInAt: 10, // keyframe % at full opacity
visibleUntil: 30, // keyframe % when the fade starts
outerGlowBlur: 8,
}}
>
<Card />
</Irisify>| Key | Purpose |
|---|---|
motion |
The full MotionSpec (below) |
stops |
Rainbow stops in OKLCH ({ l, c, h, a }[]) - exact-colour control |
stopAngles |
The flash wedge's authored conic stop angles |
duration, easing, fadeInAt, visibleUntil, gapBetween |
Flash timing - identical semantics to the original keyframes |
innerLayers |
The inner glow stack: each pass = a mask shape + blur + opacity + optional size |
outerGlowMask, outerGlowBlur, outerGlowOpacity |
The outer halo |
borderEnabled |
The 1px ring |
proceduralSpecs |
Override the 6 built-in mask shapes (pill geometry, stroke align, crop, blur…) |
symmetric |
Mirror the CSS flash into two beams (the original look) |
glowReach |
"inner" | "outer" | "both" |
flashDismiss |
hold: dismiss/replay |
Two modes:
flash- the original wedge.flashOriginAngle/flashDestAngleput the launch and convergence anywhere on the border (0° = top-centre, clockwise);flashTrail/flashTrailFade/flashOverlapshape the comet;flashHoldkeeps it lit.lights- a fully orthogonal light engine. Choose where (sides: ["top","right"]oruseCustomArc+startAngle/sweep), what (fill: "chain" | "bands",count,width+widthUnit: "arc" | "border" | "px",feather), and how (move: "travel" | "static", pluspulseandrevealtoggles that compose,direction,repeat: "loop" | "once").
All combinations are reachable - 3 bands pulsing on the top edge, a single comet on a custom 120° arc, a draw-on reveal of the left side, etc.
const i = irisify(el, options);
i.update({ speed: 2 }); // merge options in
i.setOptions(options); // replace all options (controlled integrations)
i.pause(); i.play();
i.dismiss(); i.show(); // hold preset
i.refresh(); // re-measure after a manual move
i.destroy(); // remove everything- Overlays are
aria-hidden="true"andpointer-events: none- screen readers and clicks pass straight through. prefers-reduced-motion: reduceswaps motion for a still lit border by default (reducedMotion: "static"); use"none"to render nothing or"ignore"to animate anyway.- The animated modes rewrite one CSS custom property per frame (a conic gradient string); blurred layers consume a low-resolution variant so the per-frame raster stays cheap.
preset="glow"and paused states run no loop at all. - The outer halo blooms past your element. If a parent has
overflow: hidden, the halo is clipped to it (the ring and inner glow are unaffected).
MIT © Hazem Aboelsoud