Portal Open
An appearance in two beats: a glowing cyan ring opens first in the center of the frame, then the content is revealed by a circle growing from the middle (clip-path: circle()), while the ring widens and fades; on closing, the circle shrinks back and the ring lights up again before retracting. 1.7 KB of JavaScript, closed content out of keyboard reach, a 0.15 s fade under reduced motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The .portal-open-wrapper frame (200 × 120 px, 14 px corners, overflow: hidden) stacks two elements. The .portal-open-content, a cyan #06b6d4 → #0891b2 gradient, is fully masked at rest by clip-path: circle(0 at 50% 50%): a zero-radius circle in the center. The .portal-open-ring is a centered round with a 3 px #22d3ee border and a glow (outer and inner box-shadow), at size 0 and opacity 0.
The script (1.7 KB) splits the opening into three steps. At 0 ms, the .portal-open-active class grows the ring to 200 px and lights it in 0.5 s. At 200 ms, .portal-open-visible opens the clipping circle to circle(75%) in 0.7 s (enough to reach the frame's corners). At 500 ms, .portal-open-ring-fade widens the ring to 240 px and fades it in 0.4 s after a 0.3 s wait. Measured in Chromium 151: at 150 ms, a half-lit 109 px ring and content still hidden; at 400 ms, circle at 40% and ring almost full; around 1.2 s, content whole and ring off — same steps within a few percent in Firefox 153 and WebKit 26.5. Closing reverses the order: the ring lights up again, the circle closes, and the ring retracts 400 ms later.
Each click first cancels the pending timers: a fast double click ends in a consistent state (measured: portal closed, button at aria-expanded="false"). The button points to its frame with aria-controls, which allows several independent portals on the page. When closed, the content carries inert: invisible, it must not be reached by keyboard or read either. The ring animates width and height rather than a scale, to keep a crisp 3 px border at every size; it is a single absolutely positioned element, so the layout cost stays minimal.
Accessibility
- Inert closed content: a
clip-pathhides the content from the eye, not from the keyboard or screen readers. The script therefore setsinerton the content while the portal is closed. Measured with a link in the content: portal closed, Tab goes straight to the button (Chromium, Firefox); open, Tab reaches the link. - Button state: a real
<button>witharia-controlsandaria-expanded, updated on every click (measured). The purely decorative ring isaria-hidden. - Reduced motion: under
prefers-reduced-motion, the ring is not shown and the circle does not open: the content fades in over 0.15 s, after the same 200 ms (measured in all three engines: content at 1 at 400 ms, noclip-path). - Glow and photosensitivity: the ring lights up once per opening, with no repeated flashing: no flash-related risk. White text contrast on the cyan gradient: 2.4 to 3.7:1, too low for body text — darken the gradient (
#0e7490→#155e75: 5.4 and 7.3:1) or put the text on a dark band, as scene 2 does.
Browser compatibility
CSS (animated clip-path: circle(), size and opacity transitions) + 1.7 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26.5: same opening and closing steps, double click with no stuck state, inert content once closed, two independent portals. Animating clip-path: circle() works unprefixed since Chrome 55, Firefox 54 and Safari 12.1; the inert attribute since Chrome 102, Firefox 112 and Safari 15.5.
Before inert (Chrome 55-101, Safari 12.1-15.4), the animation is the same but the closed content stays reachable by keyboard. Without JavaScript, the portal stays closed. Under reduced motion, a short fade replaces the ring and the circle.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="portal-open-stage">
<div class="portal-open-wrapper" id="portalOpenWrapper">
<div class="portal-open-ring" aria-hidden="true"></div>
<div class="portal-open-content">…</div>
</div>
<button class="portal-open-btn" type="button" aria-controls="portalOpenWrapper" aria-expanded="false">…</button>
</div>
The full HTML + CSS + JS of Portal Open, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Offsets (JS — 200 and 500 ms on opening, 400 ms on closing) |
ring, then circle at 200 ms, fade at 500 ms | Raise the first to 400 ms for a ring that "charges" longer before opening; at 0, ring and circle start together and the announcing effect is gone. |
Ring size (CSS — 200px then 240px) |
200 px then 240 px when fading | Match it to your frame: roughly the frame's smaller dimension, then +20%. A ring larger than the frame is clipped by its overflow: hidden. |
Circle opening (CSS — circle(75% at 50% 50%), 0.7 s) |
75%: the corners are reached | at 50% 100% starts the circle from the bottom; for a very elongated frame, go up to 100% so the corners are not left empty. |
Color (CSS — border-color and box-shadow of .portal-open-ring) |
cyan #22d3ee with a glow |
Change it on the ring's base rule, not only on the active state, otherwise it goes back to cyan during closing. |
Frame shape (CSS — .portal-open-wrapper) |
200 × 120 px, 14 px corners | border-radius: 50% on a square frame gives a real porthole; the clipping circle and the ring stay centered. |
FAQ
.portal-open-ring-fade in the closing branch: the ring will stay off.clip-path. A <video> or an <img> with object-fit: cover over 100% of the frame is revealed the same way. Remember to start playback only on opening, and to pause it on closing.