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-example.tsx
<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-example.tsx
<Table hoverable gridLines striped stickyHeader>
  {/* … */}
</Table>
  • Names are positive — hoverable, not disableHover. 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 size and variant never 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.

app/contacts/page.tsx
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

On compound components the root takes the presentation props and the parts take almost none. If you are looking for where an option lives, it is on the outermost element.

Controlled and uncontrolled

Both, everywhere something has a value, using the same pair of names.

tabs-example.tsx
// 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>
  • defaultValue is read once. Changing it later does nothing — that is what value is for.
  • value with onValueChange means 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.

usage.tsx
// 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

All 24 components resolve their classes internally and merge yours last. Nothing needs a wrapper utility at the call site to make that work.