smithery.ai

formedible

Expert knowledge for Formedible - A React form library built on TanStack Form with 22+ field types, multi-page forms, analytics, and type-safe validation

First seen Apr 2, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 14,489 B
  • docs SUMMARY.md 171 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Formedible Skill

Formedible is a DECLARATIVE and SCHEMA-DRIVEN form library.

The schema is the single source of truth. Define forms via fields array configuration, NOT manual JSX.

Use this skill when working with Formedible forms - creating, debugging, or extending functionality.

Quick Start

⚠️ Formedible is DECLARATIVE and SCHEMA-DRIVEN. Do NOT write manual JSX for fields! import { useFormedible } from "@/hooks/use-formedible"; import { z } from "zod"; import { toast } from "sonner";

const schema = z.object({ name: z.string().min(2), email: z.string().email(), });

const { Form } = useFormedible({ schema, fields: [ { name: "name", type: "text", label: "Name" }, { name: "email", type: "email", label: "Email" }, ], formOptions: { defaultValues: { name: "", email: "" }, onSubmit: async ({ value }) => { toast.success("Form submitted!"); console.log(value); }, }, });

return <Form className="space-y-4" />;


## Field Types Quick Reference

| Type | Key Config |
|------|------------|
| `text` | Basic text input |
| `email` | Email validation |
| `password` | Password field |
| `textarea` | `textareaConfig: { rows, showWordCount, maxLength }` |
| `number` | `min, max, step` |
| `date` | `dateConfig: { disablePastDates, disableFutureDates }` |
| `select` | `options: []` (static or function) |
| `radio` | `options: []` |
| `multiSelect` | `multiSelectConfig: { maxSelections, searchable }` |
| `checkbox` | Boolean checkbox |
| `switch` | Toggle switch |
| `rating` | `ratingConfig: { max, allowHalf, icon }` |
| `phone` | `phoneConfig: { format, defaultCountry }` |
| `array` | `arrayConfig: { itemType, minItems, maxItems, sortable, objectConfig }` |

## Key Examples (Self-Contained)

### Multi-Page Form with Dynamic Text

import { useFormedible } from "@/hooks/use-formedible"; import { z } from "zod"; import { toast } from "sonner";

const schema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), plan: z.enum(["basic", "pro"]), });

