Conventions
The four API rules every component keeps, so learning one component teaches you all twenty-four.
Flat named props
Every option is a prop with a name. There are no variant objects to compose, no class strings to merge at the call site, and no config file deciding what a component looks like.
The consequence is that the JSX is the whole answer. Reading a call site tells you what will render without opening the component, and every option is reachable from your editor's autocomplete because it is a key on a typed interface.
<Badge
variant="success"
appearance="soft"
size="sm"
rounded="full"
>
Shipped
</Badge>Booleans for two-state options
If an option is either on or off, it is a boolean and its name says which state is on. Nothing is a magic string when a flag reads better.
<Table hoverable gridLines striped stickyHeader>
{/* … */}
</Table>- Names are positive —
hoverable, notdisableHover. A double negative at a call site is a bug waiting to happen. - Anything with three or more states is a string union instead, so
sizeandvariantnever become a pile of booleans that can contradict each other.
Compound parts are exported individually
A table is built from its pieces rather than handed a schema, so anything can go in a cell and the markup on screen is the markup in your file.
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
TableCaption,
TableEmpty,
} from "kipui";Nine exports for one component is the point, not an accident. A single <Table data={rows} columns={cols} /> would have to grow a render prop the first time you needed a badge in a cell, and a second one the first time you needed a footer.
Where the props live
Controlled and uncontrolled
Both, everywhere something has a value, using the same pair of names.
// Uncontrolled — the component keeps its own state.
<Tabs defaultValue="npm">…</Tabs>
// Controlled — you keep it, and it never changes without you.
const [active, setActive] = useState("npm");
<Tabs value={active} onValueChange={setActive}>…</Tabs>defaultValueis read once. Changing it later does nothing — that is whatvalueis for.valuewithonValueChangemeans the component never moves on its own. If you forget the handler it will appear frozen, which is the correct behaviour rather than a bug.- The callback is named for what changed, not for the DOM event, so you receive the new value and not an event object to dig through.
className and overrides
Every component forwards className, merged rather than replaced. It is the right tool for one job and the wrong one for another.
// Layout from outside, appearance from props.
<Badge variant="success" className="ml-auto shrink-0">
Shipped
</Badge>Use it for how the component sits in your layout — margin, alignment, width, grid placement. That is context only the call site knows.
Do not use it to restyle the component's own appearance. If you find yourself overriding its colours or padding from outside, the honest fix is to open the file and change it, or add a prop. You own the file; a growing pile of overrides at every call site is the cost of pretending you do not.
This is why there are no variant objects