Aaditya

Wireframe Orb

AI chats

About

A voice orb drawn as a globe of latitude and longitude lines. The mesh deforms with the voice, pushing out for the agent and pressing in for the user, across eight conversation states.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Wireframe Orb to your project and fits it to your theme.

Code

How Wireframe Orb is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client"

import * as React from "react"
import { VoiceOrbState, WireframeOrb, createChatcnSpeechLevel, voiceOrbStateLabels } from "@/components/ui/wireframe-orb"

/** A turn of conversation that plays until you pick a state yourself. */
const SCRIPT: [VoiceOrbState, number][] = [
  ["listening", 3400],
  ["thinking", 1800],
  ["speaking", 4200],
  ["idle", 1400],
]

const FIT_CSS = "html,body{overflow:hidden}[data-fit-small] [data-fit-hide]{display:none!important}"

/**
 * 21st shows a demo in a small frame: about 674 by 421 on a component page, 358 by 224 on a
 * phone and 245 by 139 on a profile card. A demo taller than its frame gets scrollbars, which
 * render white on dark pages. FitFrame fills the frame exactly instead.
 *
 * - "scale": the content shrinks until it fits, centered. For visuals such as orbs.
 * - "crop": the content stays at full size from the top and fades out at the bottom. For text
 *   that streams in, where a changing scale would make the page breathe.
 *
 * Anything marked `data-fit-hide` (state buttons, the chatcn.dev line) drops out in frames
 * shorter than `hideBelow`, so a profile card shows the visual alone.
 */
function FitFrame({
  children,
  mode = "scale",
  hideBelow = 360,
}: {
  children: React.ReactNode
  mode?: "scale" | "crop"
  hideBelow?: number
}) {
  const content = React.useRef<HTMLDivElement>(null)
  const [cropped, setCropped] = React.useState(false)

  React.useLayoutEffect(() => {
    const el = content.current
    if (!el) return
    const measure = () => {
      const vw = window.innerWidth
      const vh = window.innerHeight
      el.toggleAttribute("data-fit-small", vh < hideBelow)
      // Layout sizes ignore the transform, so measuring never feeds back.
      const w = Math.max(el.offsetWidth, el.scrollWidth)
      const h = Math.max(el.offsetHeight, el.scrollHeight)
      const scale = mode === "scale" ? Math.min(1, vw / w, vh / h) : 1
      el.style.transform = scale < 1 ? `scale(${scale})` : ""
      setCropped(mode === "crop" && h > vh + 1)
    }
    measure()
    const observer = new ResizeObserver(measure)
    observer.observe(el)
    window.addEventListener("resize", measure)
    return () => {
      observer.disconnect()
      window.removeEventListener("resize", measure)
    }
  }, [mode, hideBelow])

  const fade = "linear-gradient(to bottom, #000 calc(100% - 40px), transparent)"
  return (
    <div
      className="flex w-full justify-center overflow-hidden"
      style={{
        height: "100dvh",
        alignItems: mode === "crop" ? "flex-start" : "center",
        maskImage: cropped ? fade : undefined,
        WebkitMaskImage: cropped ? fade : undefined,
      }}
    >
      <style>{FIT_CSS}</style>
      <div
        ref={content}
        className="w-full shrink-0"
        style={{ transformOrigin: mode === "crop" ? "top center" : "center" }}
      >
        {children}
      </div>
    </div>
  )
}

const STATES: VoiceOrbState[] = [
  "idle",
  "connecting",
  "listening",
  "thinking",
  "speaking",
  "interrupted",
  "muted",
  "error",
]

export default function WireframeOrbDemo() {
  const [state, setState] = React.useState<VoiceOrbState>("speaking")
  const [auto, setAuto] = React.useState(true)
  const user = React.useMemo(() => createChatcnSpeechLevel({ seed: 3 }), [])
  const agent = React.useMemo(() => createChatcnSpeechLevel({ seed: 7, loudness: 0.85 }), [])

  React.useEffect(() => {
    user.setSpeaking(state === "listening")
    agent.setSpeaking(state === "speaking")
  }, [state, user, agent])

  React.useEffect(() => {
    if (!auto) return
    let step = 2
    let timer = 0
    const next = () => {
      const [s, ms] = SCRIPT[step % SCRIPT.length]
      setState(s)
      step++
      timer = window.setTimeout(next, ms)
    }
    next()
    return () => window.clearTimeout(timer)
  }, [auto])

  return (
    <FitFrame>
      <div className="flex w-full flex-col items-center justify-center gap-8 p-6">
        <div className="flex flex-col items-center gap-4">
          <WireframeOrb
            size="lg"
            state={state}
            inputLevel={user.getLevel}
            outputLevel={agent.getLevel}
          />
          <p aria-live="polite" className="text-[0.875rem]/[1.25rem] tracking-[-0.003em] text-(color:--chatcn-fg-muted) h-5 font-medium">
            {voiceOrbStateLabels[state]}
          </p>
        </div>
        <fieldset data-fit-hide className="flex max-w-md flex-wrap justify-center gap-1">
          <legend className="sr-only">Orb state</legend>
          {STATES.map((s) => (
            <button
              key={s}
              type="button"
              aria-pressed={s === state}
              onClick={() => {
                setAuto(false)
                setState(s)
              }}
              className="text-[0.8125rem]/[1.25rem] text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) hover:bg-(color:--chatcn-fill) aria-pressed:bg-(color:--chatcn-fill-active) aria-pressed:text-(color:--chatcn-fg) rounded-(--chatcn-radius-md) h-7 px-2.5 font-medium transition-colors"
            >
              {voiceOrbStateLabels[s]}
            </button>
          ))}
        </fieldset>
        <p data-fit-hide className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint)">
          Dither, halftone, pixel and ASCII versions live on{" "}
          <a
            href="https://chatcn.dev/docs/components/wireframe-orb"
            target="_blank"
            rel="noopener"
            className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
          >
            chatcn.dev
          </a>
        </p>
      </div>
    </FitFrame>
  )
}

Dependencies

More by Aaditya

View all
  • Dither Orb

    Dither Orb

    7
  • Pixel Orb

    Pixel Orb

    3
  • Voice Orb

    Voice Orb

    2
  • Dither Gradient

    Dither Gradient

    1
  • Wireframe Globe

    Wireframe Globe

    1
  • Particle Text

    Particle Text

    1
  • Split Flap

    Split Flap

    1
  • Agent Lanes

    Agent Lanes

    1

More in AI chats

View all
  • AI Actions

    AI Actions

    335
  • System Message

    System Message

    71
  • Aquarium Prompt

    Aquarium Prompt

    9
  • Message With Header And Delivery Status

    Message With Header And Delivery Status

    50
  • Color Picker

    Color Picker

    154
  • Attachments

    Attachments

    30
  • Cost Meter

    Cost Meter

    41
  • ASCII Orb

    ASCII Orb

Explore more

Part of chatcn — browse the full library on 21st.dev.