# Animated boxes

> Ghosty's box robots for your site or app — states, shapes, screens, colours, themes and a summary row you reorder by dragging. Package @ghostystudio/boxes.

URL: https://www.ghosty.studio/en/docs/reference/boxes

Every Ghosty box is a TV-headed robot: its face says what it is doing (asleep, listening, working, failing), it follows the mouse and hops when clicked. It is the same one you see in **Space boxes**, and we publish it as a free package (MIT) so you can use it in your own pages and apps: `@ghostystudio/boxes`.

## Install

:::tabs
```html tab=HTML
<script src="https://unpkg.com/@ghostystudio/boxes/dist/boxes.iife.js"></script>

<ghosty-box state="happy"></ghosty-box>
```
```js tab=npm
// npm i @ghostystudio/boxes
import { defineGhostyBox, defineGhostyBoxes } from '@ghostystudio/boxes';
defineGhostyBox();   // <ghosty-box>
defineGhostyBoxes(); // <ghosty-boxes>
```
```tsx tab=React
// npm i @ghostystudio/boxes  (React 18 or 19)
import { RobotHead } from '@ghostystudio/boxes/react';

export function MyBox() {
  return <RobotHead state="happy" size={120} />;
}
```
:::

It works in plain HTML and in any framework: it is a custom element. React gets two components: `<RobotHead>` (one box) and `<GhostyBoxes>` (the summary row). They ship with `"use client"`, so in Next.js (App Router) you can use them as they are inside a Server Component. Every example below has a **React** tab.

## States

What the box is doing. Each state has its own face, motion and light.

:::boxes
<ghosty-box state="idle" size="96" title="idle"></ghosty-box>
<ghosty-box state="thinking" size="96" title="thinking"></ghosty-box>
<ghosty-box state="searching" size="96" title="searching"></ghosty-box>
<ghosty-box state="listening" size="96" title="listening"></ghosty-box>
<ghosty-box state="speaking" size="96" title="speaking"></ghosty-box>
<ghosty-box state="working" size="96" title="working"></ghosty-box>
<ghosty-box state="happy" size="96" title="happy"></ghosty-box>
<ghosty-box state="success" size="96" title="success"></ghosty-box>
<ghosty-box state="error" size="96" title="error"></ghosty-box>
<ghosty-box state="sleeping" size="96" title="sleeping"></ghosty-box>
:::

## Shapes

:::boxes
<ghosty-box shape="rectangle" size="110" title="rectangle"></ghosty-box>
<ghosty-box shape="square" size="110" title="square"></ghosty-box>
<ghosty-box shape="circle" size="110" title="circle"></ghosty-box>
<ghosty-box shape="hexagon" size="110" title="hexagon"></ghosty-box>
:::

Seasonal ones too: `tree` (Christmas) and `pumpkin` (Halloween).

## Screen

`face` picks what it shows: eyes, a terminal or nothing. With the terminal, `screen-icon` adds an icon: `mail`, `inbox`, `build`, `ci`, `preview`, `desktop` or `web` (register your own with `registerScreenIcon`).

:::boxes
<ghosty-box face="eyes" size="110" title="eyes"></ghosty-box>
<ghosty-box face="terminal" state="listening" size="110" title="terminal"></ghosty-box>
<ghosty-box face="terminal" screen-icon="mail" size="110" title="mail"></ghosty-box>
<ghosty-box face="terminal" screen-icon="inbox" state="working" size="110" title="inbox · working"></ghosty-box>
<ghosty-box face="off" size="110" title="off"></ghosty-box>
:::

## Colours, themes and finishes

A theme paints the box with one word; anything you set by hand wins over it. `color` is the shell, `trim-color` the ears and `screen-color` the LEDs. `texture` changes the finish and `wear` (0–1) adds wear and tear.

:::boxes
<ghosty-box theme="ghosty" size="110" title="ghosty"></ghosty-box>
<ghosty-box theme="mailmask" size="110" title="mailmask"></ghosty-box>
<ghosty-box theme="mailmaskGreen" size="110" title="mailmaskGreen"></ghosty-box>
<ghosty-box color="#7c3aed" trim-color="#fbbf24" screen-color="#fde68a" size="110" title="by hand"></ghosty-box>
<ghosty-box color="#8a8f98" texture="metal" size="110" title="metal"></ghosty-box>
<ghosty-box color="#a0522d" texture="wood" wear="0.6" size="110" title="wood · wear"></ghosty-box>
:::

