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.
Install the components
In a React or Next.js project that already uses shadcn:
npm install @usefillo/react
npx shadcn@latest add input textareaImport @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:
"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:
| Prop | What it carries |
|---|---|
field | The schema field, including id, kind, label, and required |
value | The current answer |
setValue | Updates the answer |
error | The validation message, or undefined |
ids | inputId, errorId, and name for this form instance |
api | Form 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
- Set
NEXT_PUBLIC_FILLO_KEYand allow your dev origin in the Fillo project. - Publish the form from the dashboard or with
fillo publish. - Submit one message and watch the button's pending state.
- Open Responses in Fillo and find the message.
- Submit empty, then an invalid email, then with the network off.
- Check that each case shows an error and keeps the typed message.
- 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.
Related
- Custom layout example: Compare native controls with a host-owned layout.
- Custom UI reference: Own the full layout with
FilloProviderwhen field overrides aren't enough. - Send in-app product feedback to Notion: Give the team a triage inbox for these answers.
- Build an in-app feedback form in Next.js: Start from a complete settings page.