← Back to Verso

I built a Typeform alternative in one night

webdevjavascripttypescriptproductivity

Published March 28, 2026 · 6 min read

I've been annoyed by form builders for years. Not because they're bad — Typeform is genuinely great — but because every time I need a conversational form for a project, I'm either paying $50/month or duct-taping something together with React state and a prayer. Last Tuesday at around 9 PM, I decided to stop complaining and start building.

By 6 AM, Verso was alive.

The problem

Most form builders fall into two camps: the “dump every field on one page” camp (Google Forms, etc.) and the “beautiful but SaaS-locked” camp (Typeform, Tally). I wanted something in between — forms that feel good to fill out, with one-question-at-a-time flow and smooth transitions, but that I actually own and can embed anywhere without a monthly bill.

I also wanted real-time response analytics without having to export CSVs into a spreadsheet. In 2026, there's no reason a form builder can't show me a live chart the moment someone submits.

The core insight

A conversational form is just a state machine. Each step holds one question, transitions are animated, and the whole thing resolves to a flat response object at the end. Once I framed it that way, the architecture fell into place fast.

type FormStep = {
  id: string;
  type: FieldType;
  question: string;
  required: boolean;
  validation?: ValidationRule;
  next?: string | ConditionalBranch[];
};

type FormDefinition = {
  id: string;
  steps: FormStep[];
  theme: ThemeConfig;
};

Each step knows its own next— either a static ID or an array of conditional branches. That's it. No graph library, no complex routing engine. The renderer just walks the chain, evaluates conditions against the current response state, and animates to the next step.

Key technical decisions

Next.js 15 App Router was non-negotiable. I wanted the dashboard and builder to be server-rendered for speed, but the form renderer itself to be a fully client-side component that works when embedded via <script> tag. The App Router's mix of server and client components made this clean — the builder lives in the app, the renderer gets bundled separately as an embed script.

Tailwind v4 with CSS-first configuration. No more tailwind.config.js — I defined the entire dark-first design system in a @theme block. The color palette (#0F0F14 background, #6C5CE7 violet primary, #00D2D3 teal accent) came straight from the design spec and translating it into Tailwind v4 theme variables took maybe five minutes.

shadcn/uifor the dashboard components. I didn't want to build a data table or a dropdown menu from scratch at 2 AM. shadcn gave me accessible, composable primitives that I could restyle to match the dark palette. The form builder canvas itself is custom, but everything around it — navigation, modals, the response table — is shadcn with Verso's theme layered on top.

What surprised me

The embed system was easier than expected. I was dreading the iframe-vs-script-tag decision, but I ended up supporting both. The script tag loads a tiny (~8KB) loader that mounts a Shadow DOM container, fetches the form definition, and renders the stepper inside it. Shadow DOM means the host page's styles can't leak in — something that bit me on past projects.

// embed loader — the whole thing is ~40 lines
const root = document.createElement("div");
root.attachShadow({ mode: "open" });
const sheet = new CSSStyleSheet();
sheet.replaceSync(VERSO_STYLES);
root.shadowRoot.adoptedStyleSheets = [sheet];

mountForm(root.shadowRoot, formId);

What actually surprised me was how much time I spent on animations. Getting the step transitions to feel right — not too fast, not too floaty — took more iteration than the entire data layer. I landed on 250ms ease-out for forward transitions and 200ms ease-in for going back. Small difference, but it makes the form feel like it has momentum going forward and snaps back quickly when you correct something.

The other surprise: demo data matters more than I thought. I seeded four example forms with realistic fake responses spread across the last 30 days. The moment I loaded the dashboard and saw charts that looked alive, the whole product felt real. If I'd shipped with an empty state only, I would've lost motivation at 3 AM.

What I'd do next

Conditional logic branching is in — but the builder UI for it is still rough. I want a visual flow view where you can see the branch paths, not just a dropdown that says “if answer is X, go to step Y.” That's a proper day of work, not a night-of hack.

I'd also add collaboration features. Right now it's single-user, but the form definitions are just JSON — adding real-time multi-user editing with something like Yjs or PartyKit is the obvious next step.

And webhooks. Every response should be able to fire a webhook so you can pipe data into Slack, Notion, a CRM, whatever. The response handler already emits events internally, so this is mostly plumbing.

Try it

Verso is live. The builder is free, embeds are free, and your data stays yours. If you've been looking for a form tool that respects both the person filling it out and the person building it, give it a shot.

If you have feedback or want to contribute, the repo is open — I'd love to hear what you build with it.

← Back to Verso