Docs
Install ZoBlocks and use your first component.
Add it to a React app in five steps, match Ant Design or Material UI, or use the loaders from Vue, Angular, Svelte and plain HTML.
Overview
- React gets every component. A command-line tool copies each one into your project as source you own.
- Vue, Angular, Svelte and HTML get the loaders and the switch, as web components from npm.
- Three styles. Components look like ZoBlocks by default, and take on your Ant Design or Material UI theme with one wrapper.
What it needs
20.11+
Node.js
What the CLI runs on.
19
React
18 is partly covered.
5.7+
TypeScript
Sources ship as .tsx and .ts.
4
Tailwind CSS
CSS-first config. No tailwind.config.js.
2
npm packages
clsx and tailwind-merge. Nothing else.
No component imports a Node built-in, so nothing needs a polyfill or a server runtime. Pick your package manager on any command below; the whole page follows.
Add to a React app
For a Next.js or Vite app you already have. No app yet? Create one first, then come back.
Check the @ alias
Components import each other as
@/…. Next.js apps — App Router or Pages Router — usually have this already; Vite apps need it in three files.tsconfig.jsonjson{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }vite.config.tstsimport path from "node:path"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src") } }, });tsconfig.app.json and tsconfig.jsonjsonc{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } // …keep the options already here } }Set up ZoBlocks
initwriteszoblocks.json, which records where your@/alias points (src, or.if there is nosrcfolder). Every lateraddcopies source relative to it. Run it once per project.npx @zoblocks/cli init
You should see:
Created zoblocks.jsonAdd a component
--yesalso installs the two small packages it needs.npx @zoblocks/cli add pulse-loader --yes
You should see:
writelines for the files undersrc/components/zoblocks,src/libandsrc/stylesImport the styles
Add these lines under
@import "tailwindcss";in your global stylesheet.zoblocks-tokens.cssis required — it holds the design tokens. Then add one@importfor each otherzoblocks-*.cssthataddwrote.@sourcetells Tailwind to scan the component files. Tailwind only builds classes it finds, so without it a component renders with no styling at all.src/app/globals.csscss@import "tailwindcss"; /* ZoBlocks */ @import "../styles/zoblocks-tokens.css"; @import "../styles/zoblocks-loader.css"; @source "../components/zoblocks";src/styles/globals.csscss@import "tailwindcss"; /* ZoBlocks */ @import "./zoblocks-tokens.css"; @import "./zoblocks-loader.css"; @source "../components/zoblocks";src/index.csscss@import "tailwindcss"; /* ZoBlocks */ @import "./styles/zoblocks-tokens.css"; @import "./styles/zoblocks-loader.css"; @source "./components/zoblocks";Use it
Any page or componenttsximport { PulseLoader } from "@/components/zoblocks/pulse-loader"; export default function Page() { return <PulseLoader label="Loading your records" showLabel />; }On the Next.js App Router this can be a Server Component — see Server Components below.
npm run dev
You should see: a slowly beating heart with “Loading your records” under it.
Every other component installs the same way. Its add command is at the top of its component page.
Server Components (Next.js App Router)
Components that need the browser declare their own "use client" boundary, so they work unchanged inside a Server Component. The rest — ResultValue, ClinicalStatus, AllergyChip and a few others — are pure presentation and render on the server. Either way, your page does not need "use client".
import { ResultValue, fromObservation } from "@/components/zoblocks/result-value";
export default async function Page() {
const observation = await getObservation(); // your FHIR Observation
// Works. No "use client" needed on the page.
return <ResultValue value={fromObservation(observation)} now={new Date().toISOString()} />;
}Passing a function — onOpenReport, onChange, any on… prop — needs a Client Component, because functions cannot cross from the server. Put that usage in its own file:
"use client";
import { ResultValue, type ResultValueData } from "@/components/zoblocks/result-value";
export function ResultRow({ value, now }: { value: ResultValueData; now: string }) {
return <ResultValue value={value} now={now} onOpenReport={(v) => openReport(v.id)} />;
}The Pages Router and Vite have no Server Components, so none of this applies there.
Start a new project
Create the app, then follow the five steps above.
npx create-next-app@latest my-app --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --yes --use-npmcd my-app
This comes with TypeScript, Tailwind v4 and the @ alias, so step 1 is already done. It uses the App Router; for the Pages Router, change --app to --no-app. Open http://localhost:3000 in step 5.
npm create vite@latest my-app -- --template react-tscd my-appnpm installnpm install tailwindcss @tailwindcss/vite
Then do step 1 for Vite. In step 4, replace everything in src/index.css. Open http://localhost:5173 in step 5.
Two commands, one alias, one stylesheet
Beyond init and add, a new project needs three things configured. The steps above do all of them:
01
The path alias
Components import each other as @/…. TypeScript and your bundler both need to resolve it.
Step 102
Tailwind sources and tokens
@source so Tailwind sees the components, and the tokens stylesheet they are coloured with.
Step 403
The client boundary
Next.js App Router only. Components that need the browser declare their own "use client"; the rest render on the server. Your pages can stay Server Components.
Server Components
Choose a style
Pick the design your app already uses. Components never change — only the wrapper does.
The default look, with nothing to install or wrap. The zoblocks-tokens.css import from step 4 is all it needs, and it works the same in Next.js and Vite. Use this when your app has no other component library, or when you want ZoBlocks’s own design.
For an app that already uses Ant Design. AntdBridge reads your ConfigProvider theme and hands its primary colour, radius and font to ZoBlocks. The Next.js setup follows Ant Design’s own guide, with the bridge added. Pick your framework:
npm install antd @ant-design/nextjs-registry @zoblocks/bridge-antd
"use client";
import { ConfigProvider } from "antd";
import { AntdBridge } from "@zoblocks/bridge-antd";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ConfigProvider theme={{ token: { colorPrimary: "#7c3aed", borderRadius: 10 } }}>
<AntdBridge>{children}</AntdBridge>
</ConfigProvider>
);
}import { AntdRegistry } from "@ant-design/nextjs-registry";
import { Providers } from "./providers";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AntdRegistry>
<Providers>{children}</Providers>
</AntdRegistry>
</body>
</html>
);
}AntdRegistry puts antd’s styles into the server-rendered page, so it does not flash unstyled. Keep your existing fonts and metadata in the layout.
npm install antd @ant-design/cssinjs @zoblocks/bridge-antd
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["antd", "@ant-design/cssinjs", "@zoblocks/bridge-antd"],
};
export default nextConfig;import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs";
import Document, { Head, Html, Main, NextScript } from "next/document";
import type { DocumentContext } from "next/document";
const MyDocument = () => (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const cache = createCache();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => (
<StyleProvider cache={cache}>
<App {...props} />
</StyleProvider>
),
});
const initialProps = await Document.getInitialProps(ctx);
const style = extractStyle(cache, true);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style dangerouslySetInnerHTML={{ __html: style }} />
</>
),
};
};
export default MyDocument;import type { AppProps } from "next/app";
import { ConfigProvider } from "antd";
import { AntdBridge } from "@zoblocks/bridge-antd";
import "@/styles/globals.css";
export default function App({ Component, pageProps }: AppProps) {
return (
<ConfigProvider theme={{ token: { colorPrimary: "#7c3aed", borderRadius: 10 } }}>
<AntdBridge>
<Component {...pageProps} />
</AntdBridge>
</ConfigProvider>
);
}transpilePackages is required on the Pages Router. Without it the server renders antd unstyled and ZoBlocks with antd’s default theme instead of yours.
npm install antd @zoblocks/bridge-antd
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ConfigProvider } from "antd";
import { AntdBridge } from "@zoblocks/bridge-antd";
import App from "./App";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ConfigProvider theme={{ token: { colorPrimary: "#7c3aed", borderRadius: 10 } }}>
<AntdBridge>
<App />
</AntdBridge>
</ConfigProvider>
</StrictMode>,
);For an app that already uses Material UI. MuiBridge reads your ThemeProvider theme and hands its primary colour, radius and font to ZoBlocks. The Next.js setup follows Material UI’s own guide, with the bridge added. Pick your framework:
npm install @mui/material @emotion/react @emotion/styled @zoblocks/bridge-mui @mui/material-nextjs @emotion/cache
"use client";
import { createTheme } from "@mui/material/styles";
const theme = createTheme({
palette: { primary: { main: "#7c3aed" } },
shape: { borderRadius: 10 },
});
export default theme;import { AppRouterCacheProvider } from "@mui/material-nextjs/v16-appRouter";
import { ThemeProvider } from "@mui/material/styles";
import { MuiBridge } from "@zoblocks/bridge-mui";
import theme from "../theme";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider theme={theme}>
<MuiBridge>{children}</MuiBridge>
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}AppRouterCacheProvider puts MUI’s styles into the server-rendered page. enableCssLayer lets your Tailwind classes override MUI’s. On Next.js 15, import from v15-appRouter.
npm install @mui/material @emotion/react @emotion/styled @zoblocks/bridge-mui @mui/material-nextjs @emotion/cache @emotion/server
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@zoblocks/bridge-mui"],
};
export default nextConfig;import { Html, Head, Main, NextScript } from "next/document";
import type { DocumentContext, DocumentProps } from "next/document";
import {
DocumentHeadTags,
documentGetInitialProps,
type DocumentHeadTagsProps,
} from "@mui/material-nextjs/v16-pagesRouter";
export default function MyDocument(props: DocumentProps & DocumentHeadTagsProps) {
return (
<Html lang="en">
<Head>
<DocumentHeadTags {...props} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
return documentGetInitialProps(ctx);
};import type { AppProps } from "next/app";
import { AppCacheProvider } from "@mui/material-nextjs/v16-pagesRouter";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { MuiBridge } from "@zoblocks/bridge-mui";
import "@/styles/globals.css";
const theme = createTheme({
palette: { primary: { main: "#7c3aed" } },
shape: { borderRadius: 10 },
});
export default function App(props: AppProps) {
const { Component, pageProps } = props;
return (
<AppCacheProvider {...props}>
<ThemeProvider theme={theme}>
<MuiBridge>
<Component {...pageProps} />
</MuiBridge>
</ThemeProvider>
</AppCacheProvider>
);
}transpilePackages is required on the Pages Router, or the server renders ZoBlocks with MUI’s default theme instead of yours. On Next.js 15, import from v15-pagesRouter.
npm install @mui/material @emotion/react @emotion/styled @zoblocks/bridge-mui
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { MuiBridge } from "@zoblocks/bridge-mui";
import App from "./App";
import "./index.css";
const theme = createTheme({
palette: { primary: { main: "#7c3aed" } },
shape: { borderRadius: 10 },
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeProvider theme={theme}>
<MuiBridge>
<App />
</MuiBridge>
</ThemeProvider>
</StrictMode>,
);Vue, Angular, Svelte and HTML
The loaders and the switch ship as web components, so they work anywhere without Tailwind or the CLI. Install them, import once, and use the tag.
npm install @zoblocks/loaders @zoblocks/elements
// main.ts
import "@zoblocks/loaders/pulse";
// vite.config.ts — tell Vue these tags are web components
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("zb-") } } })
<!-- any .vue template -->
<zb-pulse-loader label="Loading your records"></zb-pulse-loader>// main.ts
import "@zoblocks/loaders/pulse";
// the component that uses it
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
@Component({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<zb-pulse-loader label="Loading your records"></zb-pulse-loader>`,
})<script>
import "@zoblocks/loaders/pulse";
</script>
<zb-pulse-loader label="Loading your records"></zb-pulse-loader><!-- index.html, served by Vite or any bundler -->
<script type="module">
import "@zoblocks/loaders/pulse";
import "@zoblocks/elements/switch";
</script>
<zb-pulse-loader label="Loading your records"></zb-pulse-loader>
<zb-switch label="Contact precautions" value="unknown"></zb-switch>The other loaders are /rhythm, /breath, /helix and /infusion. See each loader’s page for its attributes.
Components from npm
A few React components are npm packages instead of copied source. Install the package and import its stylesheet once — no init needed.
CLI commands
Run with npx @zoblocks/cli (or your package manager’s equivalent, shown above).
init- Create zoblocks.json. Once per project.
add <name…>- Copy components, and what they depend on, into your project.
list- List every component you can add.
--yes- Let add install the npm packages it needs. Without it, it prints the command.
--dry-run- Show what add would write, and write nothing.
--overwrite- Replace files that already exist — how you update a component.
--cwd <dir>- Run in another folder.
Troubleshooting
- The component has no styling.
- Check the
@sourceline and thezoblocks-tokens.cssimport from step 4. - Layout is fine but the status colours are missing.
- Import the component’s own stylesheet.
addlists everystyles/zoblocks-*.cssit writes; each needs an@import. - Cannot find module '@/…'
- The
@alias is missing — step 1. On Vite it must be invite.config.tsas well astsconfig. - 'React' is declared but its value is never read.
- A known issue in a few components, reported by Vite’s strict
tsc. Delete the lineimport * as React from "react";in the file it names. The dev server is unaffected. - Functions cannot be passed directly to Client Components.
- On the App Router,
providers.tsx(Ant Design) ortheme.ts(Material UI) is missing"use client"on its first line. - Event handlers cannot be passed to Client Component props.
- A Server Component passed a function, such as
onOpenReport. Move that usage into a file that starts with"use client"— see Server Components. - useState is not a function (or another hook error) in a Server Component.
- That component does not declare
"use client"yet. Render it from a file that starts with"use client", the same way. - Ant Design or MUI flashes unstyled when the page loads.
- The server is not sending the library’s styles. Use
AntdRegistryorAppRouterCacheProvideron the App Router, and the_document.tsxsetup on the Pages Router — see Choose a style. - Pages Router: ZoBlocks uses the default antd or MUI colours, not mine.
- Add the
transpilePackagesline tonext.config.tsfrom the Pages Router tab of Choose a style. - No zoblocks.json, or zoblocks.json already exists.
- Run
initonce, in the folder with yourpackage.json. To start over, add--force. - npm warns EBADENGINE, or the CLI will not start.
- Node.js is older than 20.11. Check with
node -vand install the current LTS.