Skip to main content

Build a shadcn form that saves responses

Use shadcn Input and Textarea as the visible controls while Fillo runs validation, publishing, and saved responses in React or Next.js.

Build this with your agent. Copy a prompt that hands this guide to Claude Code, Codex, Cursor, or any coding agent.

Use your shadcn Input and Textarea as the visible controls while Fillo runs validation, conditional fields, and response storage. You end with a feedback form in a React or Next.js app, with no second form-state library.

Try the preview

Submit it empty to see validation, then type a message and submit again. This preview stays in your browser and saves nothing.

Interactive preview. Answers stay in this page and are never submitted.

Product feedback

Powered by Fillo

Install the components

In a React or Next.js project that already uses shadcn:

bash
npm install @usefillo/react
npx shadcn@latest add input textarea

Import @usefillo/react/styles.css once in your root layout. It styles the layout around your fields and the submit button. Your shadcn components keep their own styles. New to shadcn? Start with its installation docs.

Connect shadcn to Fillo

Add this Client Component:

tsx
"use client";

import { useId, type ChangeEvent } from "react";
import { createClient, defineForm, FilloForm, type FieldComponentProps } from "@usefillo/react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";

const client = createClient({ key: process.env.NEXT_PUBLIC_FILLO_KEY! });
const feedback = defineForm({
  id: "shadcn-product-feedback",
  title: "Product feedback",
  pages: [{ id: "feedback", blocks: [
    { id: "message", kind: "long_text", label: "What should we improve?", required: true },
    { id: "email", kind: "email", label: "Email for a reply" },
  ] }],
  settings: { submitLabel: "Send feedback" },
});

function ShadcnTextField({ field, value, setValue, error, api, ids }: FieldComponentProps) {
  const fallback = useId();
  const inputId = ids?.inputId ?? fallback;
  const errorId = ids?.errorId ?? `${fallback}-error`;
  const props = {
    id: inputId,
    name: ids?.name ?? field.id,
    value: typeof value === "string" ? value : "",
    onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
      setValue(event.target.value),
    required: field.required,
    disabled: api.status === "submitting",
    "aria-invalid": Boolean(error),
    "aria-describedby": error ? errorId : undefined,
  };
  return (
    <div className="grid gap-2">
      <label htmlFor={inputId}>{field.label}{field.required ? " *" : ""}</label>
      {field.kind === "long_text"
        ? <Textarea {...props} rows={4} />
        : <Input {...props} type={field.kind === "email" ? "email" : "text"} />}
      {error ? <p id={errorId} role="alert">{error}</p> : null}
    </div>
  );
}

const components = { long_text: ShadcnTextField, email: ShadcnTextField };

export function ProductFeedback() {
  return <FilloForm form={feedback} client={client} components={components} />;
}

On Vite, read the key from import.meta.env.VITE_FILLO_KEY instead. Only the publishable pk_ key belongs in browser code.

Fillo passes each field component these props:

PropWhat it carries
fieldThe schema field, including id, kind, label, and required
valueThe current answer
setValueUpdates the answer
errorThe validation message, or undefined
idsinputId, errorId, and name for this form instance
apiForm state, such as api.status

Forward ids so two forms on one page don't share label targets. This example overrides text and email fields only. Fillo's standard renderer keeps handling files, conditional sections, and every other field kind.

Publish and check it saves

  1. Set NEXT_PUBLIC_FILLO_KEY and allow your dev origin in the Fillo project.
  2. Publish the form from the dashboard or with fillo publish.
  3. Submit one message and watch the button's pending state.
  4. Open Responses in Fillo and find the message.
  5. Submit empty, then an invalid email, then with the network off.
  6. Check that each case shows an error and keeps the typed message.
  7. Test the keyboard path and a narrow screen.

Keep the message and email IDs stable once responses exist. For a preview that never saves, replace client={client} with renderOnly.

Why publish first and check Responses

In a claimed workspace, a schema synced from the browser arrives as a draft. Nothing reaches respondents until someone publishes it.

onSubmitted proves the request returned. The row in Responses proves Fillo stored the answer. Field IDs name the response columns, exports, and webhook keys, so renaming one splits your data.

This page for agents: /guides/shadcn-feedback-form.md · index at /llms.txt

Updated

Was this page helpful?