Field is the control a Lux surface takes an entry in — an address, a quantity,
a search term. It renders a label and an input together and binds them, which an
Input on its own leaves to the caller.
A label reaches a control by id: htmlFor names the element carrying that id
and nothing else. A field component that accepts the control as a child has to
put the id somewhere it can reach, and what it can reach is its own wrapper — so
the label points at a <div> and the input inside it has no accessible name.
Both spellings look the same in the source, and the difference is audible only
to somebody listening to the page rather than reading it.
Whoever holds the id has to be whoever renders the input, so Field renders the
input. There is no arrangement of props that produces a label bound to nothing.
Naming the control
id names the input. Pass one and Field puts it on the input and points the
label at it; pass none and it generates one with useId and does the same. The
help and error paragraphs take ids derived from it, so every part of a field
carries a name that came from one place.
Installation
npm install @luxfi/ui @hanzo/gui @hanzo/ui @hanzo/design @tanstack/react-queryimport '@luxfi/ui/tokens.css'
import '@hanzo/design/styles.css'
import { AppProvider } from '@luxfi/ui/provider'
import { Field, type FieldProps } from '@luxfi/ui/read'AppProvider goes once at the root of the app and mounts the gui engine and a
query client. The two sheets are where the names resolve: colour from
@luxfi/ui/tokens.css, spacing, elevation and measure from @hanzo/design.
Usage
import { useState } from 'react'
import { Field } from '@luxfi/ui/read'
export function Recipient() {
const [to, setTo] = useState('')
const amiss = to !== '' && !/^0x[0-9a-f]{40}$/i.test(to)
return (
<Field
label="Recipient"
help="An address on this network."
placeholder="0x…"
value={to}
onChangeText={setTo}
error={amiss ? 'That is not an address on this network.' : undefined}
/>
)
}Help and error
Both are tied to the input through aria-describedby, so a screen reader reads
them with the field rather than stranding them beside it. The attribute is a
list of ids in the order they are read: help, then error, then whatever the
caller passed as its own aria-describedby, which is kept rather than replaced.
error is a sentence about what is wrong, and passing one also sets
aria-invalid on the input and draws its border in the refusal colour.
invalid is the same state without a sentence — an answer that fails a pattern,
where the field is wrong and there is nothing to add. aria-invalid is omitted
from FieldProps, so validity is whatever error and invalid say and cannot
be set to disagree with them.
The optional mark
optional draws the word beside the label. It is asked for rather than derived
from required, because the mark is a claim about the form it sits on: a screen
where nothing is required marks nothing, and !required would mark every field.
A value, not a reading
The rest of @luxfi/ui/read takes a reading — reading, read, or failed
with a reason — because what those components draw comes from a chain and an
indexer, where "there is nothing" and "we could not ask" are a fact and a
fabrication that look alike once both have been flattened into an empty list.
A field's value does not come from there. It comes from the person in front of
it, an empty entry is an empty entry, and there is no third thing for the type
to carry. The reading, and the components that take one, are in
Table.
Field ships in the same package because a surface that draws a reading also
takes entries against it, and Table uses Field for its own search box.
Props
FieldProps is the engine's InputProps less aria-invalid, plus what follows.
Anything not listed here reaches the engine's Input — placeholder, type,
inputMode, value, disabled, readOnly and the rest.
| Prop | Type | Description |
|---|---|---|
label | ReactNode | What this control is for, read out with it. Required. |
help | ReactNode | What a person needs to know before answering. |
error | ReactNode | What is wrong with the answer. Saying so is also saying the control is invalid. |
invalid | boolean | Wrong without a sentence about it. |
optional | ReactNode | The word drawn beside the label, in the reader's own language. Absent marks nothing: the mark is a claim about a form, and a screen where nothing is required marks nothing. |
unseen | boolean | The label names the control and is not drawn — for a field whose place on the screen already says what it does. It is the drawing that goes, not the naming. |
onChange | (e: React.ChangeEvent<HTMLInputElement>) => void | What a person typed. Declared here rather than inherited: the engine's InputProps is written for two platforms and omits a web change event, and a caller inheriting nothing got a parameter widened to any without a word from the compiler. |
onChangeText | (text: string) => void | The same thing, already unwrapped. |
id | string | Names the input, and the help and error paragraphs beneath it. Generated when absent. |
How it is drawn
Style props on gui elements — YStack, Label, Input, Paragraph — with
colours taken from paint, the package's one table of token names. A field has
its own family of them: the engine paints an input from its theme row, and the
rows installed here resolve $background and $color to a near-white box
holding near-black text, which is correct on paper and invisible on a dark
surface. So the input reads --color-input-fg, --color-input-bg,
--color-input-bg-readonly and the border tokens beside them, while the label
and the notes read the text ladder. Those are separate values for the same
roles, and a caller should not reach across. A read-only field keeps that ladder
and changes ground; a disabled one draws its label and its text in the disabled
ink. The input stands at reach.plain. The label is a block with a stated leading,
so it is a twenty-four pixel target on one line and grows on two. See gui.hanzo.ai
for the engine and Theming for the token layer.
The other Field
@luxfi/ui exports a second component under this name, from the root and from
@luxfi/ui/field. That one accepts a control as a child — an Input or an
InputGroup — injects the id into it, and can float the label into the box. It
is the design system's field; this one is the exchange's. The subpath is what
selects between them.
What it does not do
It does not validate. Whether an answer is wrong is the caller's finding, handed
in as error or invalid; there is no schema, no resolver, no submit and no
form state. It takes text and only text: a select, a checkbox, a textarea or a
group of controls is not this component, and the position the source takes is
that a control which does not exist yet can have its own part rather than a hole
in this one. It draws no required mark — optional is the only word it sets
beside a label. And it reads nothing: the value comes from a person, so there is
no fetching, retrying or caching here, and no reading for the type to carry.