Skip to main content

Build a shadcn feedback form with Fillo

Keep shadcn inputs in your React app while Fillo handles the schema, validation, publishing, and saved responses.

Your app already has shadcn components. Use them for the visible controls and let Fillo run validation, conditional fields, publishing, and response storage. You don't need a second form-state library for this example.

Try the controls

Submit the empty preview to see validation, then enter a message and submit again. This preview stays local; the connected version below saves responses after you publish it.

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

Product feedback

Powered by Fillo

Install the components

In an existing shadcn React or Next.js project:

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

Import @usefillo/react/styles.css once in your app's root stylesheet entry or layout. It styles the surrounding Fillo layout and submit button; your shadcn components keep their own styles. See shadcn's component documentation if your project hasn't initialized shadcn yet.

Connect shadcn to the form engine

Add this client component. The field renderer receives the current value, validation error, setter, and instance-specific IDs from Fillo. Forward the IDs so two mounted forms don't share label targets.

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} />;
}

For Vite, read the public key from import.meta.env.VITE_FILLO_KEY instead. Use a project publishable pk_ key in browser code; keep management tokens and identity secrets on the server.

The overrides above cover only text and email fields. Keep Fillo's standard renderer for files, conditional sections, and field kinds you haven't implemented. If you add descriptions or custom controls, carry their help text, keyboard behavior, and accessible relationships through too.

Publish and prove it saves

  1. Set the public key and allow your development origin in the Fillo project.
  2. Mount the component. Review the staged schema in Fillo, then publish it.
  3. Submit one message from the app. Check that the submit button shows pending state and a successful save.
  4. Open Responses and find the saved message. A success callback alone isn't your end-to-end check.
  5. Submit an empty message, an invalid email, and a valid message with the network disconnected. Verify field errors, submission errors, and retry without losing the typed message.
  6. Test keyboard focus and mobile width. Keep the message and email IDs stable after collecting responses.

For a local-only variant, replace client={client} with renderOnly. Label it as a preview: it cannot save responses or upload files.

Send feedback where the team works

Connect the published form to a Notion feedback inbox or an n8n workflow. Your app keeps the UI while teammates work with the responses.

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

Updated

Was this page helpful?