ComponentsForm Panel

Form Panel

section

The full form block — sectioned fields, inline validation and a sunken action bar.

Form panel
Sectioned fields, validation & an action bar — the form block.
Best practices
Do
  • One column by default — label above control; every field is a .field carrying its label, control and .field__hint.
  • Show validation inline: aria-invalid on the control + the message in .field__error, next to the field it belongs to.
  • Commit actions live in the sunken foot — one .btn--primary, secondaries as .btn--ghost.
Don't
  • Don't use placeholders as labels — they vanish the moment the user types.
  • Don't disable the submit button while the user is still typing; validate on blur and on submit.
New workspace
Set up a workspace for your team.
Seats
Phone (billing alerts)
Plan
Email notifications
Send a weekly digest to the owner.
You can change these later in Settings.

Composes: Input · Form primitives · Button · Select · Number Input · Phone Input · Switch · Checkbox & Radio

Best practices

Do
  • One column by default — label above control; every field is a .field carrying its label, control and .field__hint.
  • Show validation inline: aria-invalid on the control + the message in .field__error, next to the field it belongs to.
  • Commit actions live in the sunken foot — one .btn--primary, secondaries as .btn--ghost.
Don't
  • Don't use placeholders as labels — they vanish the moment the user types.
  • Don't disable the submit button while the user is still typing; validate on blur and on submit.

Recipe CSS

This is the exact CSS your kit ships for Form Panel — token-driven, so it re-themes with every knob. Get it (and the rest) from the configurator's Use kit panel.

/* === Form panel ===
   The editing-surface BLOCK — what you reach for when a screen needs a real form,
   not a loose stack of inputs. A bordered panel with a titled header, a body of
   labelled fields on a responsive grid, sectioned groups, an inline validation
   summary, and a footer action bar (Cancel / Save). COMPOSES the field atoms
   (.field / .lab + .in · .select · .numinput · .phoneinput · .switch · .radio-card)
   and .buttons, so a labelled, validated, sectioned form is rebuildable from the
   kit — not hand-assembled per screen.

   Anatomy
     .formpanel                  frame (border + radius + surface)
       .formpanel__head          title + description (divider below)
       .formpanel__body          padded field area (vertical rhythm)
         .formpanel__error       inline validation summary (when submit fails)
         .formpanel__grid        responsive 2-up field grid; a child takes
           .formpanel__full      to span the full width
         .formpanel__section     a titled sub-group (divider above)
           .formpanel__section-title
       .formpanel__foot          action bar — note (left) + buttons (right) */
.formpanel { border: 1px solid var(--k-border); border-radius: var(--k-radius-md); background: var(--k-surface); overflow: hidden; container: formpanel / inline-size; }
/* Container query (B2) — the field grid already collapses to one column via
 * auto-fit; the query adds what auto-fit can't: under ~380px the action FOOT
 * stacks (note above, buttons full-width block) so the primary action stays
 * thumb-reachable on a narrow container. Responds to the panel's own width. */
@container formpanel (max-width: 380px) {
  .formpanel__foot { flex-direction: column; align-items: stretch; }
  .formpanel__foot-note { margin-right: 0; }
  .formpanel__foot .btn { width: 100%; }
}
.formpanel__head { padding: var(--k-s-16); border-bottom: var(--k-divider); }
.formpanel__title { font-size: var(--k-type-h3); font-weight: var(--k-weight-semibold); color: var(--k-fg); }
.formpanel__desc { font-size: var(--k-type-small); color: var(--k-fg-muted); margin-top: var(--k-s-2); }
.formpanel__body { padding: var(--k-s-16); display: flex; flex-direction: column; gap: var(--k-s-16); }

/* Responsive field grid — two columns on a roomy panel, collapses to one when the
 * panel is narrow (no container query needed: minmax floor + auto-fit). This is
 * the field-layout grammar a bare card lacks; a full-width field opts out. */
.formpanel__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: var(--k-s-16); }
.formpanel__full { grid-column: 1 / -1; }

/* A titled sub-group inside the body — divider above + an eyebrow label, so a long
 * form reads as sections ("Profile", "Security") instead of one undifferentiated wall. */
.formpanel__section { display: flex; flex-direction: column; gap: var(--k-s-16); padding-top: var(--k-s-16); border-top: var(--k-divider); }
.formpanel__section-title { font-size: var(--k-type-eyebrow); font-weight: var(--k-weight-semibold); text-transform: uppercase; letter-spacing: var(--k-track-eyebrow); color: var(--k-fg-muted); }

/* Inline validation summary — an alert band at the top of the body when submit
 * fails, in the danger-soft tone so it matches the .alert atom's error variant. */
.formpanel__error { display: flex; align-items: flex-start; gap: var(--k-s-8); padding: var(--k-s-10) var(--k-s-12); border-radius: var(--k-radius-sm); background: var(--k-danger-soft); color: var(--k-danger-soft-fg, var(--k-danger)); font-size: var(--k-type-small); }
.formpanel__error svg { flex: none; }

/* Footer action bar — mirrors the data-table foot surface for family consistency:
 * a sunken band with a top divider, an optional note on the left, the primary
 * action trailing right. */
.formpanel__foot { display: flex; align-items: center; gap: var(--k-s-12); flex-wrap: wrap; }
.formpanel__foot-note { margin-right: auto; font-size: var(--k-type-small); color: var(--k-fg-muted); }

/* --horizontal: labels-on-left (Tailwind "form layout with labels on left"). Each
   .lab row reflows to [label | control] — the label in a fixed leading column, a
   hairline dividing the rows — the dense settings-form layout. Collapses back to
   stacked on a narrow container (the label column would crush the control).
   Override the label width with --k-form-label-w. */
.formpanel--horizontal .formpanel__body { gap: 0; }
.formpanel--horizontal .formpanel__grid { display: block; }
.formpanel--horizontal .lab {
  display: grid;
  grid-template-columns: var(--k-form-label-w, 11rem) minmax(0, 1fr);
  align-items: center;
  gap: var(--k-s-12);
  padding: var(--k-s-14) 0;
  border-top: var(--k-divider);
}
.formpanel--horizontal .lab > span:first-child { align-self: center; }
.formpanel--horizontal .formpanel__grid > .lab:first-of-type { border-top: 0; }
@container formpanel (max-width: 34rem) {
  .formpanel--horizontal .lab { grid-template-columns: 1fr; gap: var(--k-s-4); padding: var(--k-s-10) 0; }
}