React
Server components by default, composition over configuration, and how Forme components handle state.
Forme components are ordinary React components. There is no provider to mount and no configuration object to fill in, and a component that needed one would have been written differently.
Server component by default#
A component is a server component until it needs state, effects, refs or event handlers. Only then does it get "use client".
That boundary is not free: everything under it ships to the browser. Forme keeps it as low in the tree as it can, so importing a Forme component into a server component does not quietly pull React into your client bundle.
Controlled state lives where you need it, not in a context:
"use client";
import { useState } from "react";
import { Button } from "@/components/forme/button/button";
export function SaveBar() {
const [saving, setSaving] = useState(false);
return (
<Button loading={saving} onClick={() => setSaving(true)}>
Save changes
</Button>
);
}Composition over configuration#
Forme prefers composition. When a component might have needed a variant for every special case, it takes children instead:
// A variant flag for every situation eventually becomes a variant nobody reads.
<Shell sidebarVariant="compact-with-icons-and-labels" />
// Composition says the same thing and stays readable.
<Shell>
<Sidebar />
<Content />
</Shell>Controlled and uncontrolled#
An interactive component that can be either takes both, and you choose:
<Input value={value} onChange={setValue} /> // controlled
<Input defaultValue="draft" /> // uncontrolledThere is no internal state to fight with and no useEffect that copies props into it.
Accessibility is part of the component#
Forme uses a Radix primitive where behaviour is genuinely hard to get right: focus traps, roving tabindex, typeahead, dismiss layers. Everything else is a real <label>, a real aria-* where the semantics are not obvious, and a keyboard path for every interaction.
One detail worth copying from Button: a loading button keeps its label in place and stays in the tab order, using aria-busy rather than disabled. Disabling a focused element drops focus, which loses the person using it.