const { Form } = useFormedible({ schema, fields: [ { name: "firstName", type: "text", label: "First Name", page: 1 }, { name: "lastName", type: "text", label: "Last Name", page: 1 }, { name: "email", type: "email", label: "Email", page: 2, description: "We'll contact {{firstName}} at {{email}}", // Dynamic text! }, { name: "plan", type: "radio", label: "Plan", page: 2, options: [ { value: "basic", label: "Basic - Free" }, { value: "pro", label: "Pro - $9/mo" }, ], }, ], pages: [ { page: 1, title: "Personal Info", description: "Tell us about yourself" }, { page: 2, title: "Contact", description: "How can we reach you, {{firstName}}?" }, ], progress: { showSteps: true, showPercentage: true }, formOptions: { defaultValues: { firstName: "", lastName: "", email: "", plan: "basic" as const, }, onSubmit: async ({ value }) => { toast.success("Registered!"); }, }, });

return <Form className="space-y-4" />;


### Conditional Fields AND Pages

const schema = z.object({ applicationType: z.enum(["individual", "business"]), firstName: z.string().optional(), companyName: z.string().optional(), });

const { Form } = useFormedible({ schema, fields: [ { name: "applicationType", type: "radio", label: "Application Type", page: 1, options: [ { value: "individual", label: "Individual" }, { value: "business", label: "Business" }, ], }, { name: "firstName", type: "text", label: "First Name", page: 2, conditional: (values: any) => values.applicationType === "individual", }, { name: "companyName", type: "text", label: "Company Name", page: 3, conditional: (values: any) => values.applicationType === "business", }, ], pages: [ { page: 1, title: "Type" }, { page: 2, title: "Personal Info", conditional: (values: any) => values.applicationType === "individual", }, { page: 3, title: "Business Info", conditional: (values: any) => values.applicationType === "business", }, ], formOptions: { defaultValues: { applicationType: "individual" as const, firstName: "", companyName: "", }, onSubmit: async ({ value }) => { console.log(value); }, }, });


### Tabbed Form

const schema = z.object({ firstName: z.string(), theme: z.enum(["light", "dark"]), notifications: z.boolean(), });

const { Form } = useFormedible({ schema, fields: [ { name: "firstName", type: "text", label: "Name", tab: "personal" }, { name: "theme", type: "select", label: "Theme", tab: "preferences", options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, ], }, { name: "notifications", type: "switch", label: "Enable Notifications", tab: "preferences", }, ], tabs: [ { id: "personal", label: "Personal Info", description: "About you" }, { id: "preferences", label: "Preferences", description: "Settings" }, ], formOptions: { defaultValues: { firstName: "", theme: "light" as const, notifications: true, }, onSubmit: async ({ value }) => console.log(value), }, });


### Dynamic Options (Dependent Fields)

const schema = z.object({ country: z.string(), state: z.string(), });

const { Form } = useFormedible({ schema, fields: [ { name: "country", type: "select", label: "Country", options: [ { value: "us", label: "United States" }, { value: "ca", label: "Canada" }, ], }, { name: "state", type: "select", label: "State/Province", options: (values: any) => { if (values.country === "us") { return [ { value: "ca", label: "California" }, { value: "ny", label: "New York" }, ]; } if (values.country === "ca") { return [ { value: "on", label: "Ontario" }, { value: "qc", label: "Quebec" }, ]; } return []; }, }, ], formOptions: { defaultValues: { country: "", state: "" }, onSubmit: async ({ value }) => console.log(value), }, });


### Array Fields with Nested Objects

const schema = z.object({ teamMembers: z.array( z.object({ name: z.string().min(1), email: z.string().email(), role: z.enum(["dev", "design", "pm"]), }) ).min(1), });

const { Form } = useFormedible({ schema, fields: [ { name: "teamMembers", type: "array", label: "Team Members", section: { title: "Team Composition", description: "Add your team", }, arrayConfig: { itemType: "object", itemLabel: "Team Member", minItems: 1, maxItems: 10, sortable: true, addButtonLabel: "Add Member", defaultValue: { name: "", email: "", role: "dev", }, objectConfig: { fields: [ { name: "name", type: "text", label: "Name" }, { name: "email", type: "email", label: "Email" }, { name: "role", type: "select", label: "Role", options: [ { value: "dev", label: "Developer" }, { value: "design", label: "Designer" }, { value: "pm", label: "Product Manager" }, ], }, ], }, }, }, ], formOptions: { defaultValues: { teamMembers: [{ name: "", email: "", role: "dev" as const }], }, onSubmit: async ({ value }) => console.log(value), }, });


### Analytics with Proper Memoization

import React from "react";

const schema = z.object({ email: z.string().email(), });

// MUST use useCallback for analytics callbacks! const onFieldFocus = React.useCallback((fieldName: string, timestamp: number) => { console.log(Field "${fieldName}" focused at, timestamp); }, []);

const onFieldBlur = React.useCallback((fieldName: string, timeSpent: number) => { console.log(Field "${fieldName}" completed in ${timeSpent}ms); }, []);

const onFormComplete = React.useCallback((timeSpent: number, data: any) => { console.log(Form completed in ${timeSpent}ms, data); toast.success("Form completed!"); }, []);

// MUST useMemo the analytics config const analyticsConfig = React.useMemo( () => ({ onFieldFocus, onFieldBlur, onFormComplete, }), [onFieldFocus, onFieldBlur, onFormComplete] );

const { Form } = useFormedible({ schema, fields: [ { name: "email", type: "email", label: "Email" }, ], analytics: analyticsConfig, formOptions: { defaultValues: { email: "" }, onSubmit: async ({ value }) => console.log(value), }, });


### Rating Field with Config

const schema = z.object({ satisfaction: z.number().min(1).max(5), improvements: z.string().optional(), });

const { Form } = useFormedible({ schema, fields: [ { name: "satisfaction", type: "rating", label: "How satisfied are you?", ratingConfig: { max: 5, allowHalf: false, showValue: true, }, }, { name: "improvements", type: "textarea", label: "What can we improve?", conditional: (values: any) => values.satisfaction < 4, textareaConfig: { rows: 4, showWordCount: true, maxLength: 500, }, }, ], formOptions: { defaultValues: { satisfaction: 5, improvements: "" }, onSubmit: async ({ value }) => console.log(value), }, });


### Textarea with Configuration

const schema = z.object({ description: z.string().min(20).max(500), });

const { Form } = useFormedible({ schema, fields: [ { name: "description", type: "textarea", label: "Description", textareaConfig: { rows: 4, showWordCount: true, maxLength: 500, }, }, ], formOptions: { defaultValues: { description: "" }, onSubmit: async ({ value }) => console.log(value), }, });


## Critical Patterns

### 0. FORMEDIBLE IS DECLARATIVE AND SCHEMA DRIVEN (MOST IMPORTANT!)

// ❌ WRONG - Never write manual JSX for fields! <form> <input name="name" /> <input name="email" /> </form>

// ✅ CORRECT - Define fields declaratively via configuration! const { Form } = useFormedible({ schema: z.object({ name: z.string(), email: z.string().email(), }), fields: [ { name: "name", type: "text", label: "Name" }, { name: "email", type: "email", label: "Email" }, ], formOptions: { onSubmit: async ({ value }) => console.log(value), }, });


**THE SCHEMA IS THE SINGLE SOURCE OF TRUTH.** Field names in `fields` MUST match schema keys exactly!

### 1. Always Use className on Form

<Form className="space-y-4" />


### 2. Toast Notifications

import { toast } from "sonner";

onSubmit: async ({ value }) => { toast.success("Success!", { description: "Your data was saved", }); }


### 3. Use `as const` for Enums

defaultValues: { plan: "basic" as const, // ✅ role: "admin" as const, // ✅ }


### 4. Dynamic Options = Function

// ❌ Wrong options: [{ value: "a", label: "A" }]

// ✅ Correct options: (values) => { if (values.category === "tech") return techOptions; return []; }


### 5. Conditional Returns Boolean

// ❌ Wrong conditional: (values) => { if (values.type === "business") return true; }

// ✅ Correct conditional: (values) => values.type === "business"


### 6. Analytics Must Be Memoized

const callback = React.useCallback((...) => { ... }, []); const analytics = React.useMemo(() => ({ callback }), [callback]);


## Build Workflow (CRITICAL!)

**PACKAGES ARE SOURCE OF TRUTH**

1. Edit: `packages/formedible/src/...`
2. Build: `npm run build:pkg`
3. Sync: `node scripts/quick-sync.js`
4. Build web: `npm run build:web`
5. Sync components: `npm run sync-components`
6. Build web: `npm run build:web`

**NEVER edit web app files directly!**

## Common Issues

| Issue | Fix |
|-------|-----|
| Field not showing | Check field type in `field-registry.tsx` |
| Dynamic options not updating | Use function: `options: (values) => {...}` |
| Validation not showing | Schema names must match field names exactly |
| Conditional always hidden | Return boolean, never undefined |
| Analytics not firing | Use `React.useCallback` + `React.useMemo` |
| Pages not working | Pages start at 1, must be sequential |

## Type Safety

const schema = z.object({ name: z.string(), age: z.number(), });

type FormValues = z.infer<typeof schema>;

const { Form } = useFormedible<FormValues>({ schema, formOptions: { defaultValues: { name: "", // Type-safe age: 0, // Type-safe }, }, });


## File Structure Reference

packages/formedible/src/ ├── hooks/use-formedible.tsx # Main hook ├── components/formedible/ │ ├── fields/ # All 22 field components │ ├── layout/ # FormGrid, FormTabs, etc. │ └── ui/ # Radix UI primitives ├── lib/formedible/ │ ├── types.ts # TypeScript interfaces │ ├── field-registry.tsx # Field type mapping │ └── template-interpolation.ts # Dynamic text resolution


## Adding New Field Types

1. Create: `packages/formedible/src/components/formedible/fields/my-field.tsx`
2. Use `BaseFieldWrapper` for consistency
3. Add type to `packages/formedible/src/lib/formedible/types.ts`
4. Register in `packages/formedible/src/lib/formedible/field-registry.tsx`
5. Add to `packages/formedible/registry.json`

See [FIELD_TEMPLATES.md](references/FIELD_TEMPLATES.md) for templates.