A type-safe form library for Svelte 5 with SvelteKit remote functions.
blur, change, or submitremoteFunctions: true in confignpm install @samuel-charpentier/sform
Enable remote functions in svelte.config.js:
export default {
kit: {
experimental: {
remoteFunctions: true
}
}
};
Create a .remote.ts file with your form schema and handler:
// src/routes/auth.remote.ts
import * as v from 'valibot';
import { form } from '@sveltejs/kit/remote';
const loginSchema = v.object({
username: v.pipe(v.string(), v.minLength(3, 'Username must be at least 3 characters')),
_password: v.pipe(v.string(), v.minLength(8, 'Password must be at least 8 characters'))
});
export const login = form(loginSchema, async ({ username, _password }) => {
// Your authentication logic here
return { success: true, message: 'Welcome!' };
});
<script lang="ts">
import { Sform, Sfield, Sbutton } from '@samuel-charpentier/sform';
import { login } from './auth.remote.ts';
</script>
<Sform form={login} validateOn="blur">
<Sfield name="username" type="text" label="Username" />
<Sfield name="_password" type="password" label="Password" />
<Sbutton label="Login" />
</Sform>
<Sform>Wrapper component that provides form context to all child fields.
<Sform form={remoteForm} validateOn="blur" class="my-form">
<!-- Sfield components here -->
</Sform>
| Prop | Type | Default | Description |
|---|---|---|---|
form |
RemoteForm |
required | Remote form object from form() API |
validateOn |
'blur' | 'change' | 'submit' |
'blur' |
When to validate and show errors |
class |
string |
undefined |
CSS class for form element |
Validate Modes:
blur - Validate and show errors after leaving field (default)change - Validate and show errors as soon as value changessubmit - Validate and show all errors only after submit attempt<Sfield>Smart field component with type-safe props based on input type.
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
required | Field name (must match schema) |
type |
InputType |
required | Input type |
label |
string |
undefined |
Field label |
placeholder |
string |
undefined |
Placeholder text |
disabled |
boolean |
false |
Disable the field |
readonly |
boolean |
false |
Make field readonly |
validateOn |
ValidateOn |
inherited | Override form validateOn |
class |
SfieldClasses | string |
undefined |
CSS classes |
<Sfield name="email" type="email" label="Email" placeholder="[email protected]" />
<Sfield name="search" type="search" label="Search" />
<Sfield name="phone" type="tel" label="Phone" />
<Sfield name="website" type="url" label="Website" prefix="https://" />
Supported text types: text, email, tel, url, search, date, datetime-local, time, month, week, color, file, hidden
| Prop | Type | Default | Description |
|---|---|---|---|
prefix |
string | Snippet |
undefined |
Content before input |
suffix |
string | Snippet |
undefined |
Content after input |
<Sfield name="_password" type="password" label="Password" />
<Sfield name="_password" type="password" label="Password" showToggle={false} />
| Prop | Type | Default | Description |
|---|---|---|---|
showToggle |
boolean |
true |
Show eye icon to toggle visibility |
<Sfield name="age" type="number" label="Age" min={0} max={150} step={1} />
<Sfield name="price" type="number" label="Price" prefix="$" suffix="USD" align="end" />
<Sfield name="quantity" type="number" label="Qty" showControls={false} maxDecimals={0} />
| Prop | Type | Default | Description |
|---|---|---|---|
min |
number | string |
undefined |
Minimum value |
max |
number | string |
undefined |
Maximum value |
step |
number | string |
undefined |
Step increment |
prefix |
string | Snippet |
undefined |
Content before input (e.g., "$") |
suffix |
string | Snippet |
undefined |
Content after input (e.g., "USD") |
showControls |
boolean |
true |
Show spinner controls |
align |
'start' | 'end' |
'start' |
Text alignment |
maxDecimals |
number |
undefined |
Max decimal places (0 = integers only) |
<Sfield name="bio" type="textarea" label="Bio" placeholder="Tell us about yourself" />
<Sfield
name="country"
type="select"
label="Country"
options={[
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' }
]}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
options |
SelectOption[] | string[] |
required | Select options |
<Sfield name="subscribe" type="checkbox" label="Subscribe to newsletter" />
<Sfield
name="plan"
type="radio"
label="Plan"
options={[
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'enterprise', label: 'Enterprise' }
]}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
options |
SelectOption[] | string[] |
undefined |
Radio options for groups |
<Sfield name="volume" type="range" label="Volume" min={0} max={100} step={5} showValue />
| Prop | Type | Default | Description |
|---|---|---|---|
min |
number | string |
0 |
Minimum value |
max |
number | string |
100 |
Maximum value |
step |
number | string |
1 |
Step increment |
showValue |
boolean |
false |
Show current value |
formatValue |
(value: number) => string |
undefined |
Format displayed value |
<Sfield name="notifications" type="toggle" label="Enable Notifications" />
<Sfield name="darkMode" type="toggle" label="Theme" onLabel="Dark" offLabel="Light" />
| Prop | Type | Default | Description |
|---|---|---|---|
onLabel |
string |
undefined |
Label when on |
offLabel |
string |
undefined |
Label when off |
checkedValue |
string |
'true' |
Value when checked |
uncheckedValue |
string |
'false' |
Value when unchecked |
<Sfield
name="theme"
type="toggle-options"
label="Theme"
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'auto', label: 'Auto' }
]}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
options |
ToggleOption[] | string[] |
required | Toggle options |
multiple |
boolean |
false |
Allow multiple selections |
<Sfield name="phone" type="masked" label="Phone" mask="(###) ###-####" />
<Sfield name="creditCard" type="masked" label="Credit Card" mask="#### #### #### ####" />
<Sfield name="ssn" type="masked" label="SSN" mask="###-##-####" />
| Prop | Type | Default | Description |
|---|---|---|---|
mask |
string |
required | Mask pattern |
maskPlaceholder |
string |
'_' |
Placeholder character |
showMaskPlaceholder |
boolean |
false |
Show full mask with placeholders |
unmaskValue |
boolean |
true |
Store unmasked value |
prefix |
string | Snippet |
undefined |
Content before input |
suffix |
string | Snippet |
undefined |
Content after input |
Mask Tokens:
# or 9 - Numeric (0-9)a - Alphabetic (a-z, A-Z)A - Alphabetic uppercase* - Alphanumeric<Sbutton>Stateful submit button that reacts to form state.
<Sbutton label="Submit" class="my-button" />
<!-- With custom state snippets -->
<Sbutton class="submit-btn">
{#snippet defaultState(state)}
Submit Form
{/snippet}
{#snippet pendingState(state)}
Submitting...
{/snippet}
{#snippet successState(state)}
✓ Success!
{/snippet}
{#snippet errorState(state)}
Fix Errors
{/snippet}
</Sbutton>
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
'Submit' |
Button text |
buttonType |
'submit' | 'reset' | 'button' |
'submit' |
Button type |
class |
string |
undefined |
CSS class |
disabled |
boolean |
false |
Disable button |
onsubmit |
() => void | Promise<void> |
undefined |
Callback before validation/submit |
defaultState |
Snippet |
undefined |
Default state snippet |
pendingState |
Snippet |
undefined |
Pending state snippet |
successState |
Snippet |
undefined |
Success state snippet |
errorState |
Snippet |
undefined |
Error state snippet |
Sfield adds these classes automatically:
.sform-field - Wrapper element.sform-label - Label element.sform-input - Input element.sform-messages - Error messages container.sform-field-error - Added to wrapper when field has errors<!-- String class applies to wrapper -->
<Sfield name="email" type="email" class="my-field" />
<!-- Object for granular control -->
<Sfield
name="email"
type="email"
class={{
wrapper: 'field-wrapper',
label: 'field-label',
input: 'field-input',
messages: 'field-errors'
}}
/>
Sform uses preflight validation with Valibot schemas. Native browser validation (required, minlength, pattern) is disabled to allow showing all errors at once on submit.
import * as v from 'valibot';
const signupSchema = v.object({
email: v.pipe(v.string(), v.email('Please enter a valid email')),
_password: v.pipe(
v.string(),
v.minLength(8, 'Password must be at least 8 characters'),
v.regex(/[A-Z]/, 'Password must contain an uppercase letter'),
v.regex(/[0-9]/, 'Password must contain a number')
),
age: v.pipe(v.number(), v.minValue(18, 'Must be at least 18 years old'))
});
Sform uses TypeScript discriminated unions to provide type-safe props for each input type:
// ✅ TypeScript knows 'showToggle' is only valid for password type
<Sfield name="_password" type="password" showToggle={false} />
// ✅ TypeScript knows 'options' is required for select type
<Sfield name="country" type="select" options={countries} />
// ✅ TypeScript knows 'min', 'max', 'step' are valid for number type
<Sfield name="age" type="number" min={0} max={150} />
// ❌ TypeScript error: 'showToggle' doesn't exist on text type
<Sfield name="username" type="text" showToggle />
# Install dependencies
npm install
# Start dev server
npm run dev
# Run tests
npm test
# Build library
npm run package
MIT