Your own theme: `registerTheme('mybrand', { color: '#…', trimColor: '#…', screenColor: '#…' })`, then `theme="mybrand"`.

## A summary row you reorder by dragging

`<ghosty-boxes>` is the summary row: title, counter, one box per item with its name, state and dots, and free slots switched off. Grab a box: it lifts, swings and smiles; drop it and it lands with a hop. Keyboard too: Alt + ← / →.

:::boxes
<ghosty-boxes theme="mailmask" data='{"title":"Mailboxes","count":"4/6","sub":"12 unread · 1 importing","items":[{"id":"hello","name":"hello@","label":"8 unread","tone":"talk","dots":{"on":8,"cap":10},"head":{"state":"happy"}},{"id":"sales","name":"sales@","label":"importing","tone":"work","dots":{"on":3,"cap":10},"head":{"state":"working","screenIcon":"inbox"}},{"id":"support","name":"support@","label":"4 unread","tone":"talk","dots":{"on":4,"cap":10},"head":{"state":"listening"}},{"id":"invoices","name":"invoices@","label":"almost full","tone":"error","dots":{"on":10,"cap":10},"head":{"state":"error"}},{"id":"f1","name":"free","off":true},{"id":"f2","name":"free","off":true}]}'></ghosty-boxes>
:::

Data can also come from code, and the new order comes back in an event:

:::tabs
```js tab=JavaScript
const row = document.querySelector('ghosty-boxes');
row.data = { title: 'Mailboxes', count: '4/6', items: [/* … */] };
row.addEventListener('box-reorder', (e) => save(e.detail.order)); // { id, from, to, order }
row.addEventListener('box-open', (e) => open(e.detail.id));
```
```tsx tab=React
import { useState } from 'react';
import { GhostyBoxes, type SummaryItem } from '@ghostystudio/boxes/react';

export function Mailboxes({ initial }: { initial: SummaryItem[] }) {
  const [items, setItems] = useState(initial);
  return (
    <GhostyBoxes
      theme="mailmask"
      data={{ title: 'Mailboxes', count: `${items.length}/6`, items }}
      // { id, from, to, order }: keep your state in the new order and save it
      onReorder={({ order }) => {
        setItems(order.map((id) => items.find((it) => it.id === id)!));
        save(order);
      }}
      onOpen={({ id }) => open(id)}
    />
  );
}
```
:::

Each item has `id`, `name`, and optional `label`, `tone` (`work`, `talk`, `error`, `sleep`, `calm`), `dots` (`{ on, cap }`), `off` (a fixed free slot), `group` (it only moves within its group) and `head` (its box's props). The card's colours come from CSS variables on `.gbx`: `--gbx-bg`, `--gbx-border`, `--gbx-fg`, `--gbx-muted`, `--gbx-work`, `--gbx-talk`, `--gbx-error`, `--gbx-dot`, `--gbx-radius`.

## `<ghosty-box>` attributes

| Attribute | Values | Default |
|---|---|---|
| `theme` | `ghosty`, `mailmask`, `mailmaskGreen` or your own | — |
| `state` | `idle`, `thinking`, `searching`, `listening`, `speaking`, `working`, `happy`, `success`, `error`, `sleeping` | `idle` |
| `shape` | `rectangle`, `square`, `circle`, `hexagon`, `tree`, `pumpkin` | `rectangle` |
| `size` | px | `160` |
| `color` · `trim-color` · `screen-color` | hex or `rgb()` | blue · steel · white |
| `face` | `eyes`, `terminal`, `off` | `eyes` |
| `screen-icon` | with terminal: `mail`, `inbox`, `build`, `ci`, `preview`, `desktop`, `web` | — |
| `texture` | `grain`, `smooth`, `paper`, `fabric`, `metal`, `leather`, `wood`, `ribs` | `grain` |
| `wear` | 0–1 | `0` |
| `speed` | multiplier; `0` freezes it | `1` |
| `seed` | 0–1, so a row does not blink in unison | random |
| `paused` · `interactive="false"` · `antenna="false"` · `floor-shadow="false"` | flags | — |

In React they are the same props in camelCase (`trimColor`, `screenIcon`, `floorShadow`), with numbers and flags in braces (`size={96}`, `antenna={false}`). It respects `prefers-reduced-motion`. Attribute changes show up instantly.

The robot is [robot-heads](https://github.com/fayazara/robot-heads) by Fayaz Ahmed (MIT), with states, shapes, screens, themes and finishes added by Ghosty.
