Ghost Typewriter

ProInteractivity3DWebGLpowered by @vectorvesper/motion

A keyboard broken into six fragments, hanging in the air around a headline that types itself. Each keycap presses as its letter lands. Drag a fragment anywhere, spin it with the right button, roll it with the wheel — then read the arrangement back out as coordinates. Type on your own keyboard and the caps answer.

Ghost Typewriter

Included with membership

Members install Ghost Typewriter with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.

When to use it

The top of a page you want remembered — a homepage hero, a product launch, a landing page where the headline is the whole pitch. It wants the full width of the screen and a good deal of height. One per page: it takes the keyboard, and two of them would fight over it.

  • Keep title text short and single-word if you can. It is set very large and uppercase, and the layout is built around one word landing in the middle.
  • Use variant="paragraph" for anything conversational. It switches to a smaller editorial setting and a quicker default cadence, and auto picks it for you once the text runs past twenty characters or contains a space.
  • Set captureKeyboard={false} if the page has anything else keyboard-driven below it. Left on, any keystroke on the page presses a cap.
  • Load the display face yourself and name it in fontFamily. The default stack asks for Plus Jakarta Sans and falls back to the system UI face if you have not loaded it.
  • Arrange the fragments by hand, then read them back with the ref's getLayout() and paste the result into layout. That is what the drag, spin and wheel controls are for.

Avoid:

  • Do not pass a long paragraph with autoPlay on and a slow typingSpeed. At the default title speed a forty-character sentence takes half a minute to finish and then waits before starting again.
  • Do not expect every character to have a cap. The fragments carry A–Z, space and backspace; anything else — digits, punctuation, accents — presses a random cap instead, which is deliberate but looks arbitrary if the text is full of them.
  • Do not put interactive controls in children without pointer-events-auto. The overlay is pointer-transparent so the fragments stay draggable underneath it.
  • Do not drive text from state that changes every keystroke of a real form. Changing text resets the typing sequence to the first character.
  • Do not use it as a section in the middle of a long page with captureKeyboard left on. Someone reading further down who presses a key will set an off-screen keyboard clattering.

Props

A mechanical keyboard taken apart and left hanging in the air — six fragments, each a circuit board under a sheet of frosted polycarbonate, with real keycaps on switch stems. A headline types itself in the middle, and the matching cap presses as each letter lands. Type on your own keyboard and the caps answer. Every fragment can be dragged, spun with the right button and rolled with the wheel, and the arrangement you end up with can be read back out as coordinates and passed straight back in.

<GhostTypewriter />

The scene, the typed headline and all the interaction. Paints its own background and fills its container. Also exported as SuspendedKeycapTyping; the two names are the same component.

