Formular-Layouts
Komplette Formulare aus den Kit-Feldern: Kontakt, Bewerbung und Einstellungen, mit Abschnitten, Raster und Aktionen.
Eine von 9 Components in Forms und Inputs
Details
Was Formular-Layouts mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Forms und Inputs
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/form-layout.tsx
- Bringt mit
- utilskit
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle FormSectionProps aus components/ui/form-layout.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
titlePflichtReact.ReactNodedescriptionoptionalReact.ReactNodelayoutoptional'side' | 'top'side: title in a column on the left from desktop up. top: title above the fields.
childrenPflichtReact.ReactNode
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Nutzt die Rolle alert.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 112 Zeilen
'use client';
import { useEffect, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useInView } from '@/lib/kit';
/*
* The frame of a form: sections with a title on the side (settings) or on
* top (contact, application), a responsive grid for fields and a footer for
* the actions. Fields come from input, select, textarea and friends.
*/
/**
* Lets the direct children rise one after another once the group scrolls
* into view (kit-stagger from kit.css). Server-rendered visible, so nothing
* hides without JavaScript or with reduced motion. After the entrance the
* delays go, so focus and hover react at once.
*/
function useRise(step = 60) {
const [ref, inView] = useInView<HTMLDivElement>();
const [done, setDone] = useState(false);
useEffect(() => {
if (!inView) return;
const t = window.setTimeout(() => setDone(true), step * 8 + 800);
return () => window.clearTimeout(t);
}, [inView, step]);
return { ref, className: cn('kit-stagger', !inView && 'kit-stagger-wait', done && 'kit-stagger-done'), style: { '--kit-step': `${step}ms` } as React.CSSProperties };
}
export interface FormSectionProps {
title: React.ReactNode;
description?: React.ReactNode;
/** side: title in a column on the left from desktop up. top: title above the fields. */
layout?: 'side' | 'top';
className?: string;
children: React.ReactNode;
}
/** A group of fields with a heading. Several sections are divided by hairlines. */
export function FormSection({ title, description, layout = 'top', className, children }: FormSectionProps) {
const head = useRise(80);
if (layout === 'side') {
return (
<section className={cn('grid gap-x-12 gap-y-6 py-8 first:pt-0 last:pb-0 lg:grid-cols-[minmax(0,17rem)_minmax(0,1fr)] [&+&]:border-t [&+&]:border-border', className)}>
<div ref={head.ref} className={head.className} style={head.style}>
<h3 className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">{title}</h3>
{description && <p className="mt-1.5 max-w-[34ch] text-pretty text-[13.5px] leading-relaxed text-muted-foreground">{description}</p>}
</div>
<div className="min-w-0">{children}</div>
</section>
);
}
return (
<section className={cn('py-7 first:pt-0 last:pb-0 [&+&]:border-t [&+&]:border-border', className)}>
<div ref={head.ref} className={head.className} style={head.style}>
<h3 className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">{title}</h3>
{description && <p className="mt-1 text-pretty text-[13.5px] leading-relaxed text-muted-foreground">{description}</p>}
</div>
<div className="mt-5">{children}</div>
</section>
);
}
/** Fields side by side from tablet up. Use span={2} on wide fields (col-span-full). The fields rise one after another as the form scrolls in. */
export function FormGrid({ columns = 2, className, children }: { columns?: 1 | 2 | 3; className?: string; children: React.ReactNode }) {
const rise = useRise(60);
return (
<div ref={rise.ref} style={rise.style} className={cn('grid gap-x-5 gap-y-6', columns === 2 && 'sm:grid-cols-2', columns === 3 && 'sm:grid-cols-2 lg:grid-cols-3', rise.className, className)}>
{children}
</div>
);
}
/** The action row: secondary actions left, the main one right. Stacks with the main action on top on phones. */
export function FormActions({ note, divider = true, className, children }: { note?: React.ReactNode; divider?: boolean; className?: string; children: React.ReactNode }) {
return (
// Right after a section its bottom padding is the gap, no extra margin on top
<div className={cn('mt-8 flex flex-col-reverse gap-4 sm:flex-row sm:items-center sm:justify-between [section+&]:mt-0', divider && 'border-t border-border pt-6', className)}>
{note ? <p className="text-pretty text-[13px] leading-relaxed text-muted-foreground sm:max-w-[26rem]">{note}</p> : <span className="max-sm:hidden" />}
<div className="flex flex-col-reverse gap-2.5 sm:flex-row sm:items-center [&>*]:max-sm:w-full">{children}</div>
</div>
);
}
/** A summary of all errors on top of a long form, linking to each field. Put it where focus goes after a failed submit. */
export function FormErrorSummary({ title, errors, className }: { title: React.ReactNode; errors: { field: string; message: React.ReactNode }[]; className?: string }) {
// A new set of errors (after a failed submit) nudges the box once, so the eye finds it
const ref = useRef<HTMLDivElement>(null);
const key = errors.map((e) => e.field).join('|');
const last = useRef(key);
useEffect(() => {
if (last.current === key) return;
last.current = key;
const el = ref.current;
if (!el || !key || prefersReducedMotion() || typeof el.animate !== 'function') return;
el.animate([{ transform: 'translateX(0)' }, { transform: 'translateX(-6px)' }, { transform: 'translateX(5px)' }, { transform: 'translateX(-3px)' }, { transform: 'translateX(0)' }], { duration: 420, easing: 'ease-out' });
}, [key]);
if (!errors.length) return null;
return (
<div ref={ref} role="alert" tabIndex={-1} className={cn('rounded-xl border border-[color-mix(in_oklab,var(--destructive)_40%,var(--border))] bg-[color-mix(in_oklab,var(--destructive)_6%,var(--card))] p-4', className)}>
<p className="text-sm font-semibold text-destructive">{title}</p>
<ul className="mt-2 grid gap-1 text-[13px] text-foreground">
{errors.map((e) => (
<li key={e.field} className="flex gap-2 before:mt-[7px] before:size-1 before:shrink-0 before:rounded-full before:bg-destructive before:content-['']">
<a href={`#${e.field}`} className="underline decoration-[color-mix(in_oklab,var(--destructive)_40%,transparent)] underline-offset-4 hover:decoration-destructive">
{e.message}
</a>
</li>
))}
</ul>
</div>
);
}Häufige Fragen
Wie installiere ich Formular-Layouts?
Mit aiw add @ai-website/form-layout. Der Befehl legt die Datei components/ui/form-layout.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Formular-Layouts?
3 Varianten, Kontakt, Bewerbung und Einstellungen.
Kann ein KI-Assistent Formular-Layouts einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Formular-Layouts mit aiw_components und installieren sie mit aiw_add @ai-website/form-layout.