Openware UI
React components and design tokens with a quiet, exact look: a midnight brand colour, a brass accent kept small, cool slate surfaces, hairline buttons, and Inter. Light and dark themes, and a brand colour you can swap.
Docs: openwarelabs.github.io/ui: every component with live examples and props, every block, the tokens and the guides. It's published from each release, so it matches the packages on npm.
Docs
To run them from the repository:
npm install # installs and builds dist/
npm run dev # the docs site: http://localhost:5173
npm run docs:build # the same site as static files in docs-dist/
The docs have a page for every component, with live examples, the code to copy, keyboard behaviour, accessibility notes and a props table read from the TypeScript source, plus guides to installation, theming, colours, type and tokens. The Blocks section has 92 whole page sections (headers, heroes, pricing, settings, sign-in, chat and more), each live in a frame you can narrow to a tablet or a phone, with its .tsx and .css to copy. Search with ⌘K or Ctrl+K. They rebuild as you edit src/, docs/ or tokens/tokens.json.
To document a new component, add docs/examples/<Name>.tsx: a meta export (title, category, description, source file) and one exported function per example. The function's JSDoc becomes the example's description, and its code is what the page shows.
To add a block, list it in docs/app/blocks.tsx and write docs/blocks/<category>/<Name>.tsx and <Name>.css; docs/blocks/README.md has the rules, and npm run blocks:check checks them.
Install in a project
npm install @openwarelabs/ui
React 18 or 19 is a peer dependency. Toasts come from sonner and the colour picker from react-colorful, both installed with it.
Set up once
import "@openwarelabs/ui/fonts.css"; // Inter and Geist Mono, self-hosted
import "@openwarelabs/ui/styles.css"; // tokens + components
<html data-theme="light">
<body class="ow-app">
Then use the components:
import { Button, Field, Input, Toaster, toast } from "@openwarelabs/ui";
<Field label="Project name" hint="Letters, numbers and dashes.">
<Input placeholder="recipe-app" />
</Field>
<Button variant="primary" onClick={() => toast.success("Saved")}>Save</Button>
<Toaster />
Components
149 components, each with a page in the docs.
Hooks for your own components: useControllableState, useFloating (top-layer positioning that flips), usePresence (keeps a layer mounted while it animates out), useDismiss, useModalDialog, useFocusTrap (the trap behind FocusTrap, for your own element), useMenuKeyboard, useVirtualizer (the windowing behind VirtualList). Every prop is typed and described, so your editor explains it on hover.
Blocks as components
Every block is also a component in its own package, @openwarelabs/blocks, that takes its content as props, so you don't copy its code: all 95, from Header01 and Hero01 to Settings03 and Chat03 (the list).
npm install @openwarelabs/blocks
import "@openwarelabs/blocks/styles.css"; // after @openwarelabs/ui/styles.css
import { Hero01 } from "@openwarelabs/blocks";
<Hero01 title="The workspace for shipping software" description="…" primaryAction={{ label: "Start for free", href: "/signup" }} />;
Each block's page in the docs shows its props under the preview. The source is blocks/src/<category>/<Name>/ (the component, its .types.ts and its .css); npm run blocks:build builds the package, and npm run release publishes it with the library at the same version.
Use with AI
The MCP server, @openwarelabs/ui-mcp, gives Claude Code, Codex, Cursor and other AI tools the setup, every component's examples and props, every block's code and the tokens, so they build with the real components:
claude mcp add openware-ui -- npx -y @openwarelabs/ui-mcp # Claude Code
codex mcp add openware-ui -- npx -y @openwarelabs/ui-mcp # Codex
Cursor and most other clients take { "mcpServers": { "openware-ui": { "command": "npx", "args": ["-y", "@openwarelabs/ui-mcp"] } } }. mcp/README.md has every tool and client.
Its check_code tool checks a file the tool wrote, before it runs: components imported from the package and nothing used without its import, real props with real values, every input labelled, the library's controls instead of raw <button> and <input>, tokens instead of hard-coded colours, and --ow- names that exist. The same rules are an ESLint plugin for the project's own lint and CI, @openwarelabs/eslint-plugin-ui:
// eslint.config.js
import openware from "@openwarelabs/eslint-plugin-ui";
export default [openware.configs.recommended];
In the docs, every component and block has a Copy for AI button: a prompt with its whole reference (examples and props, or a block's two files) to paste into any AI tool. The built docs also have llms.txt, llms-full.txt and a Markdown copy of every page under ai/. npm run mcp:build rebuilds the server from the docs, and npm run eslint:build the plugin.
Tokens
Every colour, space, radius, shadow and weight is a CSS variable named --ow-… (var(--ow-surface), var(--ow-radius-container)), and every text style is a class (ow-type-section-title, ow-type-lead). tokens/tokens.json is the single source; npm run tokens regenerates the CSS and npm run contrast checks every text and focus pair in both themes.
Rebrand by overriding the eleven brand steps; the primary button, focus ring, checkboxes, tints and the primary button's glow follow, in both themes. brandScale builds the steps from any colour, on Openware's lightness curve so the contrast holds, and applyBrand puts them on the page:
import { applyBrand, brandCss, brandPresets, brandScale } from "@openwarelabs/ui";
applyBrand("#0d9488"); // the whole page, from one colour
applyBrand(brandPresets[2]!.scale); // or a preset: Midnight, Ocean, Teal, Forest, Plum, Rose, Ember
brandCss("#0d9488"); // ":root { --ow-brand-50: …; }" for a stylesheet
The docs' header has a colour picker that does this live, and the Theming page shows each preset in both themes.
Motion uses the timing tokens: things enter with ow-ease-emphasized and leave faster with ow-ease-exit, and knobs and indicators settle with ow-ease-spring. Menus, popovers, listboxes and tooltips grow from their trigger and shrink back into it; dialogs and drawers animate both ways; segmented controls and tabs slide their indicator. Everything stands still under prefers-reduced-motion.
Tailwind v4: @import "@openwarelabs/ui/tailwind.css"; after @import "tailwindcss"; gives you bg-surface, text-text-muted, rounded-container, p-lg, font-action, shadow-menu and the rest, plus a dark: variant tied to data-theme="dark".
No bundler? dist/openware-ui.iife.js sets window.OpenwareUI, reading React from window.React. A CDN serves it straight from npm: https://cdn.jsdelivr.net/npm/@openwarelabs/ui/dist/openware-ui.iife.js, with dist/styles.css beside it.
Contributing
main takes changes only through pull requests, squash-merged, so each one lands as a single commit named after the PR. The PR title follows Conventional Commits because npm run release reads it: feat: add a date range picker, fix(Menu): close on Escape, feat!: rename Stack's gap sizes. Put BREAKING CHANGE: … in the PR description for a breaking change.
Five checks have to pass to merge:
- Checks: types, contrast, blocks, the library, the MCP server and the docs, and the bundle size: what an app pays, minified and gzipped, for each package and for one piece of it imported alone, against the limits in
size-limits.json(npm run sizeafter a build). Raise a limit in the PR that needs it, so the growth is seen in review. It also lints for React's hooks rules (npm run lint;eslint-suppressions.jsonlists the problems there already were), and packs both packages asnpm publishwould, installs them in a new app and imports, requires, bundles and type-checks them there (npm run smoke). - PR title: the title follows the convention (
npm run commits:check -- --title "feat: …"locally). - Cleaner: openwarelabs/cleaner on the
.tsand.tsxfiles the PR changes. Types go in a<name>.types.tsbeside the component, and imports from another folder use the aliases instead of../:@/…forsrc/(in the library and the docs),@docs/…fordocs/(?highlightimports too) and@build/…inmcp/. Files that haven't been touched yet don't follow it; they catch up as they change. - Visual: every block (desktop and phone width) and every component example, in light and dark, rendered from the PR and from
mainand compared pixel by pixel. If something looks different, the run's summary lists it and thevisual-reportartifact shows each one before, after and with the changed pixels marked. When the change is meant, add the visual change label and the check passes.npm run visualruns the same comparison locally againstmain(-- --filter Herofor a few). - Accessibility: axe over every block (desktop and phone width) and every component example, in light and dark, against WCAG 2.2 A and AA. The run's summary lists each problem with the element and how to fix it.
a11y-known.jsonlists the problems there already were, which don't fail it; fixing one means lowering its count.npm run a11yruns it locally (-- --filter Herofor a few).
Dependabot opens a pull request each Monday with the week's npm and GitHub Actions updates, minor and patch ones grouped, and React with React DOM in one. Only Checks runs on those (types, builds, size and the package smoke test): PR title, Cleaner, Visual and Accessibility are for changes people make, and skipped jobs count as passing.
scripts/setup-github.sh sets this up on GitHub (squash merges only, the PR title as the commit, the ruleset for main, the visual change label). Repository admins can bypass the ruleset, which npm run release uses to push its version commit and tag.
Releasing
npm run release # works out the version, checks, tags and pushes
npm run release -- --dry-run # shows the version and the notes, changes nothing
npm run release -- minor # forces patch, minor, major or an exact version
The version comes from the commit subjects since the last release, written as Conventional Commits: feat: makes a minor release, fix: and perf: a patch, and feat!: or a BREAKING CHANGE: line a major one (a minor one before 1.0). chore:, docs:, refactor:, test:, ci:, build: and style: don't release on their own. The command checks that main is clean and up to date, runs every check and build, bumps both packages, writes CHANGELOG.md, commits, tags vX.Y.Z and pushes.
The tag starts .github/workflows/release.yml, which runs the checks again, publishes @openwarelabs/ui and @openwarelabs/ui-mcp to npm with trusted publishing (no token, with provenance), and makes the GitHub release from the changelog. Pull requests are squash-merged with their title as the commit, so the titles are what it reads.
Rules worth keeping
- One primary button per view. Everything else is secondary or ghost.
- Borders before shadows: cards are
--ow-surfacewith a 1px--ow-border. Shadows are for things that float or press. - Brass stays small: dots, a star, the pointer on the grid. Never a button.
- Sentence case for every label. Name the action on the button ("Delete project"), never "OK".
- Icons are Lucide (
lucide-react), 16px, 2px stroke; icon-only buttons need anaria-label.
Credits
Icons drawn inside the components are Lucide shapes (ISC). Inter and Geist Mono are under the SIL Open Font License 1.1. Toasts use sonner and the colour picker uses react-colorful, both under the MIT licence.