Form Section

PreviousNext

Inset card section for grouping form fields, headings, actions, and submission feedback.

Request access

Zod errors are passed directly to each form component.

No card required.
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/matos-ui/button";
import { FormGrid } from "@/components/matos-ui/form-grid";
import { FormSection } from "@/components/matos-ui/form-section";
import { InputField } from "@/components/matos-ui/input";
import { Select } from "@/components/matos-ui/select";

const workspaceSchema = z.object({
  email: z.email("Enter a valid work email."),
  teamSize: z.string().min(1, "Choose a team size."),
});

type WorkspaceValues = z.infer<typeof workspaceSchema>;

export function FormWithZodDemo() {
  const {
    control,
    register,
    handleSubmit,
    formState: { errors, isSubmitting, isSubmitSuccessful },
  } = useForm<WorkspaceValues>({
    resolver: zodResolver(workspaceSchema),
    defaultValues: { email: "", teamSize: "" },
    mode: "onBlur",
  });

  async function onSubmit() {
    await new Promise((resolve) => window.setTimeout(resolve, 800));
  }

  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      className="mx-auto w-full max-w-xl"
      noValidate
    >
      <FormSection
        size="compact"
        title="Request access"
        description="Zod errors are passed directly to each form component."
        footer={
          <>
            <span className="text-xs text-muted-foreground">
              {isSubmitSuccessful
                ? "Request ready for review."
                : "No card required."}
            </span>
            <Button
              type="submit"
              size="sm"
              loading={isSubmitting}
              loadingText="Sending..."
            >
              Continue
            </Button>
          </>
        }
      >
        <FormGrid gap="compact">
          <InputField
            label="Work email"
            placeholder="you@company.com"
            required
            error={errors.email?.message}
            {...register("email")}
          />
          <Controller
            name="teamSize"
            control={control}
            render={({ field }) => (
              <Select
                label="Team size"
                required
                options={[
                  { value: "small", label: "1-10 people" },
                  { value: "medium", label: "11-50 people" },
                  { value: "large", label: "51+ people" },
                ]}
                value={field.value || null}
                onValueChange={(nextValue) => field.onChange(nextValue ?? "")}
                name={field.name}
                inputRef={field.ref}
                error={errors.teamSize?.message}
                placeholder="Choose size"
              />
            )}
          />
        </FormGrid>
      </FormSection>
    </form>
  );
}

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/form-section.json

Usage

import { FormSection } from "@/components/matos-ui/form-section"

<FormSection
  title="Account details"
  description="Information used to personalize your workspace."
  footer={<Button type="submit">Continue</Button>}
>
  <FormGrid>{fields}</FormGrid>
</FormSection>

Use size="compact" for embedded panels or size="roomy" when the form is the page focus.