@maholan/ui
v1.2.0
Published
The single distributable UI component library for the MHL UI Platform. Built on React Aria, styled with Tailwind CSS v4, following a clean, modern design language.
Readme
@maholan/ui
The single distributable UI component library for the MHL UI Platform. Built on React Aria, styled with Tailwind CSS v4, following a clean, modern design language.
Features
- Accessible by default — every interactive component wraps a
react-aria-componentsprimitive; no hand-rolled ARIA or keyboard handling - Next.js App Router ready — all interactive components carry
"use client"; purely presentational components are Server Component safe - Type-friendly — zero
any,VariantPropsre-exported so consumers can type their own wrappers - Easy to use — sensible
defaultVariants, consistent prop names (size,variant,isDisabled,leadingIcon,trailingIcon) across all components - Flexible —
classNameon every component, React Aria render props exposed, icon slots accept anyReact.ReactNode - Design-led — variants and sizes follow MHL's design system patterns
Installation
Option A — npm package (managed dependency)
pnpm add @maholan/ui @maholan/tokens @maholan/theme
# peer dependencies
pnpm add react react-domImport components directly and add both CSS files to globals.css:
/* globals.css */
@import "tailwindcss";
@import "@maholan/tokens/mhl-tokens.css"; /* token variables */
@import "@maholan/ui/styles.css"; /* pre-built component utilities */import { Button, ButtonUtility, CloseButton } from "@maholan/ui";Why
@maholan/ui/styles.css? Tailwind v4 does not scannode_modulesby default.styles.cssis a pre-built stylesheet generated at build time from all component source files — it contains every utility class the components use. Without it, components render unstyled.
Option B — CLI (copy source into your project)
npx @maholan/cli init # copies mhl-tokens.css locally, injects @import
npx @maholan/cli add buttonFiles are copied to src/components/ui/ — you own and can modify the code.
Tailwind scans your src/ natively — no extra CSS import needed.
To keep the local token file current:
npx @maholan/cli sync # update mhl-tokens.css to latest
npx @maholan/cli sync --check # CI check — exits 1 if outdatedQuick Start
// app/page.tsx — Server Component, no "use client" needed here
import { Button } from "@maholan/ui";
export default function Page() {
return (
<main>
<Button color="primary" size="lg">
Get started
</Button>
<Button color="secondary" size="md">
Learn more
</Button>
<Button color="primary-destructive" size="md">
Delete
</Button>
</main>
);
}Components
Button
Accessible button built on React Aria. Renders as <button> by default,
switches to <a> (via React Aria Link) when href is provided.
import { Button } from "@maholan/ui";
// Standard button
<Button color="primary" size="md">Save changes</Button>
// Link button (renders as <a>)
<Button href="/dashboard" color="link-gray">Go to dashboard</Button>
// With icon slots — pass FC (receives className) or ReactNode
<Button iconLeading={PlusIcon} color="primary">Add item</Button>
<Button iconLeading={<PlusIcon className="size-4" />} color="primary">Add item</Button>
<Button iconTrailing={<ArrowRightIcon />} color="link-color">Continue</Button>
// Loading state
<Button isLoading color="primary">Saving…</Button>
// Disabled
<Button isDisabled color="primary">Unavailable</Button>
// Full width
<Button fullWidth color="primary">Submit form</Button>color prop: primary · secondary · tertiary · link-gray ·
link-color · primary-destructive · secondary-destructive ·
tertiary-destructive · link-destructive
Sizes: sm (36px, default) · md (40px) · lg (44px) · xl (48px) ·
2xl (60px)
ButtonUtility
Icon-only button for compact actions (e.g., toolbar buttons, close icons within larger components). No text content — only an icon.
import { ButtonUtility } from "@maholan/ui";
<ButtonUtility size="md" aria-label="Delete item">
<TrashIcon />
</ButtonUtility>;CloseButton
Specialised icon-only button for dismissing modals, toasts, and drawers. Pre-configured with the correct ARIA label and close icon.
import { CloseButton } from "@maholan/ui";
<CloseButton size="sm" onPress={handleClose} />;Variant Props
Every component re-exports its VariantProps type so you can type wrappers
without duplicating the union:
import { Button, type ButtonVariantProps } from "@maholan/ui";
interface MyButtonProps extends ButtonVariantProps {
label: string;
}
function MyButton({ label, color, size }: MyButtonProps) {
return (
<Button color={color} size={size}>
{label}
</Button>
);
}Styling Override
Pass className to any component to add or override classes. Uses cn()
(clsx + tailwind-merge) internally so there are no specificity conflicts:
<Button color="primary" className="w-full rounded-full">
Full-width pill button
</Button>React Aria Render Props
All interactive components expose React Aria's render props. Use them to apply your own conditional styles:
<Button
color="secondary"
className={(renderProps) =>
cn(
"base-class",
renderProps.isFocusVisible && "ring-4 ring-blue-500",
renderProps.isPressed && "opacity-80"
)
}
>
Custom states
</Button>Icon Slots
Components that can contain icons accept iconLeading and iconTrailing. Pass
either a React function component (receives className automatically) or any
ReactNode. Not locked to any specific icon library:
import { SearchIcon } from "lucide-react";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
// FC — className is injected automatically for consistent sizing
<Button iconLeading={SearchIcon}>Search</Button>
// ReactNode — rendered as-is
<Button iconLeading={<MagnifyingGlassIcon className="size-4" />}>Search</Button>
<Button iconLeading={<span>🔍</span>}>Search</Button>
// Trailing icon
<Button iconTrailing={ArrowRightIcon} color="link-color">Continue</Button>Package Exports
| Import | Contents |
| ----------------------- | -------------------------------------------------------- |
| @maholan/ui | All components, types, cn() utility |
| @maholan/ui/stories | All Storybook stories (for master Storybook aggregation) |
| @maholan/ui/storybook | Storybook decorators, viewports, and theme config |
Component File Structure
Each component lives in src/components/<group>/<name>/:
src/components/base/buttons/button/
├── button.tsx # Component implementation ("use client")
├── button.variants.ts # CVA variant definitions
├── button.stories.tsx # Storybook stories (autodocs + per-variant)
├── button.test.tsx # Vitest + jest-axe tests
└── index.ts # Barrel exportDevelopment
# Build the package
pnpm build
# Watch mode
pnpm dev
# Run tests
pnpm test
# Type check
pnpm type-check
# Start Storybook
pnpm storybookRelated Packages
@maholan/tokens— Design tokens andmhl-tokens.css@maholan/theme—ThemeProvider,ThemeScript,useTheme@maholan/cli— CLI to copy component source into consumer projects@maholan/registry— Registry JSON builder
License
MIT
