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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "formedible" with this command: npx skills add dimitrigilbert/formedible/dimitrigilbert-formedible-formedible

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

```tsx
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

IssueFix
Field not showingCheck field type in field-registry.tsx
Dynamic options not updatingUse function: options: (values) => {...}
Validation not showingSchema names must match field names exactly
Conditional always hiddenReturn boolean, never undefined
Analytics not firingUse React.useCallback + React.useMemo
Pages not workingPages 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 for templates.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

not-ai-writer

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

agents-md

No summary provided by upstream source.

Repository SourceNeeds Review
General

gh-profile

No summary provided by upstream source.

Repository SourceNeeds Review
General

better-t-stack

No summary provided by upstream source.

Repository SourceNeeds Review