Components
Loading preview...
A form wrapper component that simplifies validation and submission with flex column layout and gap between fields.
npx shadcn@latest add https://21st.dev/r/coss.com/form"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { Form } from "@/components/ui/component";
import { Field as FieldPrimitive } from "@base-ui/react/field";
import { Input as InputPrimitive } from "@base-ui/react/input";
import { cn } from "@/lib/utils";
import { Loader2Icon } from "lucide-react";
function Field({
className,
...props
}: FieldPrimitive.Root.Props) {
return (
<FieldPrimitive.Root
className={cn("flex flex-col items-start gap-2", className)}
data-slot="field"
{...props}
/>
);
}
function FieldLabel({
className,
...props
}: FieldPrimitive.Label.Props) {
return (
<FieldPrimitive.Label
className={cn(
"inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4",
className,
)}
data-slot="field-label"
{...props}
/>
);
}
function FieldError({
className,
...props
}: FieldPrimitive.Error.Props) {
return (
<FieldPrimitive.Error
className={cn("text-destructive-foreground text-xs", className)}
data-slot="field-error"
{...props}
/>
);
}
function Input({
className,
...props
}: React.ComponentProps<typeof InputPrimitive>) {
return (
<span
className={cn(
"relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
data-slot="input-control"
>
<InputPrimitive
className="h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none [transition:background-color_5000000s_ease-in-out_0s] placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5"
data-slot="input"
{...props}
/>
</span>
);
}
function Button({
className,
children,
loading = false,
disabled,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { loading?: boolean }) {
return (
<button
className={cn(
"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow sm:text-sm",
"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90",
"h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64",
loading && "select-none text-transparent",
className,
)}
disabled={loading || disabled}
{...props}
>
{loading ? (
<>
<span className="invisible inline-flex items-center gap-2">{children}</span>
<Loader2Icon
aria-label="Loading"
className="pointer-events-none absolute animate-spin text-primary-foreground"
role="status"
/>
</>
) : (
children
)}
</button>
);
}
export default function FormDefault() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
alert(`Email: ${formData.get("email") || ""}`);
};
return (
<div className="flex items-center justify-center w-full min-h-screen bg-background p-8">
<Form className="max-w-64" onSubmit={onSubmit}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input placeholder="you@example.com" required type="email" />
<FieldError>Please enter a valid email.</FieldError>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
</div>
);
}