Design a business card in the browser and walk away with files a print shop will actually accept.
Most free card makers hand you a picture of a card. Enlarge it and it goes soft; send it to a printer and they ask for something else. This one draws the card as a true vector, so the same design exports as a crisp image for your email signature, a print-ready PDF for a professional printer, and a tiled sheet you can run off on an office printer and cut up the same afternoon.
Everything happens inside the browser. There is no account to create, no upload, and no server holding anyone's contact details.
| Two finished designs | Studio, a modern asymmetric layout, and Mono, a centred classical one. Each ships a light and a dark colourway. |
| Your details, your styling | Name, role, company, contact, tagline and tags — then control the colour of every element and the typeface, size, weight and slant of every line. |
| Your brand | Upload a logo (SVG, PNG or JPEG). Pick from three bundled typefaces or a curated Google Fonts catalogue. |
| A working QR code | Point it at a website, or generate a contact card that adds you straight to someone's phone. |
| Four ways out | High-resolution PNG, print-ready PDF, editable SVG, and a multi-up sheet with cut guides. |
| Nothing to lose | Your card saves in your browser as you type. You can also save it to a file and reopen it later, or hand it to a colleague. |
Anyone who needs a card and does not have a designer: founders, freelancers, consultants, small teams. It is deliberately not a free-form design tool — you pick a finished layout and fill it in, so the result looks considered without requiring taste or time.
This is the one choice everything else follows from, and it is worth understanding before changing anything.
The card is drawn as SVG — a set of shapes and text, not a grid of pixels. That single drawing feeds the on-screen preview and every export, so what you see cannot drift from what you get.
The commercial consequences:
- A printer can scale it. Vector artwork stays sharp at any size, so the PDF is genuinely press-ready rather than a screenshot in a PDF wrapper.
- The text stays text. Open the exported PDF and you can select the words. That is the proof it is vector.
- Resolution is a choice, not a limit. The PNG can be generated at whatever density is asked for, because it is rendered fresh each time.
It also made the product simpler. Rendering to vector removed the need for a screenshot library and a PDF library — the PNG is a canvas draw and the PDF is the browser's own print engine. Fewer dependencies, less to break.
cp .env.example .env.development # then set the two ports
cd code && npm install && npm run devThe .env files live at the repository root. Both Docker Compose and Vite
read them, so there is one source of truth and nothing to keep in sync.
Open the URL printed in the terminal.
Both stacks run from the repository root.
# Development — hot reload
docker compose --env-file .env.development \
-f docker-compose.dev.yml up
# Production — built and served by nginx
cp .env.example .env # then set FRONTEND_PORT
docker compose -f docker-compose.prod.yml up -d --buildProduction needs no --env-file: Compose auto-loads a file named exactly
.env, which is the production environment file.
Development does need it, because Compose only auto-loads that one name.
env_file: inside the compose file passes variables into the container,
while the ${...} substitutions are resolved by Compose beforehand — two
different mechanisms. Forget the flag and the dev stack stops with a clear
error rather than quietly starting on the production port.
Do not run the Docker dev stack and a local npm run dev at the same time —
the port is claimed strictly, so the second one fails loudly rather than
silently picking another.
VITE_SITE_URL in the .env files is the public origin, with no trailing
slash. It drives the canonical link, the Open Graph and Twitter tags, and
the generated robots.txt and sitemap.xml — so the domain is written in
one place rather than scattered through markup and static files.
.env.development VITE_SITE_URL=http://localhost:$FRONTEND_PORT
.env VITE_SITE_URL=https://your-domain.example
Set the production value before the first deploy. Until then the social previews and sitemap will point at a placeholder.
| Command | What it does |
|---|---|
npm run dev |
Development server with hot reload |
npm run lint |
ESLint across the project |
npx tsc --noEmit |
Type check |
npm test |
Unit tests (Vitest) |
npm run build |
Type check, then production build |
Lint and type check run after every unit of work. The production build runs only when the owner asks for it.
A Vite + React + TypeScript single-page application. No backend, no database, no authentication — the product needs none of them, and adding them would mean holding other people's contact details for no benefit.
| Area | Choice |
|---|---|
| Framework | React 19, TypeScript (strict), Vite 8 |
| Styling | Tailwind v4 plus a fluid design-token system |
| State | One useCardData hook; no global store |
| Validation | Zod — the schema is the single source of truth |
| Storage | localStorage, validated on read |
| QR | qrcode.react, rendered as SVG inside the card |
| Serving | nginx in production |
.
├── code/ # the application
│ ├── src/
│ │ ├── components/card/ # the SVG card and its text fitting
│ │ ├── components/editor/# the control panels
│ │ ├── lib/card/ # schema, layouts, vCard
│ │ ├── lib/fonts/ # registry, Google fetch, export embedding
│ │ └── lib/export/ # PNG, print sheet, file save
│ ├── Dockerfile.dev
│ ├── Dockerfile.prod
│ └── nginx.conf
├── docs/
│ ├── ARCHITECTURE.md # the implementation contract — read first
│ ├── Vite-Build-Template.md
│ └── Frontend-UI-Motion-Tooling-Standard.md
├── docker-compose.dev.yml
└── docker-compose.prod.yml
Both are easy to break by accident and both cause the export to stop matching the preview:
- Nothing on the card reads the app's light/dark setting. Card colours come from the card's own data. If the card followed the interface theme, switching to dark mode would silently change the file you export.
- Animation stops at the card's edge. Motion belongs to the editor. The exported file has no animation, so moving artwork would make the preview disagree with the output.
The head carries a descriptive title, meta description, canonical link,
Open Graph and Twitter Card tags, WebApplication structured data, a web
manifest, and a full icon set. robots.txt and sitemap.xml are generated
at build time from VITE_SITE_URL.
One honest caveat: this is a client-rendered single-page app, so what a crawler indexes is the static head above — not the editor, which only exists after JavaScript runs. That is the right trade for a tool (there is no content to rank beyond the landing page), but it does mean SEO effort belongs in the head and the social card, not in the app itself.
When a typeface is used on a card, that font file has to be packed inside the export. A font is not a colour — it is a file, and a PNG or PDF that merely references it will fall back to a different typeface on someone else's machine. Worse, it fails silently.
The app therefore collects every face in use and embeds it before rasterising. The Font embedding proof panel exists to verify this: pick a face, export the proof, and confirm it matches the preview exactly.
docs/ARCHITECTURE.md— the implementation contract: data model, layout rules, font pipeline, build order and verification.Agenthandoff.md— current state, decisions and watchouts. Read before changing anything.STACK.md— what is actually installed, scanned from the manifests.Changelog.md— release history.
Version 1 is implemented end to end. Known boundaries, all deliberate:
- Two layouts. More are additive; the layout contract holds without a template language.
- No bleed or crop marks. Cards export at trim size. Bleed can be added by composing the trim card into a larger canvas — the path is open.
- Local system fonts are best-effort. The browser feature that reads fonts off your machine exists only in Chrome and Edge. Where it is missing, the option is hidden rather than offered and broken.
- Uploaded raster logos stay raster. An SVG logo stays sharp at any print size; a PNG or JPEG is a photograph and will soften when enlarged. The app warns rather than blocks.
- No accounts, no sharing service. Cards move between people as files.
© 2026 Triffix Solutions. All rights reserved.