---
name: energy-orb
description: Use when you want "Futuristic, magical, premium" - Creates a glowing circular object with particles, waves, or gradients.
---

# Energy Orb

> **Category:** Premium / Futuristic  -  **Personality:** Futuristic, magical, premium
>
> **Best use:** AI assistants, hero sections
>
> **Ratings:** Professional 3/5 - Casual 4/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 4/5 - Performance cost 4/5

## What it is

A luminous sphere that reads as a living ball of energy: a spherically-shaded gradient body with swirling internal plasma, a pulsing outer aura, and particles orbiting in front of and behind it. It is the natural centrepiece for an AI-assistant or voice hero, where a single tap flips it from a calm **idle** state to an energised **listening** state. The orb is built almost entirely from layered CSS gradients + blur; only the orbiting particles use a light `<canvas>` pass.

## Dependencies / CDN

None for the effect — pure CSS + vanilla `<canvas>`. The demo only loads two display/body fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Unbounded:wght@400;600;800&display=swap" rel="stylesheet">
```

## HTML

The orb is a `<button>` (so it is keyboard-focusable and toggles a state) wrapped between two canvases — one painted *behind* the sphere, one *in front* — so particles can pass around it in 3D:

```html
<div class="orbwrap" id="orbwrap">
  <canvas class="pcanvas back" id="back" aria-hidden="true"></canvas>
  <button class="orb" id="orb" type="button" aria-pressed="false" aria-label="Activate voice assistant">
    <span class="aura"></span>
    <span class="aura2"></span>
    <span class="sphere">
      <span class="plasma"></span>   <!-- swirling conic energy   -->
      <span class="plasma2"></span>  <!-- counter-rotating blobs   -->
      <span class="core"></span>     <!-- hot white centre          -->
      <span class="shine"></span>    <!-- top-left specular highlight-->
      <span class="spec"></span>
    </span>
  </button>
  <canvas class="pcanvas front" id="front" aria-hidden="true"></canvas>
</div>
<p class="status" aria-live="polite"><span id="status">Tap the orb to start</span></p>
```

## CSS

```css
.orbwrap{position:relative;width:300px;height:300px;margin:auto;overflow:visible}
.pcanvas{position:absolute;z-index:1;pointer-events:none;left:0;top:0}   /* JS sizes & positions */
.pcanvas.front{z-index:4}
.orb{position:absolute;inset:0;z-index:3;width:100%;height:100%;border:0;border-radius:50%;
  background:transparent;cursor:pointer;overflow:visible;transition:transform .5s cubic-bezier(.2,.8,.2,1)}
.orb:hover{transform:scale(1.015)}

/* pulsing aura behind the ball */
.aura{position:absolute;left:50%;top:50%;width:158%;height:158%;transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(circle,rgba(124,92,255,.5),rgba(124,92,255,.2) 36%,transparent 66%);
  filter:blur(8px);animation:pulse 4.6s ease-in-out infinite}

