Skip to content

Forms

Forms

Luc UI does not own form state — React Hook Form, TanStack Form or a reducer all work. It owns the part every product otherwise rebuilds: one field anatomy, one submit lifecycle, one error summary, and inputs that agree with the rest of the system.

import { Form, FormActions, FormSection, SubmitButton } from "@luc/ui/forms";

Examples

Field anatomy

Label, description, control and message. The field owns the ids and the ARIA wiring, so a control can never be labelled by accident or by hand.

Ticker or venue-qualified symbol.

Settlement currency, whole units.

Value date, not trade date.

States

Six states, one message slot: description, validating, success, error, disabled, read-only. Only one of them is ever rendered, so the field never stacks four lines of text.

Helper text sits at --fg-muted, not disabled grey.

Checking…

Handle is available

Locked by the workspace policy.

Computed from the instrument.

Groups and sections

A real fieldset for a set of controls that answer one question; a responsive grid for fields that stand alone.

Delivery

Where the daily report is sent.

Markdown is supported.

Units are stated, never assumed

A percentage field requires a declared scale, because 0.12 is ambiguous and a silent conversion is how a 12% allocation becomes a 0.12% one.

Stored as a fraction: 0.12 is 12%.

Stored in percent points: 12 is 12%.

canonical allocation value: 0.12 (a fraction, not percent points)

Specialised inputs

Password, one-time codes and tag lists are LUC components too — same shell, same focus language, same field wiring.

Stored locally; never sent by this demo.

Paste the whole code — it fills every slot.

BTCETH

Enter or comma commits; paste a list to add several.

One submit lifecycle

idle → submitting → success | error. The button owns the pending state, the form owns the outcome, and nothing here needs a form library.

Profile

How the workspace is identified in reports.

status idle

Usage

import { Form, FormActions, SubmitButton } from "@luc/ui/forms";
<Form onSubmit={save}>
  <Field label="Run name">
    <Input defaultValue="Momentum v3" />
  </Field>
  <FormActions>
    <SubmitButton loadingText="Saving">Save</SubmitButton>
  </FormActions>
</Form>

Return a promise from `onSubmit` and the shell owns `submitting` and the error; return a status instead when the outcome is yours to decide.

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

Exported symbols
ExportKindSignature
Form5 documented props({ children, className, onSubmit, onError, errors, error, errorMessage, noValidate, ...props }: FormProps) => import("@types/react").JSX.Element
FormActions2 documented props({ align, sticky, className, ...props }: FormActionsProps) => import("@types/react").JSX.Element
FormErrorSummary3 documented props({ className, always, title, errors: providedErrors, ...props }: FormErrorSummaryProps) => import("@types/react").JSX.Element | null
SubmitButton8 documented props({ loadingText, children, disabled, ...props }: SubmitButtonProps) => import("@types/react").JSX.Element
useFieldIdPrefixexport(explicit?: string) => string
useLucFormexport() => FormStatusValue

Form props

Type: FormProps

Values the compiler reads from the declaration.

Form props
PropTypeDefaultNotes
error?ReactNode—Form-level message. Rendered in the summary, never as a toast-only error.
errorMessage?ReactNode—Message used when a submit rejects without one of its own.
errors?FormErrorItem[] | undefined—Field errors for `FormErrorSummary`. Derived by the caller's form engine.
onError?((error: unknown) => void) | undefined—Called after a submit rejects, for logging or analytics.
onSubmit?((event: FormEvent<HTMLFormElement>) => void | FormStatus | Promise<void | FormStatus>) |…—Submit handler. Returning a promise puts the form into `submitting`; throwing (or rejecting) sets `error` and the summary. Return a `FormStatus` to decide the outcome yourself — a…

FormActions props

Type: FormActionsProps

Values the compiler reads from the declaration.

FormActions props
PropTypeDefaultNotes
align?"start" | "end" | "between" | undefined—
sticky?boolean | undefined—Pins the row to the bottom of a scrolling container (a Sheet or a long settings page). Sticky rows are opaque, per the system overlay rule.

FormErrorSummary props

Type: FormErrorSummaryProps

Values the compiler reads from the declaration.

FormErrorSummary props
PropTypeDefaultNotes
always?boolean | undefined—Render even when no submit has been attempted (e.g. an edit form).
errors?FormErrorItem[] | undefined—Explicit error list. Form engines with their own error tree (React Hook Form, TanStack Form) pass theirs here instead of pushing into the context.
title?string | undefined—

SubmitButton props

Type: SubmitButtonProps

Values the compiler reads from the declaration.

SubmitButton props
PropTypeDefaultNotes
block?boolean | undefined—
leadingIcon?ReactNode—
loadingLabel?string | undefined—
loadingText?ReactNode—Shown while submitting. Defaults to the button's own label.
size?"sm" | "md" | "lg" | null | undefined—
tactile?boolean | undefined—Press ink and pointer light. On by default — a filled control that answers the finger is the family's signature — and switchable off for print, dense toolbars or a surface that al…
trailingIcon?ReactNode—
variant?"danger" | "primary" | "accent" | "secondary" | "outline" | "ghost" | null | undefined—

Guidelines

Do

  • Return a promise from `onSubmit` and let the shell own the pending state — `SubmitButton` already reads it.
  • Keep your form engine, whatever it is, and let `errors` carry its result into the summary.
  • Group a long form with `FormSection`, so the summary can name the part an error came from.

Don't

  • Track `isSubmitting` beside the form: `useLucForm` publishes the same status the submit button is reading.
  • Report a failed save in a toast alone — after pressing the button, the reader looks back at the form.
  • Rely on native validation: the shell turns it off by default so the messages come from `Field`.

The lifecycle, in one place

idle → submitting
A promise from `onSubmit` sets the status; `SubmitButton` shows it and stays focusable.
success
The resolved promise returns the form to rest — and the page decides what to say about it.
error
A rejection or a returned status fills the summary from `errors` and `errorMessage`.

Accessibility

The summary appears after a failed submit rather than while the reader is typing, it is announced as an alert, and each row is a button that moves focus to the field that caused it. It deliberately does not take focus on mount, so it never competes with a form engine that focuses the first invalid control.

Luc UI v1.0Formdark theme