[{"data":1,"prerenderedAt":647},["ShallowReactive",2],{"content-\u002Fdocs\u002Frecipes\u002Ffocus-on-error":3},{"id":4,"title":5,"body":6,"description":16,"extension":641,"meta":642,"navigation":249,"path":643,"seo":644,"stem":645,"__hash__":646},"docs\u002Fdocs\u002Frecipes\u002Ffocus-on-error.md","Focus (and scroll) to the first error",{"type":7,"value":8,"toc":634},"minimark",[9,13,17,22,98,101,175,191,195,201,507,510,529,533,536,563,580,584,612,616,630],[10,11,5],"h1",{"id":12},"focus-and-scroll-to-the-first-error",[14,15,16],"p",{},"When a form fails validation, dropping the user at the top of the\npage isn't helpful — they want to see which field is wrong and\nstart fixing. Two ways to wire it up.",[18,19,21],"h2",{"id":20},"declarative-most-forms","Declarative (most forms)",[23,24,29],"pre",{"className":25,"code":26,"language":27,"meta":28,"style":28},"language-ts shiki shiki-themes github-light github-dark","const { handleSubmit } = useForm({\n  schema,\n  key: 'signup',\n  onInvalidSubmit: 'focus-first-error',\n})\n","ts","",[30,31,32,62,68,81,92],"code",{"__ignoreMap":28},[33,34,37,41,45,49,52,55,59],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"szBVR","const",[33,42,44],{"class":43},"sVt8B"," { ",[33,46,48],{"class":47},"sj4cs","handleSubmit",[33,50,51],{"class":43}," } ",[33,53,54],{"class":39},"=",[33,56,58],{"class":57},"sScJk"," useForm",[33,60,61],{"class":43},"({\n",[33,63,65],{"class":35,"line":64},2,[33,66,67],{"class":43},"  schema,\n",[33,69,71,74,78],{"class":35,"line":70},3,[33,72,73],{"class":43},"  key: ",[33,75,77],{"class":76},"sZZnC","'signup'",[33,79,80],{"class":43},",\n",[33,82,84,87,90],{"class":35,"line":83},4,[33,85,86],{"class":43},"  onInvalidSubmit: ",[33,88,89],{"class":76},"'focus-first-error'",[33,91,80],{"class":43},[33,93,95],{"class":35,"line":94},5,[33,96,97],{"class":43},"})\n",[14,99,100],{},"Four policies:",[102,103,104,117],"table",{},[105,106,107],"thead",{},[108,109,110,114],"tr",{},[111,112,113],"th",{},"Policy",[111,115,116],{},"What happens",[118,119,120,135,148,161],"tbody",{},[108,121,122,128],{},[123,124,125],"td",{},[30,126,127],{},"'none'",[123,129,130,131,134],{},"Default. No-op. Wire your own via ",[30,132,133],{},"onError",".",[108,136,137,141],{},[123,138,139],{},[30,140,89],{},[123,142,143,144,147],{},"Calls ",[30,145,146],{},".focus()"," on the first errored field. The browser may scroll.",[108,149,150,155],{},[123,151,152],{},[30,153,154],{},"'scroll-to-first-error'",[123,156,143,157,160],{},[30,158,159],{},".scrollIntoView()"," on it. No focus change.",[108,162,163,168],{},[123,164,165],{},[30,166,167],{},"'both'",[123,169,170,171,174],{},"Scrolls first, then focuses with ",[30,172,173],{},"{ preventScroll: true }"," so the browser doesn't re-scroll and undo the explicit one.",[14,176,177,178,181,182,184,185,187,188,190],{},"The policy fires after ",[30,179,180],{},"errors"," is populated and before your\n",[30,183,133],{}," callback — ",[30,186,133],{}," can override it by calling ",[30,189,146],{},"\non something else.",[18,192,194],{"id":193},"imperative-server-errors-manual-flows","Imperative (server errors, manual flows)",[14,196,197,198,200],{},"For errors that don't come through ",[30,199,48],{}," — a 422 from your\nAPI, a custom \"validate + continue\" button — call the helpers\ndirectly:",[23,202,206],{"className":203,"code":204,"language":205,"meta":28,"style":28},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\n  import { useForm, parseApiErrors } from 'attaform'\n\n  const form = useForm({ schema, key: 'signup' })\n  const { handleSubmit, setFieldErrors, scrollToFirstError, focusFirstError } = form\n\n  const onSubmit = handleSubmit(async (values) => {\n    try {\n      await $fetch('\u002Fapi\u002Fsignup', { method: 'POST', body: values })\n    } catch (err) {\n      if (err.statusCode === 422) {\n        const result = parseApiErrors(err.data, { formKey: form.key })\n        if (result.ok) {\n          setFieldErrors(result.errors)\n          scrollToFirstError({ block: 'center', behavior: 'smooth' })\n          focusFirstError({ preventScroll: true })\n        }\n      }\n    }\n  })\n\u003C\u002Fscript>\n","vue",[30,207,208,231,245,251,272,303,308,343,351,374,386,404,421,430,439,459,473,479,485,491,497],{"__ignoreMap":28},[33,209,210,213,217,220,223,225,228],{"class":35,"line":36},[33,211,212],{"class":43},"\u003C",[33,214,216],{"class":215},"s9eBZ","script",[33,218,219],{"class":57}," setup",[33,221,222],{"class":57}," lang",[33,224,54],{"class":43},[33,226,227],{"class":76},"\"ts\"",[33,229,230],{"class":43},">\n",[33,232,233,236,239,242],{"class":35,"line":64},[33,234,235],{"class":39},"  import",[33,237,238],{"class":43}," { useForm, parseApiErrors } ",[33,240,241],{"class":39},"from",[33,243,244],{"class":76}," 'attaform'\n",[33,246,247],{"class":35,"line":70},[33,248,250],{"emptyLinePlaceholder":249},true,"\n",[33,252,253,256,259,262,264,267,269],{"class":35,"line":83},[33,254,255],{"class":39},"  const",[33,257,258],{"class":47}," form",[33,260,261],{"class":39}," =",[33,263,58],{"class":57},[33,265,266],{"class":43},"({ schema, key: ",[33,268,77],{"class":76},[33,270,271],{"class":43}," })\n",[33,273,274,276,278,280,283,286,288,291,293,296,298,300],{"class":35,"line":94},[33,275,255],{"class":39},[33,277,44],{"class":43},[33,279,48],{"class":47},[33,281,282],{"class":43},", ",[33,284,285],{"class":47},"setFieldErrors",[33,287,282],{"class":43},[33,289,290],{"class":47},"scrollToFirstError",[33,292,282],{"class":43},[33,294,295],{"class":47},"focusFirstError",[33,297,51],{"class":43},[33,299,54],{"class":39},[33,301,302],{"class":43}," form\n",[33,304,306],{"class":35,"line":305},6,[33,307,250],{"emptyLinePlaceholder":249},[33,309,311,313,316,318,321,324,327,330,334,337,340],{"class":35,"line":310},7,[33,312,255],{"class":39},[33,314,315],{"class":47}," onSubmit",[33,317,261],{"class":39},[33,319,320],{"class":57}," handleSubmit",[33,322,323],{"class":43},"(",[33,325,326],{"class":39},"async",[33,328,329],{"class":43}," (",[33,331,333],{"class":332},"s4XuR","values",[33,335,336],{"class":43},") ",[33,338,339],{"class":39},"=>",[33,341,342],{"class":43}," {\n",[33,344,346,349],{"class":35,"line":345},8,[33,347,348],{"class":39},"    try",[33,350,342],{"class":43},[33,352,354,357,360,362,365,368,371],{"class":35,"line":353},9,[33,355,356],{"class":39},"      await",[33,358,359],{"class":57}," $fetch",[33,361,323],{"class":43},[33,363,364],{"class":76},"'\u002Fapi\u002Fsignup'",[33,366,367],{"class":43},", { method: ",[33,369,370],{"class":76},"'POST'",[33,372,373],{"class":43},", body: values })\n",[33,375,377,380,383],{"class":35,"line":376},10,[33,378,379],{"class":43},"    } ",[33,381,382],{"class":39},"catch",[33,384,385],{"class":43}," (err) {\n",[33,387,389,392,395,398,401],{"class":35,"line":388},11,[33,390,391],{"class":39},"      if",[33,393,394],{"class":43}," (err.statusCode ",[33,396,397],{"class":39},"===",[33,399,400],{"class":47}," 422",[33,402,403],{"class":43},") {\n",[33,405,407,410,413,415,418],{"class":35,"line":406},12,[33,408,409],{"class":39},"        const",[33,411,412],{"class":47}," result",[33,414,261],{"class":39},[33,416,417],{"class":57}," parseApiErrors",[33,419,420],{"class":43},"(err.data, { formKey: form.key })\n",[33,422,424,427],{"class":35,"line":423},13,[33,425,426],{"class":39},"        if",[33,428,429],{"class":43}," (result.ok) {\n",[33,431,433,436],{"class":35,"line":432},14,[33,434,435],{"class":57},"          setFieldErrors",[33,437,438],{"class":43},"(result.errors)\n",[33,440,442,445,448,451,454,457],{"class":35,"line":441},15,[33,443,444],{"class":57},"          scrollToFirstError",[33,446,447],{"class":43},"({ block: ",[33,449,450],{"class":76},"'center'",[33,452,453],{"class":43},", behavior: ",[33,455,456],{"class":76},"'smooth'",[33,458,271],{"class":43},[33,460,462,465,468,471],{"class":35,"line":461},16,[33,463,464],{"class":57},"          focusFirstError",[33,466,467],{"class":43},"({ preventScroll: ",[33,469,470],{"class":47},"true",[33,472,271],{"class":43},[33,474,476],{"class":35,"line":475},17,[33,477,478],{"class":43},"        }\n",[33,480,482],{"class":35,"line":481},18,[33,483,484],{"class":43},"      }\n",[33,486,488],{"class":35,"line":487},19,[33,489,490],{"class":43},"    }\n",[33,492,494],{"class":35,"line":493},20,[33,495,496],{"class":43},"  })\n",[33,498,500,503,505],{"class":35,"line":499},21,[33,501,502],{"class":43},"\u003C\u002F",[33,504,216],{"class":215},[33,506,230],{"class":43},[14,508,509],{},"Both helpers return a boolean:",[511,512,513,519],"ul",{},[514,515,516,518],"li",{},[30,517,470],{}," — an element was acted on.",[514,520,521,524,525,528],{},[30,522,523],{},"false"," — no errored field had a mounted, visible element (common when the bad field is behind a ",[30,526,527],{},"v-if=\"false\"",").",[18,530,532],{"id":531},"how-first-is-picked","How \"first\" is picked",[14,534,535],{},"The library walks errors in the order your schema reported them and\nacts on the first field that's:",[537,538,539,550,553],"ol",{},[514,540,541,542,545,546,549],{},"Registered via ",[30,543,544],{},"v-register"," (or a manual ",[30,547,548],{},"registerElement"," call).",[514,551,552],{},"Currently in the DOM.",[514,554,555,556,559,560,562],{},"Visible (",[30,557,558],{},"display:none"," and ancestor ",[30,561,558],{}," are skipped).",[14,564,565,566,282,569,572,573,576,577,579],{},"Fields hidden via ",[30,567,568],{},"visibility: hidden",[30,570,571],{},"opacity: 0",", or ",[30,574,575],{},"aria-hidden","\ncount as \"visible\" — they occupy layout. If that's wrong for your\nUI, inspect ",[30,578,180],{}," yourself and focus the right element.",[18,581,583],{"id":582},"edge-cases","Edge cases",[511,585,586,600,606],{},[514,587,588,592,593,595,596,599],{},[589,590,591],"strong",{},"Errors on a conditional field",": the element isn't registered,\nso both helpers return ",[30,594,523],{}," and ",[30,597,598],{},"onInvalidSubmit"," silently\nno-ops. Gate your own fallback on the return value.",[514,601,602,605],{},[589,603,604],{},"Array-of-objects forms",": each array index registers its own\nelement; \"first\" follows schema order, which usually matches\nrender order.",[514,607,608,611],{},[589,609,610],{},"Multiple elements at the same path"," (a radio-group, say): the\nfirst registered element wins.",[18,613,615],{"id":614},"when-to-use-which","When to use which",[511,617,618,624],{},[514,619,620,623],{},[589,621,622],{},"Declarative"," for the 90% case. Set it once per form.",[514,625,626,629],{},[589,627,628],{},"Imperative"," for server errors, multi-step flows, or when you\nwant to combine focus with a toast \u002F ARIA announcement.",[631,632,633],"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 .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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":28,"searchDepth":64,"depth":64,"links":635},[636,637,638,639,640],{"id":20,"depth":64,"text":21},{"id":193,"depth":64,"text":194},{"id":531,"depth":64,"text":532},{"id":582,"depth":64,"text":583},{"id":614,"depth":64,"text":615},"md",{},"\u002Fdocs\u002Frecipes\u002Ffocus-on-error",{"title":5,"description":16},"docs\u002Frecipes\u002Ffocus-on-error","NGvBGEuYrEKvowq_PAKK5PmL3jD4eFBS5GoL2Qk0w_g",1777934136495]