# Ringed Planet Dust Bands

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/ringed-planet-background-with-orbiting-dust-bands-and-ring-shadow).

- Page, with a live preview: https://shadcn.exchange/r/weinc/ringed-planet-background-with-orbiting-dust-bands-and-ring-shadow
- Screenshot: https://shadcn.exchange/shot/weinc/ringed-planet-background-with-orbiting-dust-bands-and-ring-shadow/light.png
- Registry item: https://we.inc/r/ringed-planet-background-with-orbiting-dust-bands-and-ring-shadow.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/ringed-planet-background-with-orbiting-dust-bands-and-ring-shadow.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/ringed-planet-background-with-orbiting-dust-bands-and-ring-shadow.tsx`

```tsx
import { useEffect, useRef } from "react";

// The values worth changing.
const SETTINGS = {
  dust: [236, 220, 194], // ring dust colour as [red, green, blue]
  planet: ["#e3c79c", "#9c7451", "#2a1c14"], // lit side, middle, night side
  grains: 3200, // dust grains in the ring; fewer is cheaper
  tilt: -0.22, // how much the ring leans, in radians
  speed: 1, // how fast the ring turns
};

const SCENE: any = {
  start: 0,
  still: 0,
  init(ctx: any, w: any, h: any) {
    const narrow = w < 700;
    const R = narrow ? w * 0.3 : Math.min(w * 0.15, h * 0.3);
    const cx = narrow ? w * 0.6 : w * 0.72;
    const cy = narrow ? h * 0.8 : h * 0.56;
    // Bands of dust with gaps between them, like real ring systems.
    const bands = [[1.38, 1.55, 0.5], [1.58, 1.86, 1], [1.93, 2.12, 0.75], [2.16, 2.24, 0.35]];
    const grains: any[] = [];
    for (let i = 0; i < SETTINGS.grains; i++) {
      const b = bands[Math.floor(Math.random() * bands.length)];
      const r = b[0] + Math.random() * (b[1] - b[0]);
      grains.push({ r, a: Math.random() * 6.2832, s: Math.random() < 0.08 ? 1.6 : 1, l: b[2] * (0.35 + Math.random() * 0.65) });
    }
    const stars: any[] = [];
    for (let i = 0; i < 160; i++) stars.push({ x: Math.random() * w, y: Math.random() * h, a: 0.15 + Math.random() * 0.6, s: Math.random() < 0.1 ? 1.5 : 1 });
    return { R, cx, cy, bands, grains, stars };
  },
  ring(ctx: any, st: any, t: any, front: any) {
    const { R, cx, cy } = st;
    const flat = 0.24;
    const [dr, dg, db] = SETTINGS.dust;
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(SETTINGS.tilt);
    // A faint continuous sheen under the grains gives each band body.
    for (const b of st.bands) {
      ctx.strokeStyle = `rgba(${dr},${dg},${db},${(front ? 0.3 : 0.2) * b[2]})`;
      ctx.lineWidth = (b[1] - b[0]) * R * flat * 1.15;
      ctx.beginPath();
      ctx.ellipse(0, 0, ((b[0] + b[1]) / 2) * R, ((b[0] + b[1]) / 2) * R * flat, 0, front ? 0 : Math.PI, front ? Math.PI : Math.PI * 2);
      ctx.stroke();
    }
    ctx.fillStyle = `rgb(${dr},${dg},${db})`;
    for (const g of st.grains) {
      // Inner dust orbits faster than outer dust.
      const a = g.a + (t * SETTINGS.speed * 0.12) / Math.pow(g.r, 1.5);
      const sa = Math.sin(a);
      if (front ? sa < 0 : sa >= 0) continue;
      const x = Math.cos(a) * g.r * R;
      const y = sa * g.r * R * flat;
      // Dust on the far right is lit by the sun; dust in the planet's shadow goes dark.
      const shadow = !front && Math.abs(x) < R * 1.02 ? 0.25 : 1;
      ctx.globalAlpha = Math.min(1, g.l * shadow * (0.7 + 0.5 * Math.cos(a - 0.6)));
      ctx.fillRect(x, y, g.s, g.s);
    }
    ctx.globalAlpha = 1;
    ctx.restore();
  },
  draw(ctx: any, w: any, h: any, t: any, st: any) {
    ctx.fillStyle = "#05070d";
    ctx.fillRect(0, 0, w, h);
    for (const s of st.stars) {
      ctx.globalAlpha = s.a;
      ctx.fillStyle = "#e8ecf5";
      ctx.fillRect(s.x, s.y, s.s, s.s);
    }
    ctx.globalAlpha = 1;
    SCENE.ring(ctx, st, t, false);
    const { R, cx, cy } = st;
    // The planet: lit from the upper left, with soft cloud belts and a dark limb.
    ctx.save();
    ctx.beginPath();
    ctx.arc(cx, cy, R, 0, 6.2832);
    ctx.clip();
    const body = ctx.createRadialGradient(cx - R * 0.45, cy - R * 0.5, R * 0.1, cx, cy, R * 1.15);
    body.addColorStop(0, SETTINGS.planet[0]);
    body.addColorStop(0.55, SETTINGS.planet[1]);
    body.addColorStop(1, SETTINGS.planet[2]);
    ctx.fillStyle = body;
    ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
    ctx.translate(cx, cy);
    ctx.rotate(SETTINGS.tilt);
    for (let i = -9; i <= 9; i++) {
      const y = (i / 9) * R + Math.sin(i * 1.7 + t * 0.02) * 3;
      ctx.fillStyle = i % 2 ? "rgba(60,36,20,0.1)" : "rgba(255,236,200,0.06)";
      ctx.fillRect(-R * 1.2, y, R * 2.4, R * (0.05 + 0.04 * Math.abs(Math.sin(i * 2.3))));
    }
    ctx.rotate(-SETTINGS.tilt);
    // Night side: the sun is up and to the left, so the lower right falls into shadow.
    const night = ctx.createRadialGradient(-R * 0.55, -R * 0.6, R * 0.6, -R * 0.35, -R * 0.4, R * 2.1);
    night.addColorStop(0, "rgba(5,7,13,0)");
    night.addColorStop(0.55, "rgba(5,7,13,0.55)");
    night.addColorStop(1, "rgba(5,7,13,0.97)");
    ctx.fillStyle = night;
    ctx.fillRect(-R, -R, R * 2, R * 2);
    ctx.restore();
    // A thin bright crescent on the lit edge.
    ctx.strokeStyle = "rgba(255,240,215,0.35)";
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.arc(cx, cy, R - 0.75, Math.PI * 0.95, Math.PI * 1.6);
    ctx.stroke();
    SCENE.ring(ctx, st, t, true);
  },
};

// Runs a scene on a canvas: sizes it to the block, pauses off screen and in hidden tabs, cleans up after itself.
function runScene(root: any, canvas: any, scene: any, reduced: boolean) {
  const ctx = canvas.getContext("2d");
  if (!ctx) return () => {};
  let w = 0;
  let h = 0;
  let st: any = null;
  let raf = 0;
  let last = 0;
  let acc = 0;
  let onScreen = true;
  let time = reduced ? (scene.still ?? scene.start ?? 0) : (scene.start ?? 0);
  const ptr: any = { x: 0, y: 0, on: false, down: 0 };
  const step = 1 / (scene.fps || 60);
  const resize = () => {
    const r = root.getBoundingClientRect();
    const nw = Math.max(1, Math.round(r.width));
    const nh = Math.max(1, Math.round(r.height));
    if (nw === w && nh === h && st) return;
    w = nw;
    h = nh;
    // Device pixels are capped at 1.5 and scaled by the scene's own quality factor.
    const d = Math.min(window.devicePixelRatio || 1, 1.5) * (scene.scale || 1);
    canvas.width = Math.max(1, Math.round(w * d));
    canvas.height = Math.max(1, Math.round(h * d));
    ctx.setTransform(canvas.width / w, 0, 0, canvas.height / h, 0, 0);
    st = scene.init(ctx, w, h, canvas.width / w);
    scene.draw(ctx, w, h, time, st, ptr, 0);
  };
  const tick = (now: number) => {
    raf = requestAnimationFrame(tick);
    const dt = last ? Math.min(0.1, (now - last) / 1000) : step;
    last = now;
    acc += dt;
    if (acc < step * 0.95) return;
    const used = Math.min(acc, 0.1);
    acc = 0;
    time += used;
    scene.draw(ctx, w, h, time, st, ptr, used);
  };
  const play = () => {
    if (raf || reduced || !onScreen || document.hidden) return;
    last = 0;
    raf = requestAnimationFrame(tick);
  };
  const stop = () => {
    cancelAnimationFrame(raf);
    raf = 0;
  };
  const ro = new ResizeObserver(resize);
  ro.observe(root);
  const io = new IntersectionObserver((entries: any) => {
    onScreen = entries[0].isIntersecting;
    if (onScreen) play();
    else stop();
  });
  io.observe(root);
  const onVis = () => (document.hidden ? stop() : play());
  const onMove = (e: any) => {
    const r = root.getBoundingClientRect();
    ptr.x = e.clientX - r.left;
    ptr.y = e.clientY - r.top;
    ptr.on = true;
  };
  const onLeave = () => {
    ptr.on = false;
  };
  const onDown = (e: any) => {
    onMove(e);
    ptr.down += 1;
  };
  document.addEventListener("visibilitychange", onVis);
  root.addEventListener("pointermove", onMove);
  root.addEventListener("pointerleave", onLeave);
  root.addEventListener("pointerdown", onDown);
  resize();
  play();
  return () => {
    stop();
    ro.disconnect();
    io.disconnect();
    document.removeEventListener("visibilitychange", onVis);
    root.removeEventListener("pointermove", onMove);
    root.removeEventListener("pointerleave", onLeave);
    root.removeEventListener("pointerdown", onDown);
  };
}

export default function RingedPlanetBackground() {
  const rootRef = useRef<HTMLElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  useEffect(() => {
    const root = rootRef.current;
    const canvas = canvasRef.current;
    if (!root || !canvas) return;
    // Visitors who prefer reduced motion get one settled frame and no loop.
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    return runScene(root, canvas, SCENE, reduced);
  }, []);

  return (
    <section ref={rootRef} className="relative h-[660px] w-full overflow-hidden bg-[#05070d] text-[#f1f2f6]">
      <canvas ref={canvasRef} aria-hidden="true" className="pointer-events-none absolute inset-0 block h-full w-full" />
      <div className="relative z-10 mx-auto flex h-full max-w-6xl flex-col justify-start items-start text-left px-6 py-14 sm:px-10">
        <div className="max-w-xl">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-[#ead9bc]">Perihelion Observatory</p>
          <h2 className="mt-4 text-4xl font-semibold leading-[1.05] tracking-tight sm:text-5xl lg:text-6xl">Public nights under a dark sky.</h2>
          <p className="mt-5 text-base leading-relaxed text-[#c4c9d6] sm:text-lg">Look through the big telescope on the ridge every clear Friday, with an astronomer at the eyepiece.</p>
          <a href="#" className="mt-8 inline-flex h-12 items-center rounded-[6px] bg-[#ead9bc] px-6 text-sm font-semibold text-[#14100a] transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ead9bc] focus-visible:ring-offset-2 focus-visible:ring-offset-[#05070d]">Book a viewing night</a>
        </div>
      </div>
    </section>
  );
}
```
