[{"data":1,"prerenderedAt":640},["ShallowReactive",2],{"content-\u002Fdocs\u002Fwhy":3},{"id":4,"title":5,"body":6,"description":633,"extension":634,"meta":635,"navigation":133,"path":636,"seo":637,"stem":638,"__hash__":639},"docs\u002Fdocs\u002Fwhy.md","Why Attaform",{"type":7,"value":8,"toc":623},"minimark",[9,13,17,22,25,75,78,82,85,273,279,283,321,325,328,357,372,376,379,438,442,455,498,501,505,527,531,619],[10,11,5],"h1",{"id":12},"why-attaform",[14,15,16],"p",{},"You're choosing a form library for a Vue 3 or Nuxt project, and you\nhave to bet on something that'll still feel right at the end of the\nyear — not just at the end of the afternoon. Here's the case for\nAttaform, on its own terms.",[18,19,21],"h2",{"id":20},"one-source-of-truth-your-schema","One source of truth: your schema",[14,23,24],{},"Write a Zod schema. That's the source of truth for:",[26,27,28,41,59,65],"ul",{},[29,30,31,35,36,40],"li",{},[32,33,34],"strong",{},"Types"," — every path, value, error, and write shape is inferred.\nNo ",[37,38,39],"code",{},"any",", no manual generics, no reaching for the type plumbing\nwhenever you add a field.",[29,42,43,46,47,50,51,54,55,58],{},[32,44,45],{},"Defaults"," — Attaform reads the schema's slim shape (",[37,48,49],{},"''"," for\nstrings, ",[37,52,53],{},"0"," for numbers, ",[37,56,57],{},"false"," for booleans) and uses it as the\nstorage default. Override per field; don't repeat what the schema\nalready says.",[29,60,61,64],{},[32,62,63],{},"Validation"," — refinements run synchronously by default, async\nrefinements await before submit dispatches.",[29,66,67,70,71,74],{},[32,68,69],{},"Errors"," — refinements emit, paths surface — ",[37,72,73],{},"form.errors.email","\nis reactive end-to-end.",[14,76,77],{},"One schema in, full reactive surface out. The schema is the API.",[18,79,81],{"id":80},"type-safe-end-to-end","Type-safe end to end",[14,83,84],{},"Every part of the public surface is typed against your schema:",[86,87,93],"pre",{"className":88,"code":89,"language":90,"meta":91,"style":92},"language-ts shiki shiki-themes github-light github-dark","import { useForm } from 'attaform\u002Fzod'\nimport { z } from 'zod'\n\nconst schema = z.object({\n  email: z.email(),\n  age: z.number().int().min(13),\n})\n\nconst form = useForm({ schema })\n\n\u002F\u002F `form.fields.\u003Cpath>` knows the exact set of paths in the schema.\n\u002F\u002F `form.errors.\u003Cpath>` is reactive, typed, narrowable.\n\u002F\u002F `form.setValue('age', 'twenty-one')` is a type error.\nform.setValue('age', 21)\n","ts","twoslash","",[37,94,95,115,128,135,158,170,199,205,210,226,231,238,244,250],{"__ignoreMap":92},[96,97,100,104,108,111],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"szBVR","import",[96,105,107],{"class":106},"sVt8B"," { useForm } ",[96,109,110],{"class":102},"from",[96,112,114],{"class":113},"sZZnC"," 'attaform\u002Fzod'\n",[96,116,118,120,123,125],{"class":98,"line":117},2,[96,119,103],{"class":102},[96,121,122],{"class":106}," { z } ",[96,124,110],{"class":102},[96,126,127],{"class":113}," 'zod'\n",[96,129,131],{"class":98,"line":130},3,[96,132,134],{"emptyLinePlaceholder":133},true,"\n",[96,136,138,141,145,148,151,155],{"class":98,"line":137},4,[96,139,140],{"class":102},"const",[96,142,144],{"class":143},"sj4cs"," schema",[96,146,147],{"class":102}," =",[96,149,150],{"class":106}," z.",[96,152,154],{"class":153},"sScJk","object",[96,156,157],{"class":106},"({\n",[96,159,161,164,167],{"class":98,"line":160},5,[96,162,163],{"class":106},"  email: z.",[96,165,166],{"class":153},"email",[96,168,169],{"class":106},"(),\n",[96,171,173,176,179,182,185,187,190,193,196],{"class":98,"line":172},6,[96,174,175],{"class":106},"  age: z.",[96,177,178],{"class":153},"number",[96,180,181],{"class":106},"().",[96,183,184],{"class":153},"int",[96,186,181],{"class":106},[96,188,189],{"class":153},"min",[96,191,192],{"class":106},"(",[96,194,195],{"class":143},"13",[96,197,198],{"class":106},"),\n",[96,200,202],{"class":98,"line":201},7,[96,203,204],{"class":106},"})\n",[96,206,208],{"class":98,"line":207},8,[96,209,134],{"emptyLinePlaceholder":133},[96,211,213,215,218,220,223],{"class":98,"line":212},9,[96,214,140],{"class":102},[96,216,217],{"class":143}," form",[96,219,147],{"class":102},[96,221,222],{"class":153}," useForm",[96,224,225],{"class":106},"({ schema })\n",[96,227,229],{"class":98,"line":228},10,[96,230,134],{"emptyLinePlaceholder":133},[96,232,234],{"class":98,"line":233},11,[96,235,237],{"class":236},"sJ8bj","\u002F\u002F `form.fields.\u003Cpath>` knows the exact set of paths in the schema.\n",[96,239,241],{"class":98,"line":240},12,[96,242,243],{"class":236},"\u002F\u002F `form.errors.\u003Cpath>` is reactive, typed, narrowable.\n",[96,245,247],{"class":98,"line":246},13,[96,248,249],{"class":236},"\u002F\u002F `form.setValue('age', 'twenty-one')` is a type error.\n",[96,251,253,256,259,261,264,267,270],{"class":98,"line":252},14,[96,254,255],{"class":106},"form.",[96,257,258],{"class":153},"setValue",[96,260,192],{"class":106},[96,262,263],{"class":113},"'age'",[96,265,266],{"class":106},", ",[96,268,269],{"class":143},"21",[96,271,272],{"class":106},")\n",[14,274,275,278],{},[37,276,277],{},"form.fields(path)"," returns aggregated state at any depth — leaves\nor containers, both. You don't write a separate \"are any of these\nfields touched\" reducer; the rolled-up FieldState already knows.",[18,280,282],{"id":281},"live-layered-validation","Live, layered validation",[26,284,285,299,302,314],{},[29,286,287,288,266,291,294,295,298],{},"Per-field on ",[37,289,290],{},"change",[37,292,293],{},"blur",", or ",[37,296,297],{},"submit"," — your call, per form.",[29,300,301],{},"Sync refinements fire on the keystroke, async refinements await.",[29,303,304,305,308,309,313],{},"A form's ",[37,306,307],{},"meta.valid"," is ",[310,311,312],"em",{},"gated"," — it only flips true after every\nactive path has resolved at least one validation pass, including\nthe async ones. No flash-of-valid window for users with a slow\nuniqueness check.",[29,315,316,317,320],{},"Server-side errors map back into the same reactive store via\n",[37,318,319],{},"parseApiErrors",". The render surface is the same whether the\nerror came from Zod or your API.",[18,322,324],{"id":323},"ssr-first-hydration-clean","SSR-first, hydration-clean",[14,326,327],{},"Forms render server-side and hydrate without a flash:",[26,329,330,340],{},[29,331,332,335,336,339],{},[32,333,334],{},"Nuxt"," — zero config. The module ships an SSR plugin that\nthreads form state through ",[37,337,338],{},"nuxtApp.payload",". Values, errors,\ntouched \u002F focused \u002F blurred flags all round-trip.",[29,341,342,348,349,352,353,356],{},[32,343,344,345],{},"Bare Vue 3 + ",[37,346,347],{},"@vue\u002Fserver-renderer"," — two one-liner helpers\n(",[37,350,351],{},"renderAttaformState"," \u002F ",[37,354,355],{},"hydrateAttaformState",") bridge the\nserver → client boundary.",[14,358,359,360,363,364,371],{},"The form your server rendered ",[310,361,362],{},"is"," the form your client picks up.\nRead ",[365,366,368],"a",{"href":367},".\u002Frecipes\u002Fssr-hydration",[37,369,370],{},"recipes\u002Fssr-hydration"," for the\nfull setup.",[18,373,375],{"id":374},"built-in-not-bolted-on","Built-in, not bolted on",[14,377,378],{},"These ship with the core, not as third-party plugins:",[26,380,381,400,406,412,418,424],{},[29,382,383,386,387,352,390,352,393,352,396,399],{},[32,384,385],{},"Field arrays"," — typed ",[37,388,389],{},"append",[37,391,392],{},"insert",[37,394,395],{},"remove",[37,397,398],{},"swap","\nwith stable keys and per-item validation.",[29,401,402,405],{},[32,403,404],{},"Undo \u002F redo"," — bounded history stack, opt-in per form,\nintegrates cleanly with persistence and SSR.",[29,407,408,411],{},[32,409,410],{},"Persistence"," — opt-in per field, write to localStorage,\nsessionStorage, or IndexedDB. Sensitive paths (passwords, tokens)\nare excluded by default.",[29,413,414,417],{},[32,415,416],{},"Discriminated unions"," — variant-aware fields, snapshot\u002Frestore\non discriminator change. Switch between branches without losing\nthe values you typed.",[29,419,420,423],{},[32,421,422],{},"DevTools"," — every form shows up in the Vue DevTools panel:\ninspect state, errors, history, persistence drafts.",[29,425,426,429,430,433,434,437],{},[32,427,428],{},"Schema-attached metadata"," — ",[37,431,432],{},"withMeta(schema, { label, description })"," flows directly into ",[37,435,436],{},"form.fields.\u003Cpath>.label",".\nStop hard-coding labels in JSX.",[18,439,441],{"id":440},"native-inputs-vue-directive","Native inputs, Vue directive",[14,443,444,447,448,451,452,454],{},[37,445,446],{},"v-register"," is a Vue directive, not a wrapper component. Your\n",[37,449,450],{},"\u003Cinput>"," stays a native ",[37,453,450],{},"; there's no field-component\noverhead between the DOM and the form.",[86,456,460],{"className":457,"code":458,"language":459,"meta":92,"style":92},"language-vue shiki shiki-themes github-light github-dark","\u003Cinput v-register=\"form.register('email')\" \u002F>\n","vue",[37,461,462],{"__ignoreMap":92},[96,463,464,467,471,474,477,480,482,485,487,490,493,495],{"class":98,"line":99},[96,465,466],{"class":106},"\u003C",[96,468,470],{"class":469},"s9eBZ","input",[96,472,473],{"class":153}," v-register",[96,475,476],{"class":106},"=",[96,478,479],{"class":113},"\"",[96,481,255],{"class":106},[96,483,484],{"class":153},"register",[96,486,192],{"class":106},[96,488,489],{"class":113},"'email'",[96,491,492],{"class":106},")",[96,494,479],{"class":113},[96,496,497],{"class":106}," \u002F>\n",[14,499,500],{},"That's the whole binding. A11y attributes, value sync, focus state,\nblank tracking — all native.",[18,502,504],{"id":503},"tree-shakable-esm-only","Tree-shakable, ESM-only",[14,506,507,508,510,511,514,515,518,519,522,523,526],{},"Attaform ships ESM. The Vite plugin applies ",[37,509,446],{}," transforms\nat compile time so the production bundle stays slim — no runtime\ndirective resolution, no compatibility shims for non-Vue\nenvironments. Bring only the entry you need: ",[37,512,513],{},"attaform\u002Fzod"," for\nZod 4, ",[37,516,517],{},"attaform\u002Fzod-v3"," for Zod 3, ",[37,520,521],{},"attaform\u002Fnuxt"," for the Nuxt\nmodule, ",[37,524,525],{},"attaform\u002Fvite"," for the build plugin.",[18,528,530],{"id":529},"where-to-next","Where to next",[532,533,534,547],"table",{},[535,536,537],"thead",{},[538,539,540,544],"tr",{},[541,542,543],"th",{},"Goal",[541,545,546],{},"Read",[548,549,550,562,576,587,597,607],"tbody",{},[538,551,552,556],{},[553,554,555],"td",{},"Get a form on screen",[553,557,558],{},[365,559,561],{"href":560},".\u002Fquickstart","Quick start",[538,563,564,567],{},[553,565,566],{},"Understand the full surface",[553,568,569],{},[365,570,572,575],{"href":571},".\u002Fapi\u002Fuse-form-return",[37,573,574],{},"useForm"," return value",[538,577,578,581],{},[553,579,580],{},"Add server-side errors",[553,582,583],{},[365,584,586],{"href":585},".\u002Frecipes\u002Fserver-errors","Server errors",[538,588,589,592],{},[553,590,591],{},"SSR-render forms in Nuxt or bare Vue",[553,593,594],{},[365,595,596],{"href":367},"SSR hydration",[538,598,599,602],{},[553,600,601],{},"Persist long forms across reloads",[553,603,604],{},[365,605,410],{"href":606},".\u002Frecipes\u002Fpersistence",[538,608,609,612],{},[553,610,611],{},"Compare with what you already write",[553,613,614],{},[365,615,616,617,575],{"href":571},"The ",[37,618,574],{},[620,621,622],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":92,"searchDepth":117,"depth":117,"links":624},[625,626,627,628,629,630,631,632],{"id":20,"depth":117,"text":21},{"id":80,"depth":117,"text":81},{"id":281,"depth":117,"text":282},{"id":323,"depth":117,"text":324},{"id":374,"depth":117,"text":375},{"id":440,"depth":117,"text":441},{"id":503,"depth":117,"text":504},{"id":529,"depth":117,"text":530},"Why teams pick Attaform for Vue 3 forms — schema-driven types, validation, SSR, persistence, undo\u002Fredo, devtools, all from one Zod schema.","md",{},"\u002Fdocs\u002Fwhy",{"title":5,"description":633},"docs\u002Fwhy","8NpEWx0U3R3e88ac5-tawv-bWG48iynurxSKIbq_6fw",1778164224345]