First component
The smallest real screen, composed from components, a page shell, one field and one action, and what each part states on its own.
Installation ends with a provider and one Button on the
page. The next thing most people write is a div with a class name, and this page exists to
break that habit: a screen is composed from components, and every one of them states
something the markup would otherwise have to remember, a landmark, an accessible name, an
error announcement, a submit control that cannot be pressed twice.
The screen below invites a teammate by email. Three steps, about forty lines, one file.
1. Start from the shell
import { Page, PageContent, PageHeader, Typography } from "@asheeui/web";
<Page>
<PageHeader>
<Typography role="heading-xl">Invite a teammate</Typography>
</PageHeader>
<PageContent>{/* the form goes here */}</PageContent>
</Page>;
Four components and no class names. Page claims the height of the window and paints the
theme background; PageHeader is the header landmark and holds the bar at the top;
PageContent is the main landmark and takes the height the other two leave. Typography
renders the element its role implies, heading-xl is an h1, so the one heading on the
screen is a real level-one heading without anyone stating it.
The shell adds no navigation, no data and no breakpoint. Each option belongs to the part that can
act on it, which is why <PageContent spacing="lg"> moves the content under the bar and
<Page sticky> does not compile at all, a shell has no bar to keep in view.
Page has that split in full.
2. Put one field in it
import { Form, Input } from "@asheeui/web";
import { useState } from "react";
const [email, setEmail] = useState("");
const [message, setMessage] = useState<string>();
<Form legend="Who should join?" submitLabel="Send invite" onSubmit={submit}>
<Input
label="Email"
name="email"
type="email"
description="We send the invite here."
value={email}
onChangeText={setEmail}
status={message ? "error" : "default"}
message={message}
required
/>
</Form>;
Input is one component wearing the field family's four parts: label is the accessible
name, description is the hint read with the control, and message with status is the
outcome. Because they are props rather than sibling elements, the component wires the ids
itself, htmlFor for the label, aria-describedby for the hint and the message,
aria-invalid for the status, so a field cannot ship with a message that nothing announces.
Field anatomy is that shape in full.
The value stays on the page, and the field reports every change through onChangeText. That
is what lets the page validate, reset or submit without reaching into the DOM.
3. Give it an action
Form draws the action itself: submitLabel renders a submit Button with type="submit"
inside the form's element, so Enter in the field submits it. The page supplies the
rule and the consequence.
import { type FormEvent, useState } from "react";
import {
Form,
Input,
Page,
PageContent,
PageHeader,
Typography,
} from "@asheeui/web";
export function InviteTeammate() {
const [email, setEmail] = useState("");
const [message, setMessage] = useState<string>();
const [invited, setInvited] = useState<string>();
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!email.includes("@")) {
setMessage("An email address needs an @.");
return;
}
setMessage(undefined);
setInvited(email);
}
return (
<Page>
<PageHeader>
<Typography role="heading-xl">Invite a teammate</Typography>
</PageHeader>
<PageContent spacing="lg">
<Form
legend="Who should join?"
submitLabel="Send invite"
onSubmit={submit}>
<Input
label="Email"
name="email"
type="email"
description="We send the invite here."
value={email}
onChangeText={setEmail}
status={message ? "error" : "default"}
message={message}
required
/>
</Form>
{invited && (
<Typography role="body-sm" tone="muted">
{invited} now has an invite.
</Typography>
)}
</PageContent>
</Page>
);
}
This screen leaves one state out: while the request is in flight, pass isPending to Form
and the submit control is busy and cannot be pressed twice, with its label still in place so
the row does not move. Building a form covers that, and the focus
rule below.
The component states the meaning, the page states the content
That sentence is the whole approach, and this screen is one example of each half.
| You wrote | It stated | Instead of |
|---|---|---|
Page, PageHeader, PageContent |
The shell, its height, and the header and main landmarks |
A div with min-h-dvh and a main placed by hand |
Typography role="heading-xl" |
A level-one heading, and the treatment that role carries | An h1 with a size and a weight chosen per screen |
Form legend submitLabel |
The fieldset and its legend, the submit control, and its pending state when you pass isPending |
A form, a fieldset, a legend and a button |
Input label description message status |
The accessible name, the hint, the message, aria-invalid, aria-describedby |
A label, an id on a paragraph and aria-describedby typed by hand |
PageContent |
A gutter and a maximum width | mx-auto max-w-5xl px-4 sm:px-6 |
The other half is yours, and it is smaller than the markup it replaces: the values, the words on the label and the description, the sentence that explains the failure, the rule that decides it, and what happens when it passes. None of that is styling, so none of it is a class name.
What it reads like to a screen reader
The reader arrives on this screen and hears, in this order:
- A
bannerlandmark, thePageHeader, and inside it "Invite a teammate, heading level 1". - A
mainlandmark,PageContent, which is also what a skip link points at. - "Who should join?, group", because the form's fields are grouped by the
fieldsetits legend names. - "Email, edit, required". The label is the name; the requirement is on the control, and the asterisk next to the label is only drawn.
- "We send the invite here.", read with the field through
aria-describedbyrather than drifting after it as a stray paragraph. - On a failed submit, the message, announced immediately, without waiting for a pause,
because the field's message element carries
role="alert", and the field reporting itself invalid, so it is invalid to assistive technology and not merely red. - A submit button named "Send invite", which Enter in the field presses.
That is the framework's half. The one thing it cannot do for you: after a failed submit it does not know which field to fix first, so the page moves focus to the first invalid control. Building a form has the effect that does it. For how the parts compose and what they announce, see Accessibility and Announcements.
What to reach for next
- A second field, validation and a pending submit: Building a form.
- A record count, a collection or a filter: Components has the gallery, and the table family starts at Table.
- A shell with navigation, a sidebar or a marketing band: Page compositions.
- Stating the spacing, the radius and the variants once for the whole product: Configuration, then Theming.