Responsive Form Grid
Build complex, responsive forms without manually writing <Row><Col>...</Col></Row> boilerplate, with built-in conditional field visibility.
What Problem This Solves
You need forms that:
- Automatically layout fields in responsive columns (mobile: 1 col, desktop: 2-3 cols)
- Support multiple input types (text, select, date, numeric) via a declarative API
- Show/hide fields based on other field values
- Work as both regular forms and filter panels
Prerequisites
antdreact-number-format(for numeric inputs)
Files
responsive-col.tsx
tsx
import { Col, type ColProps } from "antd";
export const ResponsiveCol = (props: ColProps & { fullWidth?: boolean }) => {
const { fullWidth, ...colProps } = props;
return (
<Col
xs={24}
md={fullWidth ? 24 : 12}
lg={fullWidth ? 24 : 8}
xl={fullWidth ? 24 : 6}
{...colProps}
/>
);
};input-item.tsx
tsx
import { Form, type FormItemProps } from "antd";
import { ResponsiveCol } from "./responsive-col";
export type InputItemProps = FormItemProps & {
fullWidth?: boolean;
};
export const InputItem = ({ fullWidth, children, ...props }: InputItemProps) => (
<ResponsiveCol fullWidth={fullWidth}>
<Form.Item {...props}>{children}</Form.Item>
</ResponsiveCol>
);input-grid.tsx
tsx
import { Row, Skeleton } from "antd";
import { lazy, Suspense } from "react";
import { InputItem } from "./input-item";
const Input = lazy(() => import("antd").then((m) => ({ default: m.Input })));
const Select = lazy(() => import("antd").then((m) => ({ default: m.Select })));
const DatePicker = lazy(() => import("antd").then((m) => ({ default: m.DatePicker })));
const NumericFormatInput = lazy(() => import("./numeric-format-input"));
const inputComponentMap = {
text: Input,
select: Select,
datePicker: DatePicker,
numeric: NumericFormatInput,
} as const;
export type InputDefinition = {
inputType: keyof typeof inputComponentMap;
name: string;
label: string;
inputProps?: Record<string, unknown>;
fullWidth?: boolean;
rules?: Array<Record<string, unknown>>;
};
type InputGridProps = {
inputs: InputDefinition[];
};
export const InputGrid = ({ inputs }: InputGridProps) => {
return (
<Row gutter={[16, 16]}>
<Suspense fallback={<Skeleton active />}>
{inputs.map((def) => {
const Component = inputComponentMap[def.inputType];
return (
<InputItem
key={def.name}
name={def.name}
label={def.label}
rules={def.rules}
fullWidth={def.fullWidth}
>
<Component {...(def.inputProps ?? {})} />
</InputItem>
);
})}
</Suspense>
</Row>
);
};numeric-format-input.tsx
tsx
import { Input } from "antd";
import { NumericFormat } from "react-number-format";
export const NumericFormatInput = (props: React.ComponentProps<typeof NumericFormat>) => (
<NumericFormat customInput={Input} thousandSeparator="," decimalSeparator="." {...props} />
);Usage
tsx
import { Form, Button } from "antd";
import { InputGrid } from "@/lib/components/data-entry/input-grid";
const userFormInputs = [
{ inputType: "text" as const, name: "name", label: "Full Name", rules: [{ required: true }] },
{
inputType: "text" as const,
name: "email",
label: "Email",
rules: [{ required: true, type: "email" }],
},
{
inputType: "select" as const,
name: "role",
label: "Role",
inputProps: { options: roleOptions },
},
{ inputType: "datePicker" as const, name: "birthDate", label: "Birth Date" },
{ inputType: "numeric" as const, name: "salary", label: "Salary", fullWidth: true },
];
export function UserForm() {
const [form] = Form.useForm();
return (
<Form form={form} onFinish={(values) => console.log(values)}>
<InputGrid inputs={userFormInputs} />
<Button type="primary" htmlType="submit">
Save
</Button>
</Form>
);
}Conditional Visibility
For fields that show/hide based on other values, wrap InputGrid and use Form.useWatch:
tsx
export function ConditionalForm() {
const [form] = Form.useForm();
const role = Form.useWatch("role", form);
const inputs = [
{ inputType: "text", name: "name", label: "Name" },
...(role === "admin" ? [{ inputType: "text", name: "adminCode", label: "Admin Code" }] : []),
];
return (
<Form form={form}>
<InputGrid inputs={inputs} />
</Form>
);
}Variations
As Filter Panel
Use the same InputGrid for table filters by adding paramKey to each input definition and wiring to handleUpdateFilter from Table with Synced Pagination.
With antd-style CSS-in-JS
If using antd-style (Emotion-based), wrap InputGrid in a styled container for consistent spacing.