Forms
Every field below runs through a real SvelteKit remote form (no database — it just returns a success message). This is the pattern every app form should follow, not a hand-rolled input + onsubmit.
Sizes
Omit size for the default 35px height. size='sm' is the one shorter footprint (28px) — for filter bars, dense panels, inline row editing. Same two-tier scale as Button.
<Input placeholder="Default (35px)" />
<Input placeholder="Small (28px)" size="sm" />A complete form
<Form> + field components + <Button form>, wired to src/lib/remote/demo.remote.ts. The success message is toasted automatically — no onSuccess wiring needed just to surface it. Field schemas come from zeyo-kit/utils/form (formText, formEmail, nullable(...), …), never raw zod.
<Form form={demoContactForm} schema={DemoContactSchema}>
<TextField name="name" form={demoContactForm}>Name</TextField>
<EmailField name="email" form={demoContactForm}>Email</EmailField>
<SelectField
name="topic"
form={demoContactForm}
getKey={(opt) => opt.value}
getLabel={(opt) => opt.label}
getValue={(opt) => opt.value}
items={topicOptions}
label="Topic"
placeholder="Topic"
/>
<NumberField name="seats" form={demoContactForm} optional>Seats (optional)</NumberField>
<MessageField name="message" form={demoContactForm} optional>Message (optional)</MessageField>
<SwitchField name="urgent" form={demoContactForm} optional variant="onoff">Urgent</SwitchField>
<CheckboxField name="subscribe" form={demoContactForm} optional>Subscribe to updates</CheckboxField>
<Button form={demoContactForm}>Send</Button>
</Form>RadioField
zeyo-ui/form/fields: one radio option. Options that share a name form a group, and group holds the selected value, so the options can sit anywhere (a table row, beside other content), not only inside one wrapper. Styled and spaced like CheckboxField.
Rows with the same value count as one lead.
Selected: growth
<RadioField name="plan" value="starter" bind:group={plan}>Starter</RadioField>
<RadioField name="plan" value="growth" bind:group={plan}>Growth</RadioField>
<RadioField name="plan" value="scale" disabled bind:group={plan}>Scale</RadioField>InfoTip
zeyo-ui/popover — a small info icon whose tooltip shows on hover or keyboard focus. Use it for a short explanation next to a label or column header. align='right' makes the tooltip open leftwards, for one placed in the last column.
Match leads by
<InfoTip label="About matching leads">
Rows with the same value count as the same lead.
</InfoTip>Combobox
zeyo-ui/form — type to filter (fuzzy match). See the dedicated Combobox page for multi-select, header/footer snippets, and form-bound usage; SelectField's items/getKey/getLabel/getValue array (also on that page) is the non-searchable alternative.
<Combobox name="priorityDemo" placeholder="Priority" bind:value={priority}>
{#each priorityOptions as opt (opt.value)}
<Option label={opt.label} value={opt.value}>{opt.label}</Option>
{/each}
</Combobox>Search
zeyo-ui/form/primitives — an Input with a search icon baked in. onChange is required; SearchBar doesn't own the filter state itself, the parent does. Matches render in a Popover anchored underneath — position: fixed, so it overlays the page instead of inline content that pushes everything else down as the result count changes. For a search that hits the server, pass debounce (ms) so onChange waits until typing stops — the second field uses debounce=300.
Committed: ""
let query = $state('');
let open = $state(false);
const filtered = $derived(
campaigns.filter((name) => name.toLowerCase().includes(query.trim().toLowerCase()))
);
<div style="position: relative;">
<SearchBar
defaultValue={query}
onChange={(v) => { query = v; open = v.trim() !== ''; }}
placeholder="Search…"
/>
<Popover align="left" width={296} bind:open>
{#each filtered as name}<div>{name}</div>{/each}
</Popover>
</div>
<SearchBar debounce={300} onChange={(v) => (query = v)} placeholder="Debounced (300ms)…" />Input
The standalone, non-form counterpart to TextField — same .form-input styling, plain bindable value. SearchBar is just this with a Search icon and type='search' baked in. Reach for this for any local-state input a remote form doesn't own (filters, modal-local fields, dynamically-named fields a static Field name can't express).
<Input bind:value placeholder="Plain text" />
<Input bind:value icon={Mail} placeholder="jane@company.com" type="email" />FileField
Not wired to a form — reads the file for client-side use (preview, direct upload to R2, …) instead of submitting through FormData.
<FileField onSelect={(files) => console.log(files)} variant="button" />
<FileField onSelect={(files) => console.log(files)} variant="dropzone" />