makeui
DocsComponents

© 2026 makeui. Open source, MIT licensed.

DocsComponentsGitHub

Text Toolbar

A floating text toolbar with a faceted color orb, style toggles, size, alignment and font.

Preview

Installation

pnpm dlx shadcn@latest add https://makeui.dev/r/text-toolbar.json

Dependencies

  • motion
  • lucide-react

Interaction

Drag on the orb to pick a color, and the text follows it. Double-click the orb to reset. Toggle bold, italic, underline, strikethrough, code, highlight, link, mention and sparkle, hold the plus and minus to change the size, and switch alignment and font. Click away to hide the toolbar and click the text to bring it back.

Props

PropTypeDescription
textstringThe line of text being styled. Defaults to "Make it feel alive."
defaultFormatPartial<TextFormat>Starting style, for example { bold: true, size: 32, hue: 210 }.
fonts{ label; family }[]Fonts in the dropdown. Defaults to system Hand, Sans, Serif and Mono stacks, so pass your own to use a loaded font.
onChange(format: TextFormat) => voidCalled with the full format after every change.
classNamestringExtra classes merged onto the root element.

How to use

import TextToolbar from "@/components/ui/text-toolbar"

export function Example() {
  return (
    <TextToolbar
      text="Make it feel alive."
      defaultFormat={{ size: 28 }}
      onChange={(format) => console.log(format)}
    />
  )
}

Source code

components/ui/text-toolbar.tsx
"use client"

import { memo, useCallback, useEffect, useId, useRef, useState } from "react"
import { AnimatePresence, motion, useReducedMotion, useSpring } from "motion/react"
import {
  AlignCenter,
  AlignLeft,
  AlignRight,
  AtSign,
  Bold,
  ChevronDown,
  Code,
  Highlighter,
  Italic,
  Link2,
  Minus,
  Plus,
  Sparkles,
  Strikethrough,
  Underline,
} from "lucide-react"
import { cn } from "@/lib/utils"

export type TextAlign = "left" | "center" | "right"

export type TextFormat = {
  bold: boolean
  italic: boolean
  underline: boolean
  strike: boolean
  code: boolean
  highlight: boolean
  link: boolean
  mention: boolean
  sparkle: boolean
  size: number
  align: TextAlign
  font: string
  /** 0-359 picked on the orb, or null for the default text color. */
  hue: number | null
}

type Font = { label: string; family: string }

const MONO = 'var(--font-mono, ui-monospace), ui-monospace, "SFMono-Regular", monospace'

// System stacks, so the component has no font dependency. Pass `fonts` to use your own
// (for example a handwriting font loaded with next/font).
const FONTS: Font[] = [
  {
    label: "Hand",
    family: '"Bradley Hand", "Segoe Print", "Chalkboard SE", "Comic Sans MS", cursive',
  },
  { label: "Sans", family: "var(--font-sans, ui-sans-serif), system-ui, sans-serif" },
  { label: "Serif", family: 'ui-serif, Georgia, "Times New Roman", serif' },
  { label: "Mono", family: MONO },
]

const DEFAULTS: TextFormat = {
  bold: false,
  italic: false,
  underline: false,
  strike: false,
  code: false,
  highlight: false,
  link: false,
  mention: false,
  sparkle: false,
  size: 22,
  align: "center",
  font: "Hand",
  hue: null,
}

const MIN = 8
const MAX = 96
const clamp = (n: number) => Math.min(MAX, Math.max(MIN, n))

type ToggleKey =
  | "bold"
  | "italic"
  | "underline"
  | "strike"
  | "code"
  | "highlight"
  | "link"
  | "mention"
  | "sparkle"

const TOGGLES: { key: ToggleKey; label: string; Icon: typeof Bold }[] = [
  { key: "bold", label: "Bold", Icon: Bold },
  { key: "italic", label: "Italic", Icon: Italic },
  { key: "underline", label: "Underline", Icon: Underline },
  { key: "strike", label: "Strikethrough", Icon: Strikethrough },
  { key: "code", label: "Code", Icon: Code },
  { key: "highlight", label: "Highlight", Icon: Highlighter },
  { key: "link", label: "Link", Icon: Link2 },
  { key: "mention", label: "Mention", Icon: AtSign },
  { key: "sparkle", label: "Sparkle", Icon: Sparkles },
]