/* the sphere — radial gradient gives it spherical form; inset shadows add rim + shading */
.sphere{position:absolute;inset:0;border-radius:50%;overflow:hidden;transition:filter .6s ease;
  background:radial-gradient(circle at 31% 27%,#cabfff,#8b6bff 24%,#4a2fc0 50%,#23165f 76%,#0e0838 100%);
  box-shadow:inset 14px 16px 46px rgba(214,205,255,.5),inset -20px -26px 64px rgba(6,3,22,.8),
    inset 0 0 60px rgba(124,92,255,.45),0 0 70px rgba(124,92,255,.5)}

/* swirling internal energy — clipped to the sphere by overflow:hidden, blended with screen */
.plasma{position:absolute;left:-30%;top:-30%;width:160%;height:160%;border-radius:50%;
  filter:blur(20px);opacity:.78;mix-blend-mode:screen;animation:spin 9s linear infinite;
  background:conic-gradient(from 0deg,transparent,#ff5cf0 14%,transparent 32%,#46e6ff 52%,transparent 70%,#7c5cff 86%,transparent)}
.plasma2{position:absolute;left:-20%;top:-20%;width:140%;height:140%;border-radius:50%;
  filter:blur(22px);opacity:.66;mix-blend-mode:screen;animation:spin 13s linear infinite reverse;
  background:radial-gradient(closest-side circle at 36% 64%,#46e6ff,transparent 60%),
    radial-gradient(closest-side circle at 70% 40%,#ff5cf0,transparent 62%)}
.core{position:absolute;left:50%;top:48%;width:46%;height:46%;transform:translate(-50%,-50%);border-radius:50%;
  filter:blur(3px);animation:core 3.6s ease-in-out infinite;
  background:radial-gradient(circle,#fff,#e7e0ff 30%,rgba(155,123,255,.4) 58%,transparent 74%)}
.shine{position:absolute;left:16%;top:11%;width:50%;height:38%;border-radius:50%;filter:blur(5px);opacity:.8;
  background:radial-gradient(closest-side,rgba(255,255,255,.9),transparent 72%)}

@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{transform:translate(-50%,-50%) scale(1);opacity:.85}50%{transform:translate(-50%,-50%) scale(1.09);opacity:1}}
@keyframes core{0%,100%{transform:translate(-50%,-50%) scale(1);opacity:.92}50%{transform:translate(-50%,-50%) scale(1.12);opacity:1}}

/* LISTENING state — a single class hue-shifts the whole sphere toward cyan + speeds the pulses */
.is-listening .orb{transform:scale(1.035)}
.is-listening .sphere{filter:hue-rotate(-62deg) saturate(1.3) brightness(1.16)}
.is-listening .aura{animation-duration:2.4s}
.is-listening .core{animation-duration:1.7s}

/* reduced motion = static glowing orb (override the looping animations entirely) */
@media (prefers-reduced-motion: reduce){
  .aura,.plasma,.plasma2,.core{animation:none!important}
}
```

## JavaScript

Only the orbiting particles need JS. Each particle rides a tilted ellipse; the sign of `sin(angle)` decides whether it is drawn on the **front** canvas (over the orb) or the **back** canvas (occluded by the opaque sphere), and its depth scales size + brightness. A glow sprite drawn with `globalCompositeOperation='lighter'` keeps it cheap. Click toggles the listening state and eases an `energy` value that speeds/brightens everything.

```js
var wrap=document.getElementById('orbwrap'), orb=document.getElementById('orb'),
    back=document.getElementById('back'), front=document.getElementById('front'),
    bctx=back.getContext('2d'), fctx=front.getContext('2d'),
    reduce=matchMedia('(prefers-reduced-motion: reduce)').matches,
    dpr=Math.min(devicePixelRatio||1,2);

// soft white-hot glow sprite reused for every particle
var sprite=document.createElement('canvas'),SS=64;sprite.width=sprite.height=SS;
var sg=sprite.getContext('2d'),g=sg.createRadialGradient(32,32,0,32,32,32);
g.addColorStop(0,'rgba(255,255,255,1)');g.addColorStop(.5,'rgba(124,168,255,.36)');g.addColorStop(1,'rgba(124,92,255,0)');
sg.fillStyle=g;sg.fillRect(0,0,SS,SS);

var cx,cy,R,csize,pad;
function layout(){
  var W=wrap.getBoundingClientRect().width;R=W/2;pad=Math.round(W*.26);csize=W+pad*2;
  [back,front].forEach(function(c){
    c.style.left=c.style.top=-pad+'px';c.style.width=c.style.height=csize+'px';
    c.width=c.height=Math.round(csize*dpr);c.getContext('2d').setTransform(dpr,0,0,dpr,0,0);
  });
  cx=cy=csize/2;
}
var ps=[],i;function rnd(a,b){return a+Math.random()*(b-a);}
for(i=0;i<44;i++)ps.push({a:rnd(0,6.28),rad:rnd(1.02,1.32),incl:rnd(.16,.5),spd:rnd(.16,.42),sz:rnd(.5,1.25)});

var energy=0,target=0,listening=false,last=0,raf=0;
function draw(t){
  var dt=last?Math.min((t-last)/1000,.05):.016;last=t;
  energy+=(target-energy)*.055;var spd=1+energy*1.5,glow=.62+energy*.6;
  bctx.clearRect(0,0,csize,csize);fctx.clearRect(0,0,csize,csize);
  bctx.globalCompositeOperation=fctx.globalCompositeOperation='lighter';
  for(var i=0,p;i<ps.length;i++){p=ps[i];
    if(!reduce)p.a+=p.spd*spd*dt;
    var x=cx+Math.cos(p.a)*p.rad*R, y=cy+Math.sin(p.a)*p.incl*p.rad*R,
        d=Math.sin(p.a), df=d*.5+.5, ctx=d>=0?fctx:bctx,
        size=(5+p.sz*12)*(.55+df*.7), a=(.18+df*.72)*glow;
    ctx.globalAlpha=a>1?1:a; ctx.drawImage(sprite,x-size/2,y-size/2,size,size);
  }
  if(!reduce)raf=requestAnimationFrame(draw);
}
layout(); reduce?draw(0):(raf=requestAnimationFrame(draw));

orb.addEventListener('click',function(){
  listening=!listening; target=listening?1:0;
  wrap.classList.toggle('is-listening',listening);   // put is-listening on whatever holds the CSS classes
  orb.setAttribute('aria-pressed',listening?'true':'false');
  if(reduce)draw(0);
});
addEventListener('resize',function(){layout();if(reduce)draw(0);});
```

## How it works

- **The sphere is one element.** A `radial-gradient` whose bright stop sits at `31% 27%` fakes a light source top-left; the inset box-shadows add a lit rim, a dark bottom-right terminator, and an inner glow — together they make a flat circle read as a 3D ball.
- **Internal swirl** = two blurred layers (`plasma`, `plasma2`) clipped by the sphere's `overflow:hidden`, counter-rotating at different speeds with `mix-blend-mode:screen` so their colours add into living plasma.
- **Aura** is a big blurred radial-gradient behind the ball, scaled 1→1.09 on a slow `pulse` loop for the "breathing" glow.
- **Orbiting particles** ride tilted ellipses on a canvas. Splitting them across a **back** and **front** canvas (by the sign of `sin(angle)`) lets the opaque sphere occlude the far half, which is what sells real depth. `globalCompositeOperation='lighter'` makes overlapping particles glow.
- **Listen/idle** is one class toggle: `hue-rotate` swings the whole sphere violet→cyan and the animation-durations shorten, while a JS `energy` value eases up to speed and brighten the particles.

## Customization

- **Hue:** the sphere is built around violet `#7c5cff`; change that stop (and the aura/particle colours) for a different energy — teal, ember, magenta. The listening shift is just `hue-rotate(-62deg)`; tune the angle.
- **Calm vs frantic:** slow every `animation-duration` and lower `spd`/particle count for a meditative orb; speed them for a reactor.
- **Particle density:** change the `44` loop count and `pad` (orbit reach). More than ~80 starts to cost on low-end mobiles.
- **Size:** drive `.orbwrap` width with `clamp()`; the canvases re-measure on resize.

## Accessibility & performance

- Respect `prefers-reduced-motion`: override the looping CSS animations with `animation:none`, and in JS draw a **single static frame** instead of starting `requestAnimationFrame` — you still get a glowing orb, just frozen.
- The orb is a real `<button>` with `aria-pressed` + `aria-label`; the status line is an `aria-live="polite"` region so the idle→listening change is announced. Decorative spans/canvases are `aria-hidden`.
- One reused glow **sprite** + `drawImage` is far cheaper than per-particle `shadowBlur` or gradients. Cap `devicePixelRatio` at 2 and pause the loop on `visibilitychange` to save battery.
- Avoid animating `filter:blur()` radii (forces re-rasterisation) — animate `transform`/`opacity` instead, as here.

## Gotchas

- **Depth needs two canvases.** One canvas can only sit fully in front of or behind the orb; the front/back split is what makes particles look like they orbit *around* it. The sphere must be visually opaque for the back canvas to be occluded.
- **`mix-blend-mode:screen` needs a stacking context** to blend against the intended layers — wrap the stage in `isolation:isolate` or blends can leak onto the page.
- **Canvas sizing is in two places:** CSS pixels (`style.width`) *and* the backing store (`canvas.width = css*dpr`) plus `setTransform(dpr,…)`, or the orbits drift off-centre / render blurry. Re-run on resize.
- `hue-rotate` on the sphere also shifts the white core slightly; if you need a pure-white core, lift it out of the filtered element.
- Keep the orbit `pad` large enough that particles at `rad*1.3` still fit inside the (larger) canvas, or they clip at the far side.
