Skip to content
ilinxa/pro-ui

Story Composer

alphav0.4.0

Instagram-style story creation surface — a locked 9:16 wrapper around the media editor.

Category: MediaUpdated: 2026-08-11Created: 2026-05-31Author: ilinxa

Context

Third and final component in the story-system trilogy alongside story-rail (discovery) and story-viewer (consumption). v0.2.0 delegates the capture + edit surface (camera, multi-layer Konva editor, all six tools, discard guard, history) to @ilinxa/media-editor (v0.1.1+), then layers the story-specific publish pipeline on top: ComposerPublishBar in the renderTopBar slot, PublishingProgressOverlay during upload, and a 14-method handle whose publish/exportBlob methods bridge ExportMetadata → PublishMetadata → PublishedStory. Camera-first defaults: rear camera on mobile, front on desktop, microphone for video. Public API 100% preserved across the v0.1.5 → v0.2.0 boundary; the 73-name export snapshot at docs/procomps/media-editor-procomp/story-composer-v0.1.5-exports.snapshot.txt resolves through the v0.2.0 barrel without omission.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/story-composer

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/story-composer-fixtures

Preview

All three modes (photo / video / text), 36 built-in emoji stickers. Wired to a fake uploader for the docs site — consumers pass `uploadUrl` or a real `uploader` for prod.

Demo source

demo.tsxtsx

Usage

When to use

StoryComposer is the creation surface for Instagram-style stories. It composes with StoryRail (discovery) and StoryViewer(consumption) to form the full story system, but doesn't depend on them. Use it whenever you need camera-first capture + on-canvas editing + one-tap publish.

Quick start

import { useState } from "react"
import { StoryComposer } from "@/components/story-composer"

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button onClick={() => setOpen(true)}>Create story</button>
      <StoryComposer
        isOpen={open}
        onClose={() => setOpen(false)}
        uploadUrl="/api/stories/upload"
        onPublished={(story) => {
          // story.items[0].src = the uploaded media URL
          console.log("uploaded:", story)
          setOpen(false)
        }}
      />
    </>
  )
}

Capture modes

  • Photo — camera tap → instant editor with the default toolbar (Text / Draw / Stickers / Filters / Adjust). Crop is opt-in via enabledTools={[…, "crop"]}— stories are 9:16-locked, so default flows don't need it.
  • Video — long-press hold OR tap-to-toggle shutter. Auto-stop at maxVideoDuration (default 30s). Edit stage shows a two-handle trim bar; overlays bake into the final video on publish.
  • Text — 8 gradient backgrounds + centered text + font + color picker. Renders to PNG on publish.

Hide modes you don't want via hideModes={["video","text"]}.

Publish

Two paths — pick one:

  • uploadUrl="/api/upload" — composer POSTs FormData ({ file, metadata }) with progress events.
  • uploader={async (blob, meta) => …} — custom uploader for signed-URL flows (S3 PUT, Cloudinary, Mux). Must return { url, thumbnailUrl? }.

Pan + zoom

Drag (single pointer — mouse or 1 finger) to pan; a drag that starts on a text/sticker overlay moves that overlay instead, and a tap never pans. Zoom 1× → 4× via 2-finger pinch (touch) or mouse wheel anchored to the cursor (desktop — native non-passive, beats the browser's Ctrl+wheel page-zoom). Arrow keys pan in the arrow direction; + / - / 0 zoom in / out / reset. Disabled while drawing or cropping.

Imperative handle

Pass a ref to drive the composer programmatically: takePhoto, switchCamera, startRecording, addText, addSticker, applyFilter, publish, exportBlob, and more (14 methods total).

More

Full reference + slot extension points + accessibility notes live in docs/procomps/story-composer-procomp/story-composer-procomp-guide.md.

Features

  • Camera capture (photo + video + text-only modes) via getUserMedia + MediaRecorder
  • Gallery picker fallback when camera is denied or unavailable
  • Multi-layer Konva editor: image / drawing / stickers / text / UI
  • Six edit tools — Text, Draw (vector + eraser), Stickers (36 built-in emoji + extensible), Filters (10 Instagram-style presets with pre-rendered thumbs), Adjust (brightness/contrast/saturation/blur), and opt-in Crop (9:16 / 1:1 / 4:5). Default `enabledTools` ships the first five; consumers add `"crop"` when their flow isn't 9:16-locked.
  • Pan + zoom on the editor canvas (1×–4×): single-pointer drag-to-pan (mouse or 1-finger; yields to draggable text/sticker overlays), 2-finger touch pinch, mouse wheel anchored to cursor (native non-passive — beats browser page-zoom), keyboard arrows pan in image direction, +/-/0 zoom/reset
  • Video record: long-press hold OR tap-to-toggle shutter; auto-stop at maxVideoDuration; two-handle trim bar; overlays bake into final video via canvas.captureStream + MediaRecorder pipeline
  • Built-in upload via XHR POST FormData with progress, plus `uploader` escape hatch for signed-URL flows (S3 / Cloudinary / Mux)
  • Responsive: mobile-fullscreen / desktop-modal sized to a viewport-relative 9:16 with a min/max clamp (can't collapse or overflow) and safe-area awareness; top bar follows the IG chrome model (X swaps to a back arrow in the edit stage; Publish hidden during photo/video capture)
  • Undo/redo on every overlay + stroke command (Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z; 50-deep stack)
  • Permission-denied auto-retry via navigator.permissions.query
  • Discard-confirm guard for unsaved edits (opt-out via confirmOnDiscard: false)
  • Live-region announcer for screen readers (Konva canvas is opaque to SR)
  • Five exported sealed-folder parts + three exported hooks for advanced compositions
  • 14-method imperative handle (open/close/reset, switchCamera/takePhoto/startRecording/stopRecording/importFromGallery, addText/addSticker/setAdjustments/applyFilter, publish/exportBlob)
  • v0.2.0 architecture: thin wrapper around @ilinxa/media-editor — capture + edit surface delegated; story-shaped publish flow + ComposerPublishBar layered on top
  • v0.4.0 — P3 feature-slicing lockstep: camera capture now flows through media-editor's `capture={mediaCapture}` extension (`@ilinxa/media-editor-capture`) instead of a static import; the v0.1.5 backward-compat re-exports scheduled for v0.3.0 removal (ComposerCamera/ComposerEditor/ComposerToolbar/ColorSwatchPicker + their Props types, and useMediaCapture/validateGalleryFile/suggestedVideoFilename/CapturedPhoto/CapturedVideo/CaptureStatus/FacingMode + their option types) are removed — breaking for any consumer still on the v0.1.5 names.

Tags

story-composerstoriescameraimage-editorvideokonvasocialpublish

Dependencies

shadcn primitives: button
npm peer deps: lucide-react@^1.11.0
internal: media-editor