Form patterns for TungShadcn. Use when building forms with React Hook Form + Zod.
https://react-19.octung112.workers.dev/llms.txtsrc/stories/ (input.stories.tsx, select.stories.tsx, etc.)Unlike vanilla shadcn/ui (FormItem/FormLabel/FormControl), TungShadcn uses a single formComposition prop:
// TungShadcn pattern
<InputForm
control={control}
name="email"
formComposition={{
label: "Email",
description: "Help text",
iconLeft: <Mail />,
iconRight: <Check />,
prefix: <span>$</span>,
suffix: <span>USD</span>,
inputClear: true,
variant: "default", // "white" | "ghost" | "inline"
size: "default", // "sm" | "lg"
labelPosition: "vertical", // "horizontal"
}}
/>
-my-form/
schema.ts # Zod validation only
context.tsx # useForm + providers
form.tsx # UI only
import { z } from "zod"
export const FormSchema = z.object({
name: z.string().min(1),
email: z.email(),
})
import { Form } from "@/components/ui/form/form"
import { ZodSchemaProvider } from "@/components/ui/form/zod-schema-context"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { FormSchema } from "./schema"
export function MyFormContext({ children }: { children: React.ReactNode }) {
const form = useForm({
resolver: zodResolver(FormSchema),
defaultValues: { name: "", email: "" },
})
return (
<ZodSchemaProvider schema={FormSchema}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>{children}</form>
</Form>
</ZodSchemaProvider>
)
}
import { InputForm } from "@/components/ui/input/input-form"
import { useFormContext } from "react-hook-form"
export function MyFormView() {
const { control } = useFormContext()
return (
<>
<InputForm
control={control}
name="name"
formComposition={{ label: "Name" }}
/>
<InputForm
control={control}
name="email"
type="email"
formComposition={{ label: "Email" }}
/>
<Button type="submit">Submit</Button>
</>
)
}
| Need | Use |
|---|---|
| Text input | InputForm |
| Number input | InputNumberForm |
| Textarea | TextareaForm |
| Select | SelectForm |
| Multi-select | MultiSelectForm |
| Tags | InputTagForm |
| Autocomplete | InputAutoCompleteForm |
| Date picker | DatePickerForm |
| Date range | DateRangePickerForm |
| Checkbox | CheckboxForm |
| Checkbox group | CheckboxGroupForm |
| Checkbox tree | CheckboxTreeForm |
| Radio group | RadioGroupForm |
| Switch | SwitchForm |
| Rating | RatingForm |
| Signature | SignaturePadForm |
| OTP | InputOTPForm |
| Slider | SliderForm |
| File upload | FileUploadForm |
ZodSchemaProvider auto-detects required fields from Zod schema:
// schema.ts
const schema = z.object({
email: z.email(), // Required → shows *
nickname: z.string().optional(), // Optional → no *
})
// form.tsx - No need to pass requiredSymbol
<InputForm name="email" formComposition={{ label: "Email" }} />
// Automatically shows * after label
formComposition={{
// Label
label: "Field Label", // Shows in label
requiredSymbol: true, // Auto from Zod
// Help text
description: "Below field",
subDescription: "0/100", // Right side (e.g., char count)
// Layout
labelPosition: "vertical", // "horizontal"
layout: { // For horizontal
leftColClass: "md:col-span-3",
rightColClass: "md:col-span-9",
},
// Icons (inside input)
iconLeft: <SearchIcon />,
iconRight: <ChevronDown />,
// Prefix/Suffix (inside container)
prefix: <span>$</span>,
suffix: <span>USD</span>,
// Prefix/Suffix (outside container)
prefixOutside: <Button>...</Button>,
suffixOutside: <Button>Check</Button>,
// Clear button
inputClear: true,
clearWhenNotFocus: false,
onClear: () => {},
// Styling
variant: "default", // "white" | "ghost" | "inline"
size: "default", // "sm" | "lg"
// Error
showErrorMsg: true,
customError: "Override error",
}}
Do:
*Form components inside formsZodSchemaProvider for auto required detectionuseForm in context, not view componentformComposition.label for accessibilityDon't:
Input/Select inside formsrequiredSymbol (let Zod handle it)