A design editor for creating professional social media graphics — LinkedIn posts, quote cards, announcements, and more. Part of the OpenClaw ecosystem. Zero cloud dependencies — runs locally with SQLite.
Built with Preact + Fabric.js + Tailwind CSS + Hono + SQLite. Ships with a Figma-inspired dark editor UI, retina canvas rendering, and pre-built LinkedIn post templates.
OpenDesign is a production-ready graphic design editor designed for the OpenClaw community. Think of it as an open-source Canva alternative — a visual design tool you can self-host, customize, and embed in any SaaS product.
Unlike Canva or Adobe Express, this runs entirely on your own infrastructure. No subscriptions, no watermarks, no vendor lock-in. Create pixel-perfect social media graphics with professional typography and export at 2x resolution.
- Template fields — name any text or image object, then fill it from data or an agent over the API; one name can drive several objects and pages at once
- Fabric.js canvas — full object manipulation with retina/HiDPI rendering (2x device pixel ratio)
- Pre-built templates — LinkedIn-optimized: Quote Card, Stats Highlight, Announcement, Tips List, Profile Card, Minimal Text
- 10 Google Fonts — Inter, Playfair Display, Montserrat, Poppins, Roboto, Open Sans, Lora, Raleway, Source Sans Pro, Merriweather
- Custom fonts — click Import font from computer… in a text or brand-kit font selector to upload TTF, OTF, WOFF or WOFF2 files (up to 10 MB). Fonts are saved with the app, load before the canvas renders, and travel with exported brand kits.
- Text editing — font family, size, weight, alignment, color, line height, letter spacing
- Shapes — rectangles, circles, triangles, lines with fill, stroke, border radius
- Image uploads — drag-and-drop or click to upload, place on canvas
- Brand kits — save a palette, a heading/body font pair and your logos, then apply them to any design in one click. Brand swatches appear wherever you pick a color. Kits export and import as plain JSON, so a kit moves between installs or over to a client instead of being stranded in one account
- Backgrounds — solid colors, gradients, uploaded images
- Canvas sizes — LinkedIn Square (1080x1080), LinkedIn Carousel (1080x1350), LinkedIn Landscape (1200x627), LinkedIn Portrait (1200x1500), Instagram Story (1080x1920)
- Undo/Redo — full history with keyboard shortcuts (Cmd+Z / Cmd+Shift+Z)
- Carousel PDF export: every page in one multi-page PDF, the only format LinkedIn accepts for a swipeable carousel
- 2x PNG export: this page or every page, at exactly 2x the design size on any display
- Auto-save — designs persist to SQLite with debounced saves
- Dual-mode UI — human-optimized + AI-agent-optimized (
?agent=true)
A swipeable carousel on LinkedIn is a document post. LinkedIn's own help pages document the format (PDF, DOC/DOCX or PPT/PPTX, up to 300 pages and 100 MB), and the short-lived native multi-image carousel it shipped in 2022 was removed in June 2023, with existing carousel posts deleted that December. Uploading a document is the way.
OpenDesign builds one end to end: add a page per slide, then Export → Carousel PDF writes them all into a single file, in canvas order, at the slide's own aspect ratio. Upload that PDF to LinkedIn as a document.
Format and limits per LinkedIn Help: upload and share documents. Note that Carousel Ads is a separate paid ads product, unrelated to this.
Use the LinkedIn Carousel canvas size (1080x1350, 4:5). It takes up the most room on a phone screen. Exports are assembled in the browser, so nothing leaves the machine.
git clone https://github.com/clawnify/OpenDesign.git
cd OpenDesign
pnpm install
pnpm run devOpen http://localhost:5173 in your browser. Designs and uploads persist in the local D1 and R2 simulators under .wrangler/.
Append ?agent=true to the URL:
http://localhost:5178/?agent=true
This activates an agent-friendly UI with:
- Explicit buttons always visible (no hover-to-reveal)
- Large click targets for reliable browser automation
- All controls accessible without drag interactions
- Semantic labels for AI navigation
Claude Code can interact with the design editor through the REST API:
# Create a new design
curl -X POST http://localhost:3006/api/designs \
-H "Content-Type: application/json" \
-d '{"name": "Q1 Results", "width": 1080, "height": 1080}'
# Load a template
curl http://localhost:3006/api/templates/1
# Update design with canvas JSON
curl -X PUT http://localhost:3006/api/designs/1 \
-H "Content-Type: application/json" \
-d '{"canvas_json": "{...}"}'OpenClaw agents can also use the browser tool to visually interact with the editor — navigate, click templates, edit text, and export PNGs.
| Layer | Technology |
|---|---|
| Frontend | Preact, TypeScript, Tailwind CSS v4, Vite |
| Canvas | Fabric.js v6 (retina rendering, object manipulation) |
| Backend | Hono, Node.js |
| Database | SQLite (better-sqlite3) |
| Fonts | Google Fonts (WebFontLoader) |
| Icons | Lucide |
- Node.js 20+
- pnpm (or npm/yarn)
schema.sql — SQLite schema (designs, templates, pages)
demo/seed.sql — Sample designs for the Clawnify demo workspace
src/
server/
db.ts — SQLite wrapper (query, get, run, transaction)
index.ts — Hono REST API (designs CRUD, templates, fields, uploads)
fields.ts — Template fields: read the fill schema, substitute values
uploads.ts — Image uploads in the app's R2 bucket
seed-templates.ts — Starter templates, inserted on first request
client/
app.tsx — Root component
fonts.ts — Bundled canvas fonts
context.tsx — Editor context + canvas size presets
hooks/
use-canvas.ts — Fabric.js state, undo/redo, zoom, export
use-designs.ts — Designs CRUD + auto-save + template loading
components/
editor.tsx — Main layout (toolbar + sidebars + canvas)
home.tsx — Home screen: templates and recent designs
canvas-area.tsx — Scrollable area holding every page's canvas
page-canvas.tsx — One page's Fabric.js canvas
pages-bar.tsx — Add, rename, duplicate and delete pages
toolbar.tsx — Size picker, undo/redo, zoom, export, save
left-sidebar.tsx — Templates, text, shapes, images, backgrounds
right-sidebar.tsx — Properties panel (context-aware per selection)
template-card.tsx — Template thumbnail in gallery
design-list.tsx — Saved designs list with rename/delete
designs (id, name, canvas_json, width, height, thumbnail_url, created_at, updated_at)
templates (id, name, category, canvas_json, width, height, thumbnail_url, sort_order)| Method | Endpoint | Description |
|---|---|---|
| GET | /api/designs |
List all designs |
| POST | /api/designs |
Create a design |
| GET | /api/designs/:id |
Get a design |
| PUT | /api/designs/:id |
Update a design |
| DELETE | /api/designs/:id |
Delete a design |
| GET | /api/designs/:id/fields |
List the design's fillable fields |
| POST | /api/designs/:id/fill |
Fill those fields with values |
| GET | /api/templates |
List all templates |
| GET | /api/templates/:id |
Get a template |
| POST | /api/uploads |
Upload an image file |
| GET | /api/uploads/:filename |
Serve an uploaded image or font |
| GET | /api/fonts |
List imported fonts |
| POST | /api/fonts |
Import a font file (multipart file, up to 10 MB) |
Design something once, then produce as many variants of it as you have rows of data — without anything on the outside having to understand canvas JSON.
Select a text or image object in the editor and give it a Field name in the properties panel. That object is now a fill slot:
curl localhost:8787/api/designs/$ID/fields[
{ "name": "headline", "type": "text", "value": "Your inspiring quote goes here", "page_ids": ["p1"] },
{ "name": "logo", "type": "image", "value": "https://.../old.png", "page_ids": ["p1", "p2"] }
]Fill it. Text fields take a string (numbers are accepted and stringified), image fields take a URL:
curl -X POST localhost:8787/api/designs/$ID/fill \
-H 'content-type: application/json' \
-d '{"values": {"headline": "Q3 revenue up 40%", "logo": "https://.../new.png"}}'The response carries the filled pages plus filled and unmatched, so a name
that matches nothing is reported rather than failing the request. The stored
design is left alone — add "save": true (and optionally "name") to persist
the result as a new design instead.
save: true writes a new editable design, not a rendered image, because
there is no server-side rasterizer. That makes it the right tool for a handful
of variants — an agent fills a card, opens it, exports it — and the wrong one
for a spreadsheet:
- Each saved variant is a row in
designs, andGET /api/designsreturns every row with its fullcanvas_jsonand no pagination. A three-object card is ~2.5 KB serialised; a branded design with an image is 10-40 KB. A few hundred variants turn the gallery response into megabytes. - So a few hundred saved variants give you a few hundred gallery entries, not a few hundred finished graphics.
For real bulk output, drive fill without save and rasterize in the browser —
the canvas is already there, and that loop is the missing piece rather than a
server-side renderer. Until it exists, treat save: true as a small-N
convenience.
Worth knowing:
- A field name may repeat. One
logoacross five pages fills all five, which is what you want for a carousel. - What gets written depends on the object, not the declared type: text objects
take
text, image objects takesrc. - A replacement image keeps the original object's box, so a different aspect ratio will be stretched. Size the slot for the images you intend to feed it.
- Only text and image objects can be fields. Naming a shape does nothing.
- Rendering to PNG still happens in the browser — there is no server-side rasterizer, because Workers have no canvas.
This project is part of the OpenClaw ecosystem. Contributions are welcome — open an issue or submit a PR.
MIT
