Build a SaaS starter in Next.js with shadcn/ui: marketing site, auth screens, a workspace-scoped app shell, and {{addon}} built in from the start.
Screens:
1. Landing page: hero with product name and one CTA, three feature cards, pricing with Free and Pro tiers, footer.
2. Sign up and log in: email and password forms with inline validation errors under each field.
3. /app dashboard: left nav (Home, {{resource}}, Team, Billing, Settings), top bar with a workspace switcher and avatar menu.
4. /app/{{resource}}: table of rows with create and edit in a Dialog, plus an archive action with confirmation.
5. /app/team: member list with role badges (owner, member) and an invite form that validates email format.
6. /app/settings: profile form, and a danger zone where deleting the account requires typing the workspace name.
7. /app/billing: current plan card and an upgrade button calling a stubbed checkout function with a TODO comment where Stripe would be wired.
Integrate {{addon}} using the shell's conventions: user-facing surfaces get a left-nav entry and screen, preferences get a control on /app/settings, and third-party credentials come from env vars with a stub path when keys are absent. Where it overlaps a screen above, extend that screen rather than duplicating it.
Data model for {{database}}: users, workspaces, memberships (userId, workspaceId, role), {{resource}} rows carrying workspaceId, and any tables {{addon}} needs, scoped the same way. Every read and write filters by the active workspace, and switching workspaces changes what the table shows.
Behaviors: visiting any /app route unauthenticated redirects to log in, then back after success. A new workspace is seeded with one example row so first login never shows an empty table. Log out clears the session and lands on the marketing page.
States: forms keep user input on failure; empty tables show a create prompt; a failed save raises a toast naming the action.
Styling: restrained and product-grade, single accent color, consistent radius, no marketing gradients inside /app.What v0 is good at
v0 is Vercel's generative UI tool. You describe a screen or component in plain text and it produces working React code, typically Next.js with Tailwind and shadcn/ui, rendered in a live preview you keep iterating on with follow-up messages. You can edit the code directly, fork versions, and push the result to a Vercel project or copy it into an existing repo.
It fits developers who want React they will actually ship and designers who want a real prototype rather than a static mockup. Against its neighbors: Lovable and Bolt aim at whole hosted applications with backends attached, and Replit Agent builds and deploys inside Replit's environment, while v0 is strongest at the interface layer of a Next.js app, individual screens, flows, and components that drop into a codebase you already run. If your stack is React on Vercel, v0's output needs the least translation of the four.
How to prompt v0
Write specs, not vibes. v0 does best with a structured brief: what the screen is, the layout regions, the components inside each, the data shape, and the states. A good build prompt runs 150 to 300 words; much shorter and v0 fills the gaps with stock dashboard tropes.
Conventions that pay off:
- One screen or flow per message. Get the settings page right, then request the next screen in a follow-up so the generation stays coherent. - Name shadcn/ui components when you care: Dialog rather than a hand-rolled modal, Sheet for side panels, the Command component for palettes. v0 knows the library and follows these cues reliably. - Spell out the data model as fields with types. Left alone, v0 invents plausible fields, and every later iteration inherits them. - List the states you expect: loading, empty, error, and what each looks like. Unprompted, v0 renders only the happy path with populated mock data. - Give a styling direction in one or two lines: palette, density, radius. Without it you get the default shadcn look every time. - When iterating, reference elements by name, "the revenue stat card", not by position, and change one thing per message.
Always say how data arrives: mocked behind a single function, fetched from an endpoint you name, or wired through an integration. That one line determines how much rework the export costs you.
All 20 v0 prompts
App 5 prompts
Build a to-do app as a single Next.js App Router page using shadcn/ui components. Data model: a tasks table with id, title, done, due_date, priority (low, medium, high), and created_at, stored in {{database}} through server actions in app/actions.ts. Screens: the main list at /, grouped into Today, Upcoming, and Done sections, plus a slide-over Sheet for editing a task. Behaviors: add a task from a single input pinned at the top, toggle done with a Checkbox and an optimistic update so the row moves into Done before the server action resolves, inline title editing on click, and delete with an undo Toast that restores the row if clicked in time. Filter tabs for All, Active, and Completed backed by a URL query param so refresh keeps the filter. Overdue tasks get a red Badge computed from due_date on the server, not in client JavaScript. Empty state: a plain card saying no tasks yet that autofocuses the input. Error state: if a server action throws, revert the optimistic change and surface a destructive Toast with a retry button. Styling: neutral background, one accent color, roomy row height, lucide-react icons only, no illustration packs. Acceptance: adding, completing, and deleting all work in the v0 preview without a full page reload, the Done count in the header always matches the visible list, and the chosen filter survives a refresh.Build a blog with a self-hosted admin instead of an external CMS. Public side: an index at /blog listing posts newest first with title, one-line excerpt, and a date formatted on the server to avoid hydration mismatch, plus a detail route at /blog/[slug] rendering the post body from markdown with heading anchors and styled code blocks. Data model: a posts table in {{database}} with id, slug, title, excerpt, body_markdown, status (draft or published), and published_at. Admin side: /admin lists every post with a status Badge, and /admin/edit/[id] is a two-pane editor, markdown textarea on the left, live preview on the right, with save draft and publish buttons calling server actions. Slugs generate from the title on create, stay editable afterward, and a uniqueness check appends a suffix on collision rather than failing the save. Behaviors: draft posts return notFound() on the public route, publishing sets published_at once and never overwrites it on later edits, and revalidatePath runs for both the index and the post after any save so public pages update. Empty states: the public index with zero published posts shows a plain nothing published yet card, and the admin list shows a create your first post button. Error state: a failed save keeps the editor content intact and shows an error Toast, never a redirect that loses work. Styling: a readable measure near 65 characters, serif headings allowed, shadcn/ui confined to the admin. Acceptance: create, publish, and read a post end to end inside the v0 preview.Build a small e-commerce storefront with three route groups: a product grid at /, product detail at /product/[slug], and a cart flow. Data model: products in {{database}} with id, slug, name, price_cents, inventory_count, and image_url, prices stored as integer cents and formatted through a single currency helper everywhere. Grid: responsive cards with a fixed image aspect ratio to prevent layout shift, name, price, and an out of stock overlay when inventory_count is zero that also disables the card link. Detail page: gallery, a quantity selector capped at available inventory, and an add to cart button that opens the cart as a shadcn/ui Sheet from the right instead of navigating away. Cart: a cookie-backed structure managed through server actions so it survives refresh in the v0 preview, line items with quantity steppers, remove buttons, a subtotal recomputed on the server, and a checkout button leading to /checkout with an order summary and an address form validated with zod. Payment stays a clearly marked stub at app/api/checkout/route.ts for a later {{addon}} integration, with the expected order payload documented in a comment. Edge cases: adding beyond inventory clamps the quantity and explains why in a Toast, a product deleted after being carted renders as unavailable with a remove prompt rather than crashing the subtotal, and unknown slugs return notFound(). Empty states: a seeded-empty grid shows a no products card, an empty cart Sheet links back to shopping. Acceptance: add, adjust, remove, and reach checkout end to end in the preview with the subtotal always matching the line items.Build a booking app where a visitor picks a time slot from a host's availability. Screens: a public booking page at /book/[username] with a month calendar on the left and that day's open slots on the right, a confirmation step collecting name and email, and a host dashboard at /dashboard for setting weekly availability and reviewing upcoming bookings. Data model in {{database}}: availability_rules (host_id, weekday, start_minute, end_minute) and bookings (id, host_id, starts_at as timestamptz, duration_minutes, guest_name, guest_email, status), with open slots computed on the server from rules minus existing bookings, never stored. Timezones are the core difficulty: store everything in UTC, detect the visitor's timezone from the browser, render slots in it with the timezone name visible above the list, and allow overriding it through a searchable Select. Behaviors: days with no open slots render dimmed and unclickable in the calendar, picking a slot holds it visually while the form completes, and the booking server action re-checks the slot inside a transaction so two simultaneous guests cannot double-book, the loser seeing a slot just taken message with refreshed slots rather than a silent failure. Past slots on the current day are filtered on the server. Confirmation screen: date and time in the guest timezone, an add to calendar link, and a cancel link tokenized by a random id in the URL. Empty state: a host with zero availability rules shows setup instructions on their public page. Acceptance: booking a slot in the v0 preview removes it from the picker immediately, and switching timezones relabels slots without changing which ones are available.Backend 5 prompts
Build a REST API for {{resource}} records as Next.js route handlers under app/api, backed by {{database}}. No pages or UI, route handlers only.
Endpoints:
- GET /api/{{resource}}: paginated list. Accept page and limit query params (default 20, max 100) plus sort=createdAt:desc. Respond with { data, page, total }.
- POST /api/{{resource}}: create from a JSON body. Return 201 and the created record.
- GET /api/{{resource}}/[id]: one record, or 404 with { error: "not_found" }.
- PATCH /api/{{resource}}/[id]: partial update. Unknown fields are a 400, not silently dropped.
- DELETE /api/{{resource}}/[id]: 204 on success, 404 if the id does not exist.
- GET /api/health: { ok: true } for smoke checks.
Data model: id (uuid), name (string, 1 to 120 chars), status ("active" | "archived"), createdAt, updatedAt. Define one zod schema and derive the POST and PATCH validators from it so the rules never drift apart.
Error contract: every failure is JSON { error, message } with the correct status code. Validation failures return 400 with zod's field-level issues. A malformed JSON body must also be a 400, never an unhandled 500.
Put all {{database}} access in lib/db.ts behind list, get, create, update, and remove functions; handlers never import the client directly. Finish with a README block showing one sample curl per endpoint so I can verify each route from a terminal.Build a GraphQL API inside a Next.js App Router project, served from one route handler at app/api/graphql/route.ts using graphql-yoga. Schema: a {{resource}} type with id, name, status, and updatedAt, a paginated list query using cursor pagination (first, after) rather than offsets, a single-item query by id, and create, update, and delete mutations that return the mutated object. Resolvers read {{database}} through a db.ts helper so the data layer stays swappable. Validation: reject an empty name with a GraphQL error carrying extensions.code BAD_USER_INPUT, and return null with a NOT_FOUND code when an id does not exist instead of throwing. Add a query depth limit so nested selections cannot recurse unbounded, and disable introspection when NODE_ENV is production. Also generate a minimal /playground page in the same project: a textarea for the query, a variables field, a run button, and a pretty-printed JSON response panel using shadcn/ui Card and Tabs, so the API is exercisable inside the v0 preview without external tooling. Playground empty state: a preloaded example query for the paginated list. Playground error state: network failures render the raw error body in a red monospace block, never a blank panel. Acceptance: the example query returns rows in the preview, a create mutation followed by the list query shows the new item, and a malformed query comes back as a structured GraphQL error response, not a 500.Add JWT authentication to an existing Next.js App Router project without pulling in a full auth provider. Route handlers: app/api/auth/login/route.ts verifies credentials against a users table in {{database}} using bcrypt comparison, then signs a short-lived access token and a longer refresh token with the jose library, both delivered as httpOnly, secure, sameSite strict cookies, never returned in the JSON body or stored in localStorage. app/api/auth/refresh/route.ts rotates the pair, and app/api/auth/logout/route.ts clears both cookies. Protection: middleware.ts guards everything under /dashboard and /api/protected, verifying signature and expiry, redirecting browsers to /login with a from query param for post-login return, and answering API calls with a JSON 401 instead of an HTML redirect. Secrets: read JWT_SECRET from env, and if it is missing show a plain configuration notice in the preview instead of crashing. UI: a /login page using shadcn/ui Form with inline field errors from server-side zod validation, a generic invalid credentials message that never reveals whether the email exists, and a disabled submit state while the request is in flight. Edge cases: an expired access token with a valid refresh token rotates silently on the next request, a tampered token clears both cookies and forces login, and the payload carries only the user id, no email or role claims. Acceptance: in the v0 preview, logging in lands on /dashboard, an incognito visit to /dashboard bounces to /login, and logout revokes access immediately.Set up a Postgres schema for a project tracker using the Neon integration and Drizzle ORM, with everything in code so the schema is reviewable. Files: db/schema.ts defines the tables, db/index.ts exports a client reading DATABASE_URL from env, and db/seed.ts inserts believable sample rows so the v0 preview has data immediately. Tables: users (id uuid defaulting to gen_random_uuid, email unique not null, created_at timestamptz defaulting to now), projects (id, owner_id referencing users with on delete cascade, name not null, archived boolean default false), and tasks (id, project_id referencing projects with on delete cascade, title not null, status as a Postgres enum of todo, doing, done, position integer for manual ordering, due_date nullable). Indexes: tasks on (project_id, status) because the board view filters on both, plus a partial index on projects where archived is false. Constraints live in the database, not only the app: a check that position is non-negative, and email uniqueness enforced by Postgres rather than a lookup before insert. Generate Drizzle relations so joined queries come back typed, and add db/queries.ts with one example, getProjectWithTasks ordered by position. Seed the awkward rows deliberately: one archived project, one task with a null due_date, and one project with zero tasks, so later UI work hits them early. Never store timestamps as text, and never expose serial ids across an API boundary where a uuid belongs. Acceptance: the seed runs cleanly twice thanks to onConflictDoNothing, and getProjectWithTasks returns typed rows in the preview.
Build a file upload endpoint on Vercel Blob using the client upload pattern, so files travel from the browser straight to Blob storage and never through a serverless function body limit. Server: app/api/upload/route.ts uses handleUpload to issue client tokens, restricting allowed content types to png, jpeg, and pdf, setting a maximum size in the token so oversized files are rejected before transfer, and prefixing pathnames with the authenticated user id so one user cannot overwrite another's files. The onUploadCompleted callback writes a row to an uploads table in {{database}} with url, pathname, size, and content_type, and that table is the source of truth for listing, never Blob list calls at request time. Client: a drop zone component with drag state styling, a per-file progress bar driven by the upload helper's progress events, an image preview from an object URL before the upload finishes, and a cancel control mid-flight. Rejections happen client side first, wrong type or too large shows an inline message naming the limit without a network round trip, and the server enforces the same rules regardless. Edge cases: duplicate filenames get random suffixes from the Blob helper rather than overwriting, a network drop mid-upload leaves a retry action on the failed item, and deleting an upload removes the Blob first and the database row second, tolerating a Blob that is already gone. A missing BLOB_READ_WRITE_TOKEN shows a setup notice in the v0 preview rather than a crash. Acceptance: upload a png, see its row in the list, delete it, and confirm its URL then returns not found.Features 4 prompts
Add search for {{resource}} to the current generation.
Placement: a search input in the top bar, focusable with the / key, plus Cmd+K (Ctrl+K on Windows) opening the same search as a command-palette style overlay.
Query behavior: debounce keystrokes by 300ms, trim whitespace, ignore queries under 2 characters, and cancel any in-flight request when a new one starts so stale responses can never overwrite fresh results. Match against name and description, case-insensitive.
Where filtering happens: server-side, in a GET /api/search?q= route handler reusing the existing data layer. If this generation only has client-side mocked data, filter the mock array through the same function signature so swapping to a real endpoint later changes one file.
Result states, all four: before any query, a hint with two example searches; while fetching, a compact skeleton list; results as rows showing name, status, and updated time with the matching substring highlighted; no results, echo the query, as in "Nothing for 'foo'", with a clear button.
URL sync: write the query to ?q= so results are shareable and the back button restores the previous search. Landing on a URL that already has ?q= runs the search immediately.
Keyboard: arrow keys move the highlight through results, Enter opens the selected item, Escape clears and closes the overlay. The highlighted row scrolls into view when it moves off screen.
Do not restyle anything outside the search components, and finish by listing the files you touched.Add an LLM chatbot to an existing app screen using the Vercel AI SDK. Server side: a streaming route handler at app/api/chat/route.ts that calls streamText with a system prompt loaded from lib/prompt.ts and a model name read from an environment variable so it can be swapped without code changes. Client side: a chat panel built as a shadcn/ui Sheet sliding in from the right, driven by the useChat hook, with streamed tokens rendering progressively rather than appearing all at once. Messages: user bubbles right-aligned, assistant bubbles left with a copy button, markdown rendered including fenced code blocks. Behaviors: Enter sends, Shift plus Enter inserts a newline, a stop button aborts the stream mid-response, and the input disables while a response is streaming. Persist the transcript to sessionStorage so closing and reopening the Sheet keeps the conversation, with a clear conversation action in the header. Empty state: three suggested starter questions rendered as clickable chips that submit on click. Error states: a missing API key shows an inline notice naming the exact env var to add in project settings, and a mid-stream failure keeps the partial response visible with a retry action instead of wiping it. Rate limit the route per session and return a friendly message when the cap is hit. Acceptance: in the v0 preview, a question streams token by token, stop works mid-answer, and refreshing the page keeps the transcript.
Add transactional email notifications to an existing app using Resend and React Email templates. Templates: real React components under emails/, welcome.tsx sent on signup and {{resource}}-updated.tsx sent when a watched record changes, each with a plain-text fallback, a preview line under 90 characters, and an unsubscribe footer link. Sending: a lib/send-email.ts helper wraps the Resend client and reads RESEND_API_KEY from env, and every trigger calls it from a server action or route handler, never from client code. Preferences: a notifications section on the settings page with a shadcn/ui Switch per email type, persisted to a notification_prefs table keyed by user id and checked before every send, so an opted-out user is skipped silently. Behaviors: sends happen after the database write commits, not before, so a failed save never emails anyone, and each attempt lands in an email_log table with recipient, template, and status for debugging. Failure handling: if the Resend call throws, the originating action still succeeds, the failure is recorded in email_log with its message, and nothing retries automatically, avoiding duplicate sends. Missing API key: the v0 preview routes sent emails into an on-screen dev inbox at /dev/emails instead of failing, so the whole flow is testable before Resend is connected. Dev inbox empty state: no emails sent yet. Acceptance: switching a preference off suppresses that email type, the dev inbox shows the welcome email after a test signup, and email_log rows match what the inbox displays.Add CSV import and export for {{resource}} records to an existing table view. Import: a Dialog opened from an Import button with three steps. Step one, a drop zone accepting only .csv with the size cap stated in the UI before parsing begins. Step two, a column mapping table where each CSV header maps to a known field through a shadcn/ui Select, with an auto-match by lowercase header name preselected. Step three, a validation preview showing the first rows with per-cell problems highlighted, bad email format, missing required name, duplicate id, plus a count of rows that will import versus be skipped. Parsing runs client side with papaparse in streaming mode so a large file never locks the UI, then rows post in batches to a server action that upserts and returns per-batch failure counts. Result screen: imported, skipped, and failed totals, with a downloadable CSV containing only the failed rows plus a reason column so users fix and retry exactly those. Export: an Export button hitting app/api/{{resource}}/export/route.ts, a route handler that streams rows with a proper Content-Disposition attachment header so the browser downloads instead of rendering, honoring the table's current filters and sort by reading them from query params. Edge cases handled with named messages rather than a generic parse error: quoted fields containing commas, a UTF-8 BOM from spreadsheet exports, an empty file, and a header-only file with zero data rows. Acceptance: importing the exported file back in produces zero failed rows in the v0 preview.Frontend 4 prompts
Build an admin dashboard for {{resource}} data in Next.js with shadcn/ui and Tailwind.
Layout: collapsible left sidebar with logo and nav (Overview, {{resource}}, Reports, Settings), a top bar holding a date-range picker and user menu, content on a 12-column grid.
Overview screen:
- Four stat cards: total count, active this week, conversion rate, errors. Each shows the delta versus the previous period with an up or down arrow.
- A line chart of daily counts for the selected range.
- A recent activity table: name, status badge, owner, updated time. Column-header sorting, 10 rows per page.
Behaviors: changing the date range refetches every widget and writes the range to the URL query string, so refresh and back button both preserve it. Clicking a table row opens a detail drawer from the right with the full record and an edit form.
States, per widget rather than global: loading shows a skeleton matching the widget's dimensions; error shows an inline message with a retry button rather than a blank card; empty shows a short explanation and a create action. A missing metric renders a placeholder character, never NaN or undefined.
Data: everything comes from a single getDashboardData(range) function returning mocked values derived from the range, so switching ranges visibly changes the numbers. I will replace this function with a real API call later.
Styling: neutral surface, one accent color, roomy spacing, no gradients. At 375px the sidebar collapses to icons and the stat cards stack vertically.Build a marketing landing page for a developer tool as one App Router page composed of clearly separated section components: Nav, Hero, LogoRow, Features, CodeDemo, PricingTeaser, FAQ, and Footer, each in its own file under components/landing/ so individual sections can be regenerated in later v0 iterations without touching the rest. Hero: a headline of eight words or fewer, one subline, a primary CTA plus a secondary docs link, no background video. CodeDemo: a tabbed shadcn/ui block with an Install tab holding the shell install command and a Quickstart tab holding a short {{language}} snippet that exercises the tool, each tab with a copy button, monospace font, and real syntax highlighting, not a screenshot. Features: a three-column grid on desktop collapsing to one column below the md breakpoint, each card with a lucide-react icon, a bolded claim, and two supporting lines, no filler adjectives. FAQ: an Accordion with five questions where only one item opens at a time. Behaviors: the nav gains a border and background blur after scrolling past the hero, anchor links scroll smoothly to their sections, and the primary CTA repeats once above the footer. Every image gets explicit width, height, and alt so nothing shifts during load, and every clickable element is a real button or link. Styling: dark theme, one accent color reserved for CTAs and links, content capped near 72rem, system font stack acceptable. Acceptance: the page holds at 375px and 1440px in the v0 preview with no horizontal scroll, the nav changes state on scroll, and the copy button copies the visible tab's snippet.Build a pricing page at /pricing with three tiers, Free, Pro, and Team, rendered from a single plans array in lib/plans.ts so copy edits never touch markup. Layout: three shadcn/ui Cards on desktop, stacked below the md breakpoint, the Pro card visually raised with a subtle ring and a Most popular Badge. Billing toggle: a monthly and annual Tabs control above the cards that swaps displayed prices from the plans array, annual showing the per-month equivalent with a small billed yearly note, and the selected term carried into every checkout link as a query param. Feature lists: check icons from lucide-react for included items, dimmed x icons for excluded ones so tiers compare at a glance, and a full comparison table further down inside an overflow-x auto wrapper so it scrolls on phones instead of breaking layout. Each tier gets a distinct CTA: Free links to signup, Pro links to a {{addon}} checkout route carrying plan and term params, Team opens a contact Dialog with a short form that validates email format inline. Edge cases: prices render from numbers through one currency formatter, never hardcoded strings, the toggle keeps keyboard focus when switching terms, and the comparison table repeats the tier CTAs in its footer row. Below the table, an Accordion with five billing questions, one open at a time. The contact Dialog needs a submitting state and a success message that replaces the form. Acceptance: toggling terms updates all three prices in the v0 preview and both paid CTAs carry the correct plan and term params.Build a sortable data table for {{resource}} records using the shadcn/ui Table primitives with TanStack Table managing column state, the pairing v0 scaffolds for this job. Columns: name, status rendered as a colored Badge, amount right-aligned through a currency formatter, created_at formatted on the server, and a row actions column holding a DropdownMenu with edit and delete. Sorting: clickable headers cycling ascending, descending, then cleared, an ArrowUp or ArrowDown lucide icon on the active column only, amount sorting numerically rather than lexically, and sort state written to URL query params like ?sort=amount&dir=desc so refreshes and shared links reproduce the view. Keep it single-column, multi-sort is out of scope. Above the table: a debounced text filter on name and a status Select that compose with sorting instead of resetting it. Pagination: a page size selector plus previous and next buttons in the footer, with a range label like showing 21 to 40 computed from real counts. States: a loading skeleton matching the exact column widths so the layout never jumps when data lands, an empty row spanning all columns when filters match nothing with a clear filters action, and an error row with a retry button if the fetch fails. Keep the basics honest without turning this into an audit: aria-sort on the active header, and buttons rather than divs for anything clickable. Acceptance: in the v0 preview, sorting by amount orders numerically, the URL updates as you sort, and clearing filters restores the full set without losing the chosen sort.Refactor 1 prompt
Audit the current generation for accessibility and fix everything you find. Keep the visual design intact except where contrast forces a change; this pass is about semantics, keyboard behavior, and screen reader output. Work through this checklist: - Landmarks and structure: exactly one main per page, nav and header where they belong, heading levels descending without skips. - Forms: every input has a programmatically associated label, and error text is connected via aria-describedby, not just placed nearby in red. - Buttons and icons: icon-only buttons get aria-label; decorative icons get aria-hidden; anything clickable is a button or a link, not a div with onClick. - Keyboard: dialogs and drawers trap focus and return it to the trigger on close; menus support arrow keys; no positive tabindex anywhere; every focusable element shows a visible focus ring, and any outline: none is removed or replaced. - Contrast: body text meets 4.5:1 and large text 3:1 against its actual background. Check muted foreground text on card backgrounds specifically, since that pairing fails most often. Adjust design tokens, not individual components. - Media and charts: meaningful images get alt text; charts get a text summary or an accessible data table alternative. - Motion: entrance and hover animations respect prefers-reduced-motion. - Live updates: toasts and async result areas announce through an aria-live region. - Add a skip-to-content link as the first focusable element on the page. When done, list each file changed with the specific fixes in it, and flag anything you could not fix automatically, such as color decisions that need a brand call.
Testing 1 prompt
Take the current generation and retrofit error handling end to end. Nothing may fail as a blank screen, a spinner that never resolves, or a silent console log.
Boundaries: wrap each route segment in an error boundary showing a short human message and a reset action, with no stack traces exposed to the user.
Data fetching: every fetch gets a failure branch rendered inline where the data would have appeared, with a retry that refetches only that section. Add an AbortController timeout of 10 seconds so a hung request becomes a visible error instead of an eternal skeleton.
Forms: submit failures render under the submit button, name what failed, and preserve everything the user typed. Field-level validation errors sit under their fields and clear when the field changes.
Mutations: optimistic updates roll back on failure and raise a toast naming the action that failed, like "Couldn't archive the item", never a bare "Something went wrong."
Route handlers, if this generation has any: wrap each in one shared helper that catches thrown errors and returns JSON { error, message } with an appropriate status, treating an unparseable body as a 400.
Offline: listen for the browser's offline and online events and show a dismissible banner while disconnected. Queue nothing, just inform.
So I can verify each path, add a dev-only debug panel with toggles that force fetch failure, mutation failure, and slow network. End by listing every file you changed and which failure path each change covers, so I can review before accepting.Prompting patterns that work in v0
Screen, regions, states
Structure a build prompt in three passes: what the screen is for, the layout regions from top to bottom, then the loading, empty, and error treatment for each region. v0 follows document order, so a spec organized this way maps cleanly onto the generated component tree.
Data shape first
Paste the field list or TypeScript interface before describing any UI, then refer to fields by name throughout. v0 threads real field names into tables, forms, and cards instead of inventing lorem-ipsum columns you rename later.
One change per message
When refining, isolate a single edit and name the target component. Bundled requests, fix the chart plus redo the nav plus add dark mode, make v0 rewrite more than you asked and regress parts that were already right.
The swap-point seam
Ask v0 to route all data access through one named function or file, and say you plan to replace it. You get a working mocked preview immediately and a single seam to cut when connecting the real backend after export.
Common mistakes
The whole-app prompt
Requesting a complete product in one message yields half a dozen shallow screens, none usable. Build the riskiest screen first, get it right, then extend the generation with follow-ups that reference what already exists.
Leaving data implicit
If you never state the fields, v0 invents them, and follow-ups keep building on the inventions. Renaming fields across a mature generation is the most tedious edit in the tool, so pin the model in the first message.
Restyling by adjectives
Asking for 'more modern' or 'cleaner' produces arbitrary changes on every attempt. Name tokens instead: the accent hex, the radius, the font, the spacing scale. v0 applies concrete values consistently; adjectives it reinterprets each message.