Design System

Get started

Install from the private feed, load the tokens and the icon font once, then import components. The library ships raw source, so there is no build step to wait on.

Authenticate to the package feed

The packages are published to a private Azure Artifacts feed, so npm needs to know where to find the @healthmetrics scope and how to authenticate.

.npmrc — at your repo root, committed
@healthmetrics:registry=https://pkgs.dev.azure.com/<organisation>/_packaging/<feed>/npm/registry/
Terminal
# Windows — writes a token to your user ~/.npmrc
npx vsts-npm-auth -config .npmrc

# macOS / Linux — create a feed PAT in Azure DevOps, then paste the
# base64 block from "Connect to feed" into your ~/.npmrc

Keep credentials out of the repo

The committed .npmrc holds only the registry mapping. Tokens belong in your user-level ~/.npmrc, which is never committed. In CI, use the npmAuthenticate@0 pipeline task rather than a checked-in token.

Install

Add the packages your app needs.

Terminal
npm install @healthmetrics/ui @healthmetrics/tailwind-config

# @healthmetrics/icons and @healthmetrics/tokens come in as dependencies;
# install them directly only if you want them without the components.

Configure your bundler

Because the library ships .tsx source, your app compiles it. In Next.js, transpile the packages. Vite needs no equivalent step.

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  transpilePackages: ["@healthmetrics/ui", "@healthmetrics/icons", "@healthmetrics/tokens"],
};

export default nextConfig;

Why ship source?

Compiling in your app is what keeps each component's "use client" boundary exact — bundling is where those directives get stripped or misplaced. It also means the prop types you see in your editor are the implementation, never a stale .d.ts. Use "moduleResolution": "bundler" in your tsconfig.

Load the tokens and point Tailwind at the source

Import Tailwind and the HMS token layer at your app root, and tell Tailwind to scan the library source so its utility classes are not purged.

app/globals.css
@import "tailwindcss";
@import "@healthmetrics/tailwind-config"; /* tokens + animation utilities */

/* Tailwind v4 does not scan node_modules — adjust these paths to match your app */
@source "../node_modules/@healthmetrics/ui/src/**/*.{ts,tsx}";
@source "../node_modules/@healthmetrics/icons/src/**/*.{ts,tsx}";

Why the @source lines?

Tailwind v4 skips node_modules. Without these, every utility class used inside the library is purged and the components render completely unstyled.

Load the Material Symbols font

Icons are font ligatures, not SVGs. This step is the one most often missed.

app/layout.tsx — in <head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0,0&display=swap"
  rel="stylesheet"
/>

Without this, icons render as literal text

<Icon name="exportToExcel" /> renders a ligature. If the font has not loaded, the browser draws the words export_to_excel where the icon should be. Nothing throws, so it reads like a styling bug. Under a strict CSP, self-host the variable font instead and keep the CSS family name material-symbols-rounded.

Set the theme before paint

The token layer keys off a dark class on <html>. Apply it in a blocking script so there is no light/dark flash.

app/layout.tsx — in <head>, before the app renders
<script
  dangerouslySetInnerHTML={{
    __html: `(function(){try{var t=localStorage.getItem('hms-theme');
      if(!t){t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}
      if(t==='dark')document.documentElement.classList.add('dark');}catch(e){}})();`,
  }}
/>
Anywhere in a client component
const { theme, toggle, mounted } = useTheme();

// The server cannot read localStorage, so `theme` is "light" until mounted.
// Gate theme-dependent output on `mounted` to avoid a hydration mismatch —
// or, better, drive it in CSS with Tailwind's `dark:` variant.
<button onClick={toggle}>{mounted && theme === "dark" ? "Light" : "Dark"}</button>;

Use components

Import and compose. Icons are typed against the Material Symbols set.

app/page.tsx
import { Button, Icon } from "@healthmetrics/ui";

export default function Page() {
  return (
    <Button>
      <Icon name="exportToExcel" size={18} />
      Export
    </Button>
  );
}

Import from subpaths in shipped routes

The root barrel re-exports all 65 components, so importing from it pulls recharts, cmdk, @tanstack/react-table and react-day-picker into your module graph. Every component also has its own subpath — import { Button } from "@healthmetrics/ui/button" resolves to a single file. Use the barrel while prototyping, subpaths in production routes.

Contributing to the design system

From a clone of the monorepo:

Terminal
npm install
npm run dev              # this docs site
npm run storybook        # component workbench (port 6006)

npm run lint             # ESLint
npm run typecheck        # TypeScript across every package
npm test                 # unit + interaction tests
npm run gate:a11y        # every story, both themes, WCAG 2.2 AA
npm run verify:packaging # tarball contents, publint, type resolution

Next: explore the components or the brand foundations.

On this page