Componentform
Form Field
Field wrapper with label, control, description, and error message with a shake-in animation on error.
Preview
Live Preview
Please enter a valid email address
We'll never share your email
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add form-fieldOr install the full npm package:
npm install bigbulluiUsage
import { FormField } from "@/components/ui/form-field";
<FormField label="Email" htmlFor="email" description="We never share it." error={error} required>
<input id="email" />
</FormField>Source code
form-field.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export type FormFieldProps = {
label?: React.ReactNode;
htmlFor?: string;
description?: React.ReactNode;
error?: string;
required?: boolean;
className?: string;
children?: React.ReactNode;
};
const formFieldShake = `
@keyframes formFieldShake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
20%, 40%, 60%, 80% { transform: translateX(4px); }
}
`;
export function FormField({ label, htmlFor, description, error, required = false, className, children }: FormFieldProps) {
const [showError, setShowError] = React.useState(false);
React.useEffect(() => {
if (error) {
setShowError(true);
}
}, [error]);
return (
<div className={cn("space-y-2", className)}>
<style>{formFieldShake}</style>
{label !== undefined && (
<label htmlFor={htmlFor} className={cn("block text-sm font-medium text-foreground", "motion-reduce:transition-none")}>
{label}
{required && <span className="ml-1 text-[10px] uppercase tracking-[0.15em] text-muted-foreground">*</span>}
</label>
)}
<div className={cn(
"rounded-md border border-input bg-background px-3 py-1.5 transition-all duration-300 motion-reduce:transition-none",
error && "ring-2 ring-ring/20"
)}>
{error && (
<p className="text-sm text-destructive mt-1 animate-[formFieldShake_0.4s_ease-out_both] motion-reduce:animate-none">{error}</p>
)}
{children}
<input
id={htmlFor}
type="text"
className={cn(
"block w-full rounded-md bg-transparent p-1.5 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-input disabled:cursor-not-allowed disabled:opacity-50",
"motion-reduce:transition-none"
)}
aria-label={typeof label === "string" ? label : undefined}
/>
</div>
{description !== undefined && (
<p className={cn("text-xs text-muted-foreground", "motion-reduce:transition-none")}>{description}</p>
)}
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| label | ReactNode | Label content. |
| htmlFor | string | Id of the associated control. |
| description | ReactNode | Helper text. |
| error | string | Error message with shake-in animation. |
| required | boolean | Marks the field required. |