Installation
Niemeyer ships as a private GitHub Packages npm package built around Tailwind 4. Six numbered steps below take you from a fresh app to a working <Button>.
1. Configure GitHub Packages access
Create a GitHub personal access token (classic) with read:packages and export it as GITHUB_TOKEN. Then add this .npmrc at the project root (Yarn Classic reads it directly; Yarn Berry users can set the equivalent npmScopes in .yarnrc.yml):
# .npmrc — at project root
@morada-ai:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
2. Install the package and peers
Tailwind 4 and @tabler/icons-react are peer dependencies — install them in your app so a single copy is hoisted next to the package.
# Install the package
yarn add @morada-ai/niemeyer
# Plus the peer dependencies (one copy in your app)
yarn add tailwindcss@^4
yarn add @tabler/icons-react3. Configure PostCSS
Tailwind 4 ships its PostCSS plugin separately. Add it to your config (no autoprefixer needed):
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};4. Wire the stylesheet
Three explicit @imports in app/globals.css, in this order. You only need one @source for your own app code — the package's theme.css already self-sources its components, and Tailwind 4 merges @source directives across all imported stylesheets.
/* app/globals.css */
@import "tailwindcss";
@import "@morada-ai/niemeyer/styles";
@import "tw-animate-css";
/* Only your app code — the package self-sources its own components */
@source "./app/**/*.{ts,tsx}";5. Load fonts
The package's design tokens reference Outfit (headings) and Lato (body) by family name only — you choose how to deliver the actual font files. Three options below, in order of recommendation.
Option A — Browser-loaded <link> (recommended; works everywhere)
Add to your layout <head> (or via @font-face). The browser fetches the CSS at runtime, so the build server never needs network access to Google Fonts. Trade-off: small flash of unstyled text (FOUT) on first paint while the CSS resolves; with display=swap there is no layout shift.
<!-- in <head>, plus a preconnect for the WOFF2 host -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Lato:wght@300;400;700;900&display=swap"
rel="stylesheet"
/>Option B — next/font/google (Next.js apps with network access at build time)
If your build environment can reach fonts.googleapis.com, next/font/google self-hosts the WOFF2 files at build time, eliminating the runtime request to Google. Be aware the **build will fail** in any environment that cannot reach Google Fonts (corporate VPN, sandboxed CI, air-gapped runners) — switch to Option A there.
// app/layout.tsx
import { Outfit, Lato } from "next/font/google";
const outfit = Outfit({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-outfit",
display: "swap",
});
const lato = Lato({
subsets: ["latin"],
weight: ["300", "400", "700", "900"],
variable: "--font-lato",
display: "swap",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html className={`${outfit.variable} ${lato.variable}`}>
<body>{children}</body>
</html>
);
}Then bind the niemeyer tokens to the next/font CSS variables in your app stylesheet, AFTER @import "@morada-ai/niemeyer/styles":
/* app/globals.css — AFTER @import "@morada-ai/niemeyer/styles" */
:root {
--font-heading: var(--font-outfit), system-ui, -apple-system, sans-serif;
--font-body: var(--font-lato), system-ui, -apple-system, sans-serif;
}Option C — Self-host
Drop Outfit-*.woff2 and Lato-*.woff2 into public/fonts/ and declare @font-face rules in your stylesheet. Zero external dependency, but you carry the asset pipeline yourself.
/* app/globals.css */
@font-face {
font-family: "Outfit";
src: url("/fonts/Outfit-Variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: "Lato";
src: url("/fonts/Lato-Regular.woff2") format("woff2");
font-weight: 400;
font-display: swap;
}
/* Repeat @font-face for every Lato weight you ship (300, 700, 900). */6. Re-export `cn()` so consumer code can import it locally
The package ships the same cn pattern as shadcn (clsx + tailwind-merge). Both helpers are bundled, so a one-line re-export is enough:
// src/lib/utils.ts
export { cn } from "@morada-ai/niemeyer/utils";Optional: enable the ESLint plugin
Catches design-system violations automatically (no hardcoded colors in className="…", no lucide-react imports, no native <button>/<input>):
// eslint.config.js
const niemeyer = require("@morada-ai/niemeyer/eslint");
module.exports = [niemeyer.configs.recommended];