Lantern
Orbit.
The dashed elliptical rings behind the Lantern hero terminal, as a decorative background layer with optional slow rotation.
hub://your-little-world/
$lantern publish ./site
published hub://your-little-world/
$
Connected3 sites
import { Diamond } from "lucide-react";
import { Orbit } from "@/components/ui/orbit";
import {
Terminal,
TerminalAddress,
TerminalBody,
TerminalCursor,
TerminalFooter,
TerminalHeader,
TerminalLine,
TerminalOutput,
} from "@/components/ui/terminal";
export default function OrbitDemo() {
return (
<div className="relative mx-auto w-full max-w-[420px] px-2 py-24">
<Orbit size={380} />
<Terminal tilt>
<TerminalHeader>
<Diamond /> LANTERN
</TerminalHeader>
<TerminalAddress>hub://your-little-world/</TerminalAddress>
<TerminalBody>
<TerminalLine>lantern publish ./site</TerminalLine>
<TerminalOutput>published hub://your-little-world/</TerminalOutput>
<TerminalLine>
<TerminalCursor />
</TerminalLine>
</TerminalBody>
<TerminalFooter>
<span>Connected</span>
<span>3 sites</span>
</TerminalFooter>
</Terminal>
</div>
);
}Installation#
With the CLI
$npx lanterncn add orbitThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/orbit. See Installation if your project is not set up yet.
Manually
Copy the source into your project
components/ui/orbit.tsximport * as React from "react"; import { cn } from "@/lib/utils"; type OrbitRingProps = React.ComponentProps<"span"> & { /** Ring width in px or any CSS length. */ width?: number | string; /** Ring height in px or any CSS length. Defaults to the width (a circle). */ height?: number | string; /** Tilt in degrees. */ rotation?: number; /** Offset from the center of the layer, in px. */ x?: number; y?: number; /** Slowly rotate the ring. Skipped when the visitor prefers reduced motion. */ spin?: boolean; /** Seconds per full turn when spinning. */ duration?: number; reverse?: boolean; }; const toLength = (v: number | string) => (typeof v === "number" ? `${v}px` : v); /** One dashed elliptical ring. Use inside Orbit to build your own arrangement. */ function OrbitRing({ className, style, width = 450, height, rotation = -20, x = 0, y = 0, spin = false, duration = 120, reverse = false, ...props }: OrbitRingProps) { return ( <span data-slot="orbit-ring" className={cn( "absolute top-1/2 left-1/2 block max-w-full rounded-full border border-dashed border-[#5a633b60]", spin && "motion-safe:animate-spin", className, )} style={{ width: toLength(width), height: toLength(height ?? width), translate: `calc(-50% + ${x}px) calc(-50% + ${y}px)`, rotate: `${rotation}deg`, ...(spin ? { animationDuration: `${duration}s`, animationDirection: reverse ? "reverse" : "normal" } : null), ...style, }} {...props} /> ); } type OrbitProps = React.ComponentProps<"div"> & { /** Number of generated rings. Ignored when you pass OrbitRing children. */ count?: number; /** Width of the outer ring in px. Each next ring gets narrower and taller, like the Lantern hero. */ size?: number; /** Tilt of every ring in degrees. */ rotation?: number; /** Slowly rotate the rings, alternating direction. Respects prefers-reduced-motion. */ spin?: boolean; /** Seconds per full turn for the outer ring. */ duration?: number; /** Extra classes for every generated ring, such as a border color. */ ringClassName?: string; }; /** * The dashed orbit rings behind the Lantern hero terminal. A decorative, absolutely positioned * layer: put it inside a relative parent, before the content it sits behind. */ function Orbit({ className, children, count = 2, size = 450, rotation = -20, spin = false, duration = 120, ringClassName, ...props }: OrbitProps) { return ( <div data-slot="orbit" aria-hidden="true" className={cn("pointer-events-none absolute inset-0 select-none", className)} {...props} > {children ?? Array.from({ length: count }, (_, i) => ( <OrbitRing key={i} width={Math.round(size * (1 - 0.22 * i))} height={Math.round(size * (1 + 0.11 * i))} x={Math.round(size * 0.07 * i)} y={Math.round(-size * 0.03 * i)} rotation={rotation} spin={spin} duration={duration + i * 30} reverse={i % 2 === 1} className={ringClassName} /> ))} </div> ); } export { Orbit, OrbitRing };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { Orbit, OrbitRing } from "@/components/ui/orbit";<div className="relative">
<Orbit size={400} />
<Terminal tilt>...</Terminal>
</div>Orbit is absolutely positioned and hidden from screen readers. Place it inside a relative parent, before the content it sits behind; that content should be positioned (Terminal already is) so it paints on top.
spin rotates the rings slowly in alternating directions and is skipped when the visitor prefers reduced motion. Pass OrbitRing children to lay out rings yourself.
Examples#
Spinning
Relay online
turtle-07
import { Orbit } from "@/components/ui/orbit";
export default function OrbitSpin() {
return (
<div className="relative flex h-72 w-full max-w-md items-center justify-center overflow-hidden">
<Orbit count={3} size={260} rotation={-30} spin duration={60} ringClassName="border-[#5a633b90]" />
<div className="relative text-center">
<div className="font-mono text-[10px] tracking-[0.2em] text-success uppercase">Relay online</div>
<div className="mt-1 font-display text-2xl font-medium tracking-tight">turtle-07</div>
</div>
</div>
);
}Custom rings
import { Orbit, OrbitRing } from "@/components/ui/orbit";
export default function OrbitCustom() {
return (
<div className="relative flex h-72 w-full max-w-md items-center justify-center overflow-hidden">
<Orbit>
<OrbitRing width={300} height={120} rotation={-12} />
<OrbitRing width={220} height={220} rotation={0} className="border-primary/30" />
<OrbitRing width={120} height={300} rotation={24} x={20} />
</Orbit>
<span className="relative size-3 rounded-full bg-primary shadow-[0_0_24px_#f5a66580]" />
</div>
);
}