A web configurator for Framework laptops. Pick a model, a back-panel finish, and a color for every expansion card slot — then see exactly what the combo will look like before you order.
Live: https://designframe.work
Built because I was ordering a Framework and couldn't commit to an expansion card colorway. Now I still can't, but at least I can see my indecision rendered in full color.
- Four models – Framework Laptop 12, 13, 13 Pro, and 16.
- Per-slot expansion card selection – pick a different finish for each expansion slot, or bulk-select one color for every slot.
- Back-panel finishes – each model ships with its real back-panel lineup (Silver, Black, Graphite, Sage, Lavender, Bubblegum, etc.).
- Live preview – cards render as SVG overlays on the actual product photo, with per-slot rotation and scale, so what you see is what you get.
- Randomize – one click shuffles the whole config, never repeats a color across slots, and never matches a card to the chassis color (so nothing disappears).
- Theme toggle – System / Light / Dark, persisted via
next-themes. - Fully prerendered – every route is static HTML, served from Cloudflare Pages.
- TanStack Start (Vite + React 19) with file-based routing via TanStack Router.
- HeroUI v3 components (built on React Aria Components), wired up to the router via
RouterProviderso every HeroUILink/Menuitem routes client-side. - Tailwind CSS v4 with HeroUI's design tokens.
next-themesfor theming.- Bun as the package manager.
- Cloudflare Pages for hosting (static prerender).
bun install
bun devDev server runs at http://localhost:3000.
bun run buildOutput lands in:
dist/client/– prerendered HTML + hashed client assets (this is what Cloudflare Pages serves).dist/server/– the SSR bundle (not deployed, just used during prerender).
bun run testsrc/
routes/ # TanStack Router file-based routes
__root.tsx # RouterProvider + ThemeProvider wrap
index.tsx # Home page (laptop picker)
$laptop.tsx # Per-laptop configurator page
components/
app-header.tsx # Top nav + theme toggle
theme-toggle.tsx # System/Light/Dark dropdown
framework/
product-viewer.tsx # SVG compositor: laptop photo + expansion cards
configuration-panel.tsx # Back + per-slot color pickers, Randomize
option-card.tsx
section-header.tsx
data/
laptops.ts # Laptop type + getLaptopById
laptop-models.ts # LAPTOP_MODELS list used by nav + home
laptops/
laptop-12.json # Per-model: image crop, card slots, backs
laptop-13.json
laptop-13-pro.json
laptop-16.json
expansion-cards.ts # ExpansionCard type + catalogue
expansion-cards.json # Expansion card catalogue (colors + photos)
public/
images/
laptop-12/backs/*.png # Back-panel photos per finish
laptop-13/backs/*.png
laptop-13-pro/*.png
laptop-16/backs/*.png
expansion-cards/*.png # Card finish photos
Everything is data-driven. No component changes needed for normal content updates.
- Drop a
<color>.pngintopublic/images/expansion-cards/. Target dimensions are2112 × 2580with the card roughly centered (see existing cards for framing). - Append an entry to
src/data/expansion-cards.json: - It'll automatically show up for every laptop, since the catalogue is shared.
Edit that laptop's JSON (src/data/laptops/laptop-13.json, etc.) and append to backs:
{
"id": "graphite",
"label": "Graphite",
"shell": "#2a2a2c", // fallback swatch if photo fails to load
"accent": "#1a1a1c",
"defaultExpansionCardId": "plastic-graphite",
"image": {
"src": "/images/laptop-13/backs/graphite.png",
"width": 1290,
"height": 1290
},
"view": { "x": 0, "y": 153, "width": 1290, "height": 968 }
}view is the sub-rectangle (in image pixels) that tightly frames the laptop body — it drives the SVG viewBox. If the photo already has the laptop edge-to-edge, you can widen the view past the image bounds (negative x / y) to add virtual padding so it renders at a similar size to the other models.
- Create
src/data/laptops/laptop-new.jsonfollowing the same shape aslaptop-13.json(see the JSDoc on the types insrc/data/laptops.ts—expansionCardSize,expansionCardSlotswithposition+scale, andbacks). - Register it in
src/data/laptops.ts:import laptopNew from "./laptops/laptop-new.json"; // ... export type LaptopId = "laptop-12" | "laptop-13" | "laptop-13-pro" | "laptop-16" | "laptop-new"; export const LAPTOPS = [laptop12, laptop13, laptop13Pro, laptop16, laptopNew] as const;
- Add it to
LAPTOP_MODELSinsrc/data/laptop-models.tsso the nav + home grid pick it up.
The $laptop.tsx route handles any id that resolves from getLaptopById, so no routing changes are needed.
Deployed as a static site on Cloudflare Pages.
- Build command:
bun run build - Build output directory:
dist/client
That's the only setting that matters — the prerender produces real HTML for /, /laptop-12, /laptop-13, /laptop-13-pro, /laptop-16, so no Pages Function is needed.
Issues and PRs welcome — especially:
- New expansion card finishes (Framework drops colorways faster than I can ship them).
- Better back-panel photos (some finishes are awkwardly cropped).
- Slot-position refinements on any model where the cards don't quite line up with the chassis cutouts.
Product photos are Framework's own, used for preview purposes. All trademarks belong to their respective owners. This project is not affiliated with or endorsed by Framework Computer Inc.
Built by @daveycodez.
MIT. See LICENSE.
{ "id": "plastic-newcolor", "label": "New Color", "color": "#hexswatch", "outline": "#hexswatch", "image": { "src": "/images/expansion-cards/newcolor.png", "width": 2112, "height": 2580 }, "imageCrop": { "x": 580, "y": 802, "width": 948, "height": 1008 } }