Portal Effect
A 140 px indigo ring spins in 6 s, a lighter arc making its rotation visible, around a glowing violet core that breathes in 3 s, and twelve particles scattered around it twinkle each at its own pace: a portal to another dimension, in CSS plus a few lines of JavaScript that place the particles — one instance per copy, and a still portal when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. 3D has 50 effects, including 8 free. Explore the category →
3 usage examples



How it works
The .fx-0047-root root (background #0a0a0f, at least 300 px tall) holds .xr-portal, a frame that takes the full width, clips what sticks out and centers the ring. .xr-portal-ring is a 140 px circle with a 3 px indigo edge at 60%, except the top arc, lavender at 95% (border-top-color): the ring makes a full turn in 6 s at constant speed, and that light arc is what makes the rotation visible; a uniform edge spinning looks still. Inside the ring, .xr-portal-inner, a 110 px disc in a violet radial gradient, breathes over 3 s: it grows by 10% and goes from 50 to 80% opacity, then back.
The JavaScript does one thing: for each .fx-0047-root on the page, it finds its .xr-portal and adds twelve .xr-portal-particle elements, 4 px #6366f1 dots with a glow. They are spread in a circle, one every 30°, at a random distance between 37.5 and 45% of the frame's width and height, with a random starting opacity too. Each twinkles over 2.4 s (scale from 0.6 to 1.5 and opacity from 20 to 100%) with a random negative delay: the twelve never light up together. No id, no global variable: two portals on a page each get their twelve particles (measured in Chromium 151, Firefox 153 and WebKit 26).
The positions are percentages of the .xr-portal frame, so their shape depends on its size. By default it takes the root's full width but only the ring's height (140 px); the particles then draw a very flat ellipse, wide and low. Give the frame a fixed size, for example 300 × 230 px as in the examples, for a rounder crown. The script runs once, when it is read: put it after the HTML, or use defer.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, a block scoped to.fx-0047-rootstops the ring, freezes the core mid-breath (scale 1.05, 65% opacity) and turns off the twinkle: the particles stay in place with their random opacity. Measured: 0% of pixels change over 0.8 s in Chromium, Firefox and WebKit, versus 0.9 to 1.3% in normal mode. - Decorative content: the portal is made of empty
divs that screen readers ignore. If the portal is clickable (go to the next level, next page of a story), wrap it in a real link or button with text, such as "Enter fairyland". - Contrast: the indigo ring measures 4.4:1 on the dark background, above the 3:1 threshold for graphical elements; the particles are deliberately faint. On a light background, darken the ring.
- Rhythm: 6 s rotation, 3 s breathing, staggered 2.4 s twinkles; no light flashes all at once.
Browser compatibility
CSS (transform and opacity animations) + 18 lines of dependency-free ES5 JavaScript. Measured in Chromium 151, Firefox 153 and WebKit 26 (Safari's engine): 0 errors, no global variable added, twelve particles in each of two copies, same stop under reduced motion. Not checked on a real Safari.
Without JavaScript, the portal stays whole (spinning ring, breathing core) but has no particles. If the script loads before the HTML, it finds no root: put it after, or use defer.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0047-root">
<div class="xr-portal">
<div class="xr-portal-ring">
<div class="xr-portal-inner"></div>
</div>
<!-- + 12 <div class="xr-portal-particle"> added by the JS -->
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Particle frame size (CSS — .xr-portal) |
Full width, ring height (140 px) | Particles are placed in percentages of the frame: give it a fixed size (width: 300px; height: 230px) for a round crown around the ring. |
| Colors (CSS — ring, light arc, core, particles) | Indigo rgba(99,102,241,.6), lavender arc, violet core, #6366f1 particles | Keep one arc clearly lighter than the rest of the edge (border-top-color), otherwise the rotation no longer shows. |
Rotation speed (CSS — 6s on .xr-portal-ring) |
6 s per turn | 14 s for a soothing portal, 3 s for a vortex. The core turns with the ring, invisibly: its gradient is round. |
Core breathing (CSS — 3s on .xr-portal-inner) |
3 s, +10%, 50 → 80% opacity | It is the effect's main rhythm: 4 to 5 s for calm breathing, 1.5 s for an unstable portal. |
| Number and distance of particles (JS) | 12 particles, at 37.5-45% of the frame | Change 12 in both places in the loop (i < 12 and i / 12), and 75 + Math.random() * 15 to bring them closer or push them out. |
Twinkle (CSS — fx0047Twinkle 2.4s) |
2.4 s, scale 0.6 → 1.5, opacity 20 → 100% | Slower (5 s) for calm scenery. If you change the duration, also change 2.4 in the JavaScript, which draws the delays within that range. |
FAQ
border-top-color) is the landmark: you see it go around in 6 s. If you recolor the ring, keep that difference, or use a dotted edge as in the story example..fx-0047-root and finds the frame by its class, with no id: each portal gets its twelve particles. Measured with two copies in Chromium, Firefox and WebKit: twelve particles and fourteen animations in each, no errors.