PropTypeDefaultDescription
textstring"ANTICIPATION"What gets typed. Changing it restarts the sequence from the first character.
subtitlestring—Small monospaced kicker above the headline. Omit it and nothing is rendered.
variant"title" | "paragraph" | "auto""auto"How the text is set. "title" is large and uppercase, "paragraph" is smaller editorial prose. "auto" picks paragraph once the text passes twenty characters or contains a space.
theme"studio" | "obsidian" | "jade""studio"Colour preset. studio is a warm off-white room, obsidian near-black, jade a dark green. Sets the background, text, board colour, shell tint and the colour a cap flashes when pressed.
typingSpeed"slow" | "normal" | "fast" | "rapid" | number650ms for title, 140ms for paragraphMilliseconds per keystroke, or a preset — slow 850, normal 400, fast 160, rapid 90. The default depends on which variant is in play.
loopDelaynumber2400How long the finished text is held before it clears and types again.
autoPlaybooleantrueRuns the typing sequence. Turned off, the text stays as given and only what someone types changes it.
interactivebooleantrueDragging, spinning and rolling the fragments. Turned off they still float and follow the pointer, but cannot be moved.
captureKeyboardbooleantrueWhether typing anywhere on the page drives the keycaps. Text fields and editable regions are always left alone; only characters, space and backspace are picked up.
layoutKeyboardFragmentLayout[]built-in arrangementStarting positions and rotations, by fragment id. Only the ids you supply are overridden. Pair it with getLayout() to save an arrangement you liked.
backgroundColorstringtheme presetColour behind the scene.
textColorstringtheme presetColour of the headline, subtitle and caret.
fontFamilystring"Plus Jakarta Sans", ui-sans-serif, system-ui, …CSS font stack for the headline. Load the face however your project loads fonts and name it here; the default falls back to the system UI face.
onKeyPress(char: string) => void—Fires on every keystroke, typed by the sequencer or by a person. Useful for driving sound.
onTextChange(newText: string) => void—Fires when someone editing by hand changes the string. Only with autoPlay off — that is the mode where typing edits the text rather than scrubbing through it.
onLayoutChange(layout: KeyboardFragmentLayout[]) => void—Fires when a fragment finishes being dragged, spun or rolled.
childrenReact.ReactNode—Overlay content above the headline, in a pointer-transparent layer. Anything clickable needs pointer-events-auto of its own.
disableAdaptiveQualitybooleanfalseRenders at full pixel density regardless of the device probe. Left off, weaker hardware drops density and turns shadows off.
classNamestring""Applied to the wrapper.
styleReact.CSSProperties—Inline styles merged onto the wrapper, after backgroundColor and fontFamily.
mount"auto" | "eager""auto"When to start typing. The default waits until the page has frame headroom to spare. Set "eager" when the typing is the headline and the reader should see it begin rather than arrive part-written. Note this defaults the opposite way from the components that build a 3D scene: those start eagerly and take "auto" to defer. Both mean the same thing by "auto" — let the runtime pick the moment — the default just matches what each component already did. Neither setting takes it off the shared frame loop or overrides the reduced-motion preference.

<GhostTypewriterRef />

type

The imperative handle, via ref. Also exported as SuspendedKeycapRef. This is how an arrangement made by dragging becomes a layout prop you can commit.

PropTypeDefaultDescription
getLayout*() => KeyboardFragmentLayout[]—Current position and rotation of every fragment, rounded to two decimals so it is fit to paste into source.
resetLayout*() => void—Returns every fragment to where it started.

<KeyboardFragmentLayout />

type

One fragment's placement.

PropTypeDefaultDescription
id*string—Which fragment. One of "frag-uiop-bksp", "frag-azx", "frag-rtyfg", "frag-spacebar-combo", "frag-dhjkl", "frag-qwes".
position*[number, number, number]—Where it sits, as [x, y, z] in scene units.
rotation*[number, number, number]—How it is turned, as [x, y, z] in radians.

<SpeedPreset />

type

A typing speed: "slow" (850ms), "normal" (400ms), "fast" (160ms), "rapid" (90ms), or a number of milliseconds.

<TextVariant />

type

Typography mode: "title", "paragraph" or "auto".

<KeycapTheme />

type

Colour preset: "studio", "obsidian" or "jade".

It reads the visitor's reduced-motion preference and stops everything that moves on its own — the fragments stop drifting and the scene stops leaning toward the pointer. Typing still types and keycaps still press, because that is the content rather than the decoration, and fragments can still be dragged.
Roughly two hundred objects, drawn honestly rather than batched: six circuit-board textures, six bevelled shells and twenty-six keycaps on switch stems, with real shadows from the key light. Keycap geometry and legend textures are cached across every instance on the page, so a second copy is much cheaper than the first. On weaker hardware shadows are dropped and pixel density falls a step; disableAdaptiveQuality turns that off. Resizing moves the camera rather than rebuilding the scene.
Interaction is worth knowing in full: left-drag moves a fragment, right-drag spins it, the wheel rolls it, and a click without a drag presses whichever cap is under the cursor. Movement only begins after about eight pixels, so a click stays a click. The right-button menu is suppressed inside the component.
Client component. It opens a 3D drawing context and draws its textures on a canvas at mount — import it dynamically with ssr: false if your route renders on the server.
It draws on the shared frame loop rather than starting one of its own, so putting several animated components on a page costs one callback, not one each, and the scheduler can set decorative work aside when frames get tight. It also waits for the page to have frame headroom before it starts typing, so it does not compete with whatever else is still loading — see mount if you would rather it began at once. The typed length is the one piece of state it writes during animation, and it writes it once per character rather than once per frame.
Back to all components