haus-components
v3.1.0
Published
haus design system components: 21 accessible React primitives built on haus-tokens.
Maintainers
Readme
haus-components
Eighteen accessible React components built on haus-tokens.
Install
npm install haus-components haus-tokensreact and react-dom are peer dependencies, ^18 || ^19, so your copy is the only copy.
Nothing else is required. Every icon in the package is drawn inline, so there is no icon
font to load and no stylesheet beyond haus-components/styles.css and the token layers.
Usage
Import the token layer, then the component stylesheet.
import 'haus-tokens/index.css'
import 'haus-components/styles.css'index.css is the four token files in the right order. They are still exported
individually if you want to swap one, and replacing brand.css is the
documented case. The order matters, and getting it wrong fails silently: an
unresolved var() drops the declaration with no warning, no build error, and a
component that renders unstyled.
Both stylesheets are in cascade layers, haus.*, so your own unlayered CSS
beats them without a specificity fight.
To theme a subtree, put a brand on it:
import 'haus-tokens/brands/vault.css'
<div data-haus-theme="vault">…</div>import { Button, Card, Badge } from 'haus-components'
<Card>
<Button variant="primary" size="lg">Save changes</Button>
<Badge variant="success">Clean</Badge>
</Card>Components
Forms and content, Avatar · Badge · Button · Callout · Card ·
Checkbox · Divider · EmptyState · Input · Radio · Select ·
Textarea · Toast · Toggle
Overlays and navigation, Modal · Popover · Tabs · Tooltip
The second group is new, and it exists because the first group was the whole
library. haus was complete for forms and empty for overlays, which is the half
a product cannot avoid writing itself and the half with the accessibility
contracts worth centralising. The six added in 1.0: Callout, Divider,
EmptyState, Popover, Tabs, Tooltip: were not chosen. They were
measured: each one had already been built independently in more than one of the
three products consuming this system, and the measurements are on the issues.
Every component forwards its ref and spreads the remaining props onto the
underlying element, so anything not modelled as a prop is still reachable. That
sentence is asserted over the barrel in src/api-surface.test.tsx, so a
nineteenth component is held to it by existing.
The ref goes to the element a caller would want: the control on the form
components, the dialog on Modal, the root elsewhere.
Modal takes either a title, which becomes the visible heading and names the
dialog, or an aria-label when the design has no heading. The types require one
of the two, since a dialog with no accessible name is announced as nothing.
Toast is the surface of a notice and nothing else: no provider, no queue, no
positioning, no timer. That is a boundary rather than a gap. See
decision 0008. You own
where notices appear and for how long; it owns how one looks, and carries its
own role="status" so it is announced wherever you put it.
The stylesheet is separate, on purpose
The CSS is emitted as a standalone styles.css rather than injected when you
import a component. Injection would write to document at import time, which
breaks server rendering, and it takes away your control over where the
component styles sit relative to the token layers. One explicit import keeps
both.
Class names are scoped at build time, as in haus-Button-button-2ZuB7, so they can
never collide with yours.
Server rendering
Renders under react-dom/server with no DOM present. The interactive
components (Modal, Toggle, Checkbox) use hooks but no layout effects at
module scope, so they hydrate cleanly.
Asserted in src/ssr.test.tsx, which runs in a node environment rather than
jsdom. Under jsdom a component can reach for document and get one, which is
the mistake the test is looking for.
One exception, and it is asserted too. An open Modal is a portal, and a
portal has nowhere to go on a server, so it throws rather than hydrating wrong.
Render it closed on the server and open it on the client.
Note there is no 'use client' directive: under a React Server Components
setup, import these from a client component.
Build
Vite library mode rather than the tsup used elsewhere in this workspace. tsup
routes every stylesheet through esbuild's global css loader, which turns each
*.module.css into plain global CSS and hands the component an empty styles
object, giving className="undefined" on every element with no error. Vite's
PostCSS pipeline scopes CSS modules properly, which is the entire reason this
package needs a build.
Licence
MIT