const ALIGNS: { value: TextAlign; label: string; Icon: typeof AlignLeft }[] = [
  { value: "left", label: "Align left", Icon: AlignLeft },
  { value: "center", label: "Align center", Icon: AlignCenter },
  { value: "right", label: "Align right", Icon: AlignRight },
]

const textColor = (hue: number | null) =>
  hue === null ? undefined : `hsl(${hue} 90% 52%)`

/* ------------------------------------------------------------------ */
/* The orb: a sphere made of hexagonal facets, drawn once as SVG paths */
/* ------------------------------------------------------------------ */

const R = 46 // orb radius inside the 100 x 100 viewBox
const HALF_PI = Math.PI / 2
const HEX = 0.12 // hexagon circumradius, in radians on the sphere
const GAP = 0.88 // < 1 leaves a thin seam between facets

// A point on the sphere, given as (x, y) radians from the centre, lands on the screen
// at sin(distance). That is what squeezes the facets together towards the rim.
function project(x: number, y: number): [number, number] {
  const t = Math.hypot(x, y)
  if (t === 0) return [50, 50]
  const k = Math.sin(Math.min(t, HALF_PI)) / t
  return [50 + x * k * R, 50 + y * k * R]
}

function buildFacets() {
  const out: { d: string; fill: string }[] = []
  const step = Math.sqrt(3) * HEX
  for (let q = -18; q <= 18; q++) {
    for (let r = -18; r <= 18; r++) {
      const cx = step * (q + r / 2)
      const cy = HEX * 1.5 * r
      const t = Math.hypot(cx, cy)
      if (t > HALF_PI - HEX * 0.35) continue

      const d =
        Array.from({ length: 6 }, (_, i) => {
          const a = (Math.PI / 3) * i + Math.PI / 6
          const [px, py] = project(
            cx + Math.cos(a) * HEX * GAP,
            cy + Math.sin(a) * HEX * GAP,
          )
          return `${i ? "L" : "M"}${px.toFixed(2)} ${py.toFixed(2)}`
        }).join("") + "Z"

      // Lit from the top left: brighter where the surface faces the light.
      const s = Math.sin(t)
      const nx = t ? (cx / t) * s : 0
      const ny = t ? (cy / t) * s : 0
      const nz = Math.cos(t)
      const diffuse = Math.max(0, nx * -0.45 + ny * -0.5 + nz * 0.74)
      const hue = (((360 - (Math.atan2(cy, cx) * 180) / Math.PI) % 360) + 360) % 360
      out.push({
        d,
        fill: `hsl(${hue.toFixed(0)} 90% ${(32 + 40 * diffuse ** 0.8).toFixed(0)}%)`,
      })
    }
  }
  return out
}

const FACETS = buildFacets()

// Static, so it never re-renders while the marker moves.
const Facets = memo(function Facets() {
  return (
    <>
      {FACETS.map((f, i) => (
        <path key={i} d={f.d} fill={f.fill} />
      ))}
    </>
  )
})

function ColorOrb({
  hue,
  onPick,
  onReset,
}: {
  hue: number | null
  onPick: (hue: number) => void
  onReset: () => void
}) {
  const tile = useRef<HTMLDivElement>(null)
  const pressed = useRef(false)
  const reduce = useReducedMotion()
  const [radius, setRadius] = useState(0.6)
  const rotX = useSpring(0, { stiffness: 220, damping: 22 })
  const rotY = useSpring(0, { stiffness: 220, damping: 22 })

  const read = (e: React.PointerEvent) => {
    const b = tile.current!.getBoundingClientRect()
    return {
      x: ((e.clientX - b.left) / b.width) * 100 - 50,
      y: ((e.clientY - b.top) / b.height) * 100 - 50,
    }
  }

  const pick = (e: React.PointerEvent) => {
    const { x, y } = read(e)
    const deg = (Math.atan2(y, x) * 180) / Math.PI
    setRadius(Math.min(Math.hypot(x, y) / R, 0.88))
    onPick(Math.round((((360 - deg) % 360) + 360) % 360) % 360)
  }

  const angle = hue === null ? 0 : ((360 - hue) * Math.PI) / 180
  const mx = 50 + Math.cos(angle) * radius * R
  const my = 50 + Math.sin(angle) * radius * R

  return (
    <div
      ref={tile}
      role="slider"
      tabIndex={0}
      aria-label="Text color"
      aria-valuemin={0}
      aria-valuemax={360}
      aria-valuenow={hue ?? 0}
      aria-valuetext={hue === null ? "Default" : `${hue} degrees`}
      title="Drag to pick a color. Double-click to reset."
      onPointerDown={(e) => {
        pressed.current = true
        e.currentTarget.setPointerCapture(e.pointerId)
        pick(e)
      }}
      onPointerMove={(e) => {
        if (pressed.current) pick(e)
        if (reduce) return
        const { x, y } = read(e)
        rotY.set((x / 50) * 10)
        rotX.set((-y / 50) * 10)
      }}
      onPointerUp={() => (pressed.current = false)}
      onPointerCancel={() => (pressed.current = false)}
      onPointerLeave={() => {
        rotX.set(0)
        rotY.set(0)
      }}
      onDoubleClick={onReset}
      onKeyDown={(e) => {
        const step = e.shiftKey ? 30 : 10
        if (e.key === "ArrowRight" || e.key === "ArrowUp") {
          e.preventDefault()
          onPick(((hue ?? 0) + step) % 360)
        } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
          e.preventDefault()
          onPick(((hue ?? 0) - step + 360) % 360)
        } else if (e.key === "Backspace" || e.key === "Delete") {
          e.preventDefault()
          onReset()
        }
      }}
      className="relative aspect-square w-32 shrink-0 touch-none cursor-pointer overflow-hidden rounded-2xl bg-muted outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
      style={{
        backgroundImage:
          "radial-gradient(color-mix(in oklab, var(--foreground) 22%, transparent) 1px, transparent 1.5px)",
        backgroundSize: "8px 8px",
      }}
    >
      <motion.div
        className="absolute inset-0 p-1"
        style={{ rotateX: rotX, rotateY: rotY, transformPerspective: 500 }}
      >
        <svg viewBox="0 0 100 100" className="size-full" aria-hidden>
          <Facets />
          {hue !== null && (
            <circle
              cx={mx}
              cy={my}
              r={4.5}
              fill={textColor(hue)}
              stroke="#fff"
              strokeWidth={2}
              style={{ filter: "drop-shadow(0 1px 2px rgb(0 0 0 / 0.4))" }}
            />
          )}
        </svg>
        {/* glossy highlight and a soft shadow towards the rim */}
        <span
          aria-hidden
          className="pointer-events-none absolute inset-1 rounded-full"
          style={{
            background:
              "radial-gradient(circle at 32% 26%, rgb(255 255 255 / 0.5), transparent 36%), radial-gradient(circle at 50% 50%, transparent 60%, rgb(0 0 0 / 0.28) 100%)",
          }}
        />
      </motion.div>
    </div>
  )
}

/* ------------------------------------------------------------------ */
/* Small controls                                                      */
/* ------------------------------------------------------------------ */

const chip =
  "grid place-items-center rounded-xl text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-40"

function Step({
  label,
  onStep,
  disabled,
  children,
}: {
  label: string
  onStep: () => void
  disabled: boolean
  children: React.ReactNode
}) {
  const wait = useRef<ReturnType<typeof setTimeout>>(undefined)
  const repeat = useRef<ReturnType<typeof setInterval>>(undefined)
  const stop = useCallback(() => {
    clearTimeout(wait.current)
    clearInterval(repeat.current)
  }, [])
  useEffect(() => stop, [stop])

  return (
    <button
      type="button"
      aria-label={label}
      disabled={disabled}
      // Hold to keep stepping. Keyboard users get one step per key press.
      onPointerDown={() => {
        onStep()
        wait.current = setTimeout(() => (repeat.current = setInterval(onStep, 60)), 350)
      }}
      onPointerUp={stop}
      onPointerLeave={stop}
      onPointerCancel={stop}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault()
          onStep()
        }
      }}
      className={cn(chip, "size-9")}
    >
      {children}
    </button>
  )
}

function FontSelect({
  value,
  fonts,
  onPick,
}: {
  value: string
  fonts: Font[]
  onPick: (label: string) => void
}) {
  const [open, setOpen] = useState(false)
  const box = useRef<HTMLDivElement>(null)
  const current = fonts.find((f) => f.label === value) ?? fonts[0]

  useEffect(() => {
    if (!open) return
    // Capture phase, because the toolbar stops pointer events from bubbling.
    const away = (e: PointerEvent) => {
      if (!box.current?.contains(e.target as Node)) setOpen(false)
    }
    const esc = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        e.stopPropagation()
        setOpen(false)
      }
    }
    document.addEventListener("pointerdown", away, true)
    document.addEventListener("keydown", esc, true)
    return () => {
      document.removeEventListener("pointerdown", away, true)
      document.removeEventListener("keydown", esc, true)
    }
  }, [open])

  return (
    <div ref={box} className="relative min-w-0 flex-1">
      <button
        type="button"
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-label={`Font: ${current.label}`}
        onClick={() => setOpen((o) => !o)}
        className="flex h-11 w-full items-center justify-center gap-1.5 rounded-2xl bg-muted px-3 text-sm font-medium"
      >
        <span className="truncate" style={{ fontFamily: current.family }}>
          {current.label}
        </span>
        <ChevronDown
          className={cn(
            "size-3.5 shrink-0 text-muted-foreground transition-transform",
            open && "rotate-180",
          )}
        />
      </button>
      <AnimatePresence>
        {open && (
          <motion.ul
            role="listbox"
            aria-label="Font"
            initial={{ opacity: 0, y: 6, scale: 0.97 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={{ opacity: 0, y: 4, scale: 0.98 }}
            transition={{ duration: 0.14 }}
            className="absolute bottom-full right-0 z-10 mb-2 w-full min-w-32 rounded-2xl border border-border bg-card p-1 shadow-lg"
          >
            {fonts.map((f) => (
              <li key={f.label} role="presentation">
                <button
                  type="button"
                  role="option"
                  aria-selected={f.label === current.label}
                  onClick={() => {
                    onPick(f.label)
                    setOpen(false)
                  }}
                  style={{ fontFamily: f.family }}
                  className={cn(
                    "w-full rounded-xl px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted",
                    f.label === current.label && "bg-muted font-medium",
                  )}
                >
                  {f.label}
                </button>
              </li>
            ))}
          </motion.ul>
        )}
      </AnimatePresence>
    </div>
  )
}

/* ------------------------------------------------------------------ */
/* The toolbar                                                         */
/* ------------------------------------------------------------------ */

function Toolbar({
  fmt,
  fonts,
  set,
  step,
}: {
  fmt: TextFormat
  fonts: Font[]
  set: (patch: Partial<TextFormat>) => void
  step: (delta: number) => void
}) {
  const id = useId()

  return (
    <motion.div
      role="toolbar"
      aria-label="Text formatting"
      initial={{ opacity: 0, y: 12, scale: 0.96 }}
      animate={{ opacity: 1, y: 0, scale: 1 }}
      exit={{ opacity: 0, y: 8, scale: 0.97 }}
      transition={{ type: "spring", stiffness: 380, damping: 30 }}
      onPointerDown={(e) => e.stopPropagation()} // keep the text selected while using the toolbar
      className="w-[22rem] max-w-full space-y-2 rounded-3xl border border-border bg-card p-2 shadow-xl"
    >
      <div className="flex gap-2">
        <ColorOrb
          hue={fmt.hue}
          onPick={(hue) => set({ hue })}
          onReset={() => set({ hue: null })}
        />
        <div className="grid min-w-0 flex-1 grid-cols-3 grid-rows-3 gap-0.5 rounded-2xl bg-muted p-1">
          {TOGGLES.map(({ key, label, Icon }) => (
            <motion.button
              key={key}
              type="button"
              aria-pressed={fmt[key]}
              aria-label={label}
              title={label}
              whileTap={{ scale: 0.9 }}
              onClick={() => set({ [key]: !fmt[key] })}
              className={cn(
                chip,
                "aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:shadow-sm",
              )}
            >
              <Icon className="size-4" />
            </motion.button>
          ))}
        </div>
      </div>

      <div className="flex gap-2">
        <div className="flex h-11 items-center rounded-2xl bg-muted px-0.5">
          <Step label="Decrease size" disabled={fmt.size <= MIN} onStep={() => step(-1)}>
            <Minus className="size-3.5" />
          </Step>
          <span
            aria-live="polite"
            className="w-7 text-center text-sm font-medium tabular-nums"
          >
            {fmt.size}
          </span>
          <Step label="Increase size" disabled={fmt.size >= MAX} onStep={() => step(1)}>
            <Plus className="size-3.5" />
          </Step>
        </div>

        <div
          role="radiogroup"
          aria-label="Alignment"
          className="flex h-11 items-center rounded-2xl bg-muted p-1"
        >
          {ALIGNS.map(({ value, label, Icon }) => {
            const on = fmt.align === value
            return (
              <button
                key={value}
                type="button"
                role="radio"
                aria-checked={on}
                aria-label={label}
                title={label}
                onClick={() => set({ align: value })}
                className={cn(
                  "relative grid size-9 place-items-center rounded-xl transition-colors",
                  on ? "text-foreground" : "text-muted-foreground hover:text-foreground",
                )}
              >
                {on && (
                  <motion.span
                    layoutId={`${id}-align`}
                    transition={{ type: "spring", stiffness: 500, damping: 36 }}
                    className="absolute inset-0 rounded-xl bg-background shadow-sm"
                  />
                )}
                <Icon className="relative size-4" />
              </button>
            )
          })}
        </div>

        <FontSelect value={fmt.font} fonts={fonts} onPick={(font) => set({ font })} />
      </div>
    </motion.div>
  )
}

/* ------------------------------------------------------------------ */
/* The component                                                       */
/* ------------------------------------------------------------------ */

const SPARKS = [
  { left: "-3%", top: "-34%", size: "size-3.5" },
  { left: "97%", top: "-22%", size: "size-4" },
  { left: "90%", top: "88%", size: "size-3" },
  { left: "3%", top: "92%", size: "size-3" },
]

export default function TextToolbar({
  children,
  text = "Make it feel alive.",
  defaultFormat,
  fonts = FONTS,
  onChange,
  className,
}: {
  /** The text to style: <TextToolbar>Hello</TextToolbar> */
  children?: React.ReactNode
  /** Same thing as a prop. Used only when there are no children. */
  text?: string
  defaultFormat?: Partial<TextFormat>
  fonts?: Font[]
  onChange?: (format: TextFormat) => void
  className?: string
}) {
  const reduce = useReducedMotion()
  const [fmt, setFmt] = useState<TextFormat>(() => ({
    ...DEFAULTS,
    font: fonts[0]?.label ?? DEFAULTS.font,
    ...defaultFormat,
  }))
  const [selected, setSelected] = useState(true)

  // The ref is the source of truth, so press-and-hold stepping never reads a stale size.
  const latest = useRef(fmt)
  const onChangeRef = useRef(onChange)
  useEffect(() => {
    onChangeRef.current = onChange
  })

  const set = useCallback((patch: Partial<TextFormat>) => {
    latest.current = { ...latest.current, ...patch }
    setFmt(latest.current)
    onChangeRef.current?.(latest.current)
  }, [])
  const step = useCallback(
    (delta: number) => set({ size: clamp(latest.current.size + delta) }),
    [set],
  )

  const family = fmt.code
    ? MONO
    : (fonts.find((f) => f.label === fmt.font) ?? fonts[0] ?? FONTS[0]).family
  const decoration = [
    fmt.underline || fmt.link ? "underline" : "",
    fmt.strike ? "line-through" : "",
  ]
    .filter(Boolean)
    .join(" ")

  return (
    <div
      className={cn("relative flex w-full flex-col items-center", className)}
      onPointerDown={() => setSelected(false)}
      onKeyDown={(e) => e.key === "Escape" && setSelected(false)}
    >
      {/* Reserved height, so the text doesn't jump when the toolbar hides. */}
      <div className="flex h-[12.75rem] w-full items-end justify-center pb-3">
        <AnimatePresence>
          {selected && (
            <Toolbar key="toolbar" fmt={fmt} fonts={fonts} set={set} step={step} />
          )}
        </AnimatePresence>
      </div>

      <div className="w-full max-w-md px-4" style={{ textAlign: fmt.align }}>
        <span
          role="button"
          tabIndex={0}
          aria-pressed={selected}
          aria-label="Selected text. Press to show the toolbar."
          onPointerDown={(e) => {
            e.stopPropagation()
            setSelected(true)
          }}
          onKeyDown={(e) => {
            if (e.key === "Enter" || e.key === " ") {
              e.preventDefault()
              setSelected(true)
            }
          }}
          className="relative inline-block max-w-full cursor-default rounded-sm px-2.5 py-1 outline-none"
        >
          <AnimatePresence>
            {selected && (
              <>
                <motion.span
                  key="selection"
                  aria-hidden
                  className="absolute inset-0 rounded-sm bg-foreground/10"
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                />
                <motion.span
                  key="handle-start"
                  aria-hidden
                  className="pointer-events-none absolute -bottom-1 -left-0.5 -top-1 w-0.5 rounded-full bg-foreground"
                  initial={{ opacity: 0, scaleY: 0.5 }}
                  animate={{ opacity: 1, scaleY: 1 }}
                  exit={{ opacity: 0 }}
                >
                  <span className="absolute -left-[3px] -top-1.5 size-2 rounded-full bg-foreground" />
                </motion.span>
                <motion.span
                  key="handle-end"
                  aria-hidden
                  className="pointer-events-none absolute -bottom-1 -right-0.5 -top-1 w-0.5 rounded-full bg-foreground"
                  initial={{ opacity: 0, scaleY: 0.5 }}
                  animate={{ opacity: 1, scaleY: 1 }}
                  exit={{ opacity: 0 }}
                >
                  <span className="absolute -bottom-1.5 -right-[3px] size-2 rounded-full bg-foreground" />
                </motion.span>
              </>
            )}
          </AnimatePresence>

          <span
            className={cn(
              "relative whitespace-pre-wrap break-words transition-[font-size] duration-200",
              fmt.code && "rounded-md bg-muted px-1.5 py-0.5",
              fmt.mention && "rounded-md bg-sky-500/15 px-1.5",
              fmt.highlight && "rounded-sm bg-yellow-300/60 px-0.5 dark:bg-yellow-400/40",
            )}
            style={{
              fontFamily: family,
              fontSize: fmt.size,
              fontWeight: fmt.bold ? 700 : 400,
              fontStyle: fmt.italic ? "italic" : "normal",
              textDecoration: decoration || "none",
              textDecorationColor: fmt.link ? "#3b82f6" : undefined,
              textDecorationThickness: fmt.link ? 2 : undefined,
              textUnderlineOffset: 4,
              color: textColor(fmt.hue),
              lineHeight: 1.3,
            }}
          >
            {fmt.mention && "@"}
            {children ?? text}
          </span>

          {fmt.sparkle &&
            SPARKS.map((s, i) => (
              <motion.span
                key={i}
                aria-hidden
                className="pointer-events-none absolute text-amber-400"
                style={{ left: s.left, top: s.top }}
                animate={
                  reduce
                    ? undefined
                    : {
                        scale: [0.6, 1.15, 0.6],
                        opacity: [0.35, 1, 0.35],
                        rotate: [0, 18, 0],
                      }
                }
                transition={{
                  duration: 1.8,
                  repeat: Infinity,
                  delay: i * 0.4,
                  ease: "easeInOut",
                }}
              >
                <Sparkles className={s.size} />
              </motion.span>
            ))}
        </span>
      </div>

      <p className="mt-5 text-xs text-muted-foreground" aria-live="polite">
        {selected
          ? "Drag on the orb to pick a color."
          : "Click the text to bring the toolbar back."}
      </p>
    </div>
  )
}

On this page

The first thing we ordered was business cards, before the product existed and before the name was final. I kept one in my wallet for years. The titles changed, the logo changed twice, and the story people told about those first months changed more than either of them. Looking back, the cards were the only part that never pretended to be anything else. The first thing we ordered was business cards, before the product existed and before the name was final. I kept one in my wallet for years. The titles changed, the logo changed twice, and the story people told about those first months changed more than either of them. Looking back, the cards were the only part that never pretended to be anything else.
22
Make it feel alive.

Drag on the orb to pick a color.