Zum Inhalt springen
UI Kit
Accent

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

3 Variants

Variants

Kontakt

Zwei Spalten auf breiten Bildschirmen, eine auf dem Telefon. Die Felder steigen beim Hereinscrollen nacheinander auf, Senden macht aus dem Knopf einen Haken.

Bewerbung

Abschnitte untereinander, eine Fehlerübersicht mit Links zu jedem Feld, Upload und Tags. Senden mit offenen Fehlern schüttelt den Knopf und nennt, was fehlt.

Einstellungen

Titel und Erklärung ab Desktop links, Felder rechts, Aktionen am Ende.

Details

Was Formular-Layouts mitbringt, aus Registry und Quellcode gelesen.

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

Props

Die Schnittstelle FormSectionProps aus components/ui/form-layout.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • titlePflicht
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • layoutoptional
    'side' | 'top'

    side: title in a column on the left from desktop up. top: title above the fields.

  • childrenPflicht
    React.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
components/ui/form-layout.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert