Guide

Installation

How to install the Diametral Design System in a real project — plain HTML, or any of the popular frameworks. The system is buildless: at its core it is one stylesheet plus design tokens, so most stacks need nothing more than a <link> (or one import) and the fonts.

What you get

The package @diametral/design-system ships several independent layers. Take only what you need — each works on its own, and none requires a build step to consume.

CSS + tokensThe whole visual language as one stylesheet of .ds-* classes backed by --ds-* custom properties. → @diametral/design-system/css/diametral.css
Design tokensThe single source of truth as JSON. → @diametral/design-system/tokens.json
Web ComponentsAn optional vanilla custom-element layer (<ds-button>, <ds-status>, …). → @diametral/design-system/components
React componentsOptional real, typed React components (Button, DataGrid, …). → @diametral/design-system/react
Tailwind presetBinds Tailwind colors/spacing/fontFamily/… to the --ds-* variables. → @diametral/design-system/tailwind-preset
SCSS variables$ds-* variables, each resolving to the matching CSS var. → @diametral/design-system/dist/tokens.scss
AssetsFree font CSS (Fraunces fallback) + logo SVGs + license notes — the commercial Ufficio font is not bundled. → @diametral/design-system/assets/*

The CSS and the tokens are the foundation; the Web Components and React layers render the same .ds-* markup, so styling and theming always come from the one stylesheet. Change a token, every layer follows.

Install

With npm (or pnpm / yarn / bun):

npm install
npm i @diametral/design-system

react and react-dom are optional peer dependencies — add them only if you use the React layer. CSS-only and Web Component consumers don't pull them in.

Optional React peers
# only if you use @diametral/design-system/react
npm i react react-dom

Without npm. The system is plain CSS + fonts + SVG + a sprinkle of vanilla JS — no bundler required. Either copy the css/ and assets/ folders into your project and link css/diametral.css, or link it straight from a CDN that serves npm packages (unpkg, jsDelivr, esm.sh):

CDN stylesheet
<link rel="stylesheet" href="https://unpkg.com/@diametral/design-system/css/diametral.css">

Load the CSS + fonts

Three things to wire up once, at your app root.

1 · The one stylesheet. Everything is bundled behind a single entry that @imports the tokens, reset, typography, and every component:

Stylesheet · bundler / npm
// bundler / npm
import "@diametral/design-system/css/diametral.css";
Stylesheet · no build
<!-- no build -->
<link rel="stylesheet" href="css/diametral.css">

2 · The body font — Geist (free, OFL):

Geist · Google Fonts
<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=Geist:wght@300;400;500;600&display=swap" rel="stylesheet">

3 · The title font — Ufficio (opt-in, commercial). The --ds-font-title token lists Ufficio first, but the face is declared only in assets/fonts/ufficio.css. Import it only if you hold an Ufficio license:

Ufficio · no build
<!-- only if you hold an Ufficio license -->
<link rel="stylesheet" href="assets/fonts/ufficio.css">
Ufficio · via npm
// via npm
import "@diametral/design-system/assets/fonts/ufficio.css";

If you don't import it, the unknown family name is skipped and titles fall back to the free Fraunces stack automatically — no token change needed. To load the free fonts (Fraunces + Geist) explicitly in one shot, import assets/fonts/fallback.css instead.

Global reset. css/diametral.css includes a light global reset (box-sizing, zeroed margins, base body type). Every class is namespaced .ds-*, so it won't collide with your app's CSS or Tailwind utilities. If your app already has a reset and you want to avoid overlap, import css/tokens.css plus the individual css/components/*.css partials instead of the full bundle.

Quick start · Plain HTML / no build

Link the CSS, load the fonts, write .ds-* markup. Add the Web Components module if you want the custom elements.

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="css/diametral.css">
  <!-- optional Web Components -->
  <script type="module" src="components/index.js"></script>
</head>
<body>
  <!-- plain classes -->
  <button class="ds-button ds-button--primary">Save</button>

  <!-- or the optional custom elements -->
  <ds-button variant="primary">Save</ds-button>
  <ds-status status="success" heading="Approved"></ds-status>
</body>
</html>

Quick start · Vite (React)

Import the CSS once in main.jsx, then import components from the React entry.

main.jsx
// main.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import "@diametral/design-system/css/diametral.css";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(<App />);
App.jsx
// App.jsx
import { Button, DataGrid } from "@diametral/design-system/react";

export default function App() {
  return <Button variant="primary">Save</Button>;
}

Load the Geist <link> in index.html's <head> (and assets/fonts/ufficio.css if licensed).

Quick start · Next.js (App Router)

Import the global CSS once in app/layout.tsx. The React components use hooks and event handlers, so they are client components — render them inside a "use client" boundary.

app/layout.tsx
// app/layout.tsx
import "@diametral/design-system/css/diametral.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
app/page.tsx
// app/page.tsx (or any client component)
"use client";
import { Button, Status } from "@diametral/design-system/react";

export default function Page() {
  return <Button variant="primary">Save</Button>;
}

Fonts: either load Geist via the Google <link> in layout.tsx's <head>, or use next/font/google and map the token:

next/font/google
import { Geist } from "next/font/google";
const geist = Geist({ subsets: ["latin"], weight: ["300", "400", "500", "600"] });
// then apply geist.className to <body>, or set :root { --ds-font-sans: ... }

Quick start · Create React App

Import the CSS once in index.js.

src/index.js
// src/index.js
import React from "react";
import { createRoot } from "react-dom/client";
import "@diametral/design-system/css/diametral.css";
import App from "./App";

createRoot(document.getElementById("root")).render(<App />);
src/App.js
// src/App.js
import { Button } from "@diametral/design-system/react";

export default function App() {
  return <Button variant="primary">Save</Button>;
}

Quick start · Angular

Add the stylesheet to the styles array in angular.json:

angular.json
// angular.json → projects.<app>.architect.build.options
"styles": [
  "node_modules/@diametral/design-system/css/diametral.css",
  "src/styles.scss"
]

…or @import it from src/styles.scss:

src/styles.scss
@import "@diametral/design-system/css/diametral.css";

Then use the .ds-* classes directly in templates. To use the Web Components, import the module once (e.g. in main.ts) and allow custom elements in the modules/components that use them:

main.ts
// main.ts
import "@diametral/design-system/components";
app.component.ts
// the standalone component (or NgModule) that uses <ds-*> elements
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";

@Component({
  selector: "app-root",
  template: `<ds-button variant="primary">Save</ds-button>`,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {}

Quick start · Vue

Import the CSS once in main.js:

main.js
// main.js
import { createApp } from "vue";
import "@diametral/design-system/css/diametral.css";
import App from "./App.vue";

createApp(App).mount("#app");

Use the .ds-* classes in templates, or the Web Components — register the module once and Vue will render the custom elements as-is:

main.js · Web Components
// main.js
import "@diametral/design-system/components";
App.vue
<!-- App.vue -->
<template>
  <button class="ds-button ds-button--primary">Save</button>
  <ds-status status="success" heading="Approved"></ds-status>
</template>

Vue treats any unknown hyphenated tag as a custom element by default. With Vite, if you ever need to be explicit, set isCustomElement: (tag) => tag.startsWith("ds-") in @vitejs/plugin-vue's compilerOptions.

Quick start · CDN / import map (buildless React)

Because the React components have no build dependency, you can run them straight from a CDN with an import map — exactly what the live demo does. Great for a throwaway prototype or a CodePen; for a real app, prefer npm + a bundler.

Buildless React · import map
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/@diametral/design-system/css/diametral.css">

<div id="app"></div>

<script type="importmap">
{ "imports": {
  "react": "https://esm.sh/react@18.3.1",
  "react-dom": "https://esm.sh/react-dom@18.3.1?external=react",
  "react-dom/client": "https://esm.sh/react-dom@18.3.1/client?external=react",
  "@diametral/design-system/react": "https://esm.sh/@diametral/design-system/react"
} }
</script>
<script type="module">
  import React from "react";
  import { createRoot } from "react-dom/client";
  import { Button } from "@diametral/design-system/react";
  createRoot(document.getElementById("app"))
    .render(React.createElement(Button, { variant: "primary" }, "Save"));
</script>

The ?external=react query keeps a single React instance across the imports. See the full working demo in react.html.

Quick start · Streamlit (Python)

Streamlit renders its own widgets, so you don't install the package into it. Align colors in .streamlit/config.toml, inject the stylesheet, then render .ds-* blocks with st.markdown. Full guide: docs/streamlit.md · runnable example (Docker): examples/streamlit/.

.streamlit/config.toml
[theme]
primaryColor = "#ff2a00"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f5f5f5"
textColor = "#161616"
font = "sans serif"
inject the stylesheet + flatten widgets
import urllib.request, streamlit as st

# Fetch the FLATTENED bundle (dist/, not css/ which is @import-based) and inline
# it — Streamlit can strip a bare <link> but keeps an inline <style>.
CDN = "https://unpkg.com/@diametral/design-system/dist/diametral.css"
css = urllib.request.urlopen(CDN, timeout=15).read().decode()

st.markdown(f"""<style>
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&display=swap');
{css}
html, body, button, input, textarea {{ font-family: "Geist", sans-serif; }}
.stButton > button {{ border-radius:0; border:1px solid #161616; background:#161616; color:#fff; box-shadow:none; }}
.stTextInput input, [data-baseweb="select"] > div {{ border-radius:0 !important; box-shadow:none !important; }}
</style>""", unsafe_allow_html=True)
render a .ds-* block
st.markdown("""
<div class="ds-statgrid">
  <div class="ds-statgrid__cell"><div class="ds-statgrid__label">Revenue</div><div class="ds-statgrid__value">€4.5M</div></div>
  <div class="ds-statgrid__cell"><div class="ds-statgrid__label">Margin</div><div class="ds-statgrid__value">24.6%</div></div>
</div>
""", unsafe_allow_html=True)

Tailwind

Add the preset to presets in your Tailwind config — it binds colors, spacing, fontFamily, and the rest to the --ds-* custom properties:

tailwind.config.js
// tailwind.config.js
module.exports = {
  presets: [require("@diametral/design-system/tailwind-preset")],
  content: ["./src/**/*.{html,js,ts,jsx,tsx,vue}"],
};

Now Tailwind utilities resolve to the design tokens — e.g. bg-accent, text-ink, p-4, and font-title emit var(--ds-accent), var(--ds-ink), var(--ds-space-4), var(--ds-font-title). Because they are backed by the variables, runtime theming still works: flip data-theme="dark" and the Tailwind-styled elements re-theme too.

SCSS option. Prefer SCSS variables? dist/tokens.scss exposes $ds-* variables (each resolving to the CSS var, so theming still applies):

SCSS variables
@use "@diametral/design-system/dist/tokens.scss" as ds;
.thing { color: ds.$ds-ink; padding: ds.$ds-space-4; }

// or the legacy syntax
@import "@diametral/design-system/dist/tokens.scss";
.thing { color: $ds-ink; }

The Tailwind preset (dist/tailwind-preset.cjs) and dist/tokens.scss are generated by the design-system repo's build. If you are working from a clone (not the npm package), run npm run build (or npm run build:tokens) once to produce dist/. The published package ships dist/ already, so installed consumers need nothing.

Theming

Dark mode (and any theme) is a one-liner — import the theme stylesheet and set the selector on a root element:

Theme import
import "@diametral/design-system/css/diametral.css";
import "@diametral/design-system/css/themes/dark.css";
Theme selector
<html data-theme="dark">   <!-- or class="dark", or class="dark-theme" -->

css/themes/dark.css targets [data-theme="dark"], .dark, and :root.dark-theme, so it drops in regardless of which convention your app uses. For OS-driven dark mode, add class="ds-auto-dark" to <html>. Themes override only the semantic tokens — see Theming for per-brand theming and the Tailwind/SCSS/shadcn notes.

TypeScript

No extra setup. Types ship with the React entry and are wired through the package exports map (react/index.d.ts), so the import below is fully typed out of the box — typed props, children, event handlers, and forwardRef on Button / Input.

Typed import
import { Button } from "@diametral/design-system/react";

Troubleshooting

"node_modules JSX isn't transpiled." It doesn't need to be. The React components are authored as plain ES modules with React.createElement (no JSX) and ship as valid JS — Vite, Next, CRA, Remix, etc. import them directly. You don't have to add @diametral/design-system to a transpilePackages / transpileDependencies allowlist.

Fonts don't load offline. Geist and Ufficio load fine over file://; only the Google Fonts <link> needs a network. The system still renders with system fallbacks offline. Self-host the fonts (the bundled assets/fonts/ufficio.css is already local) or import assets/fonts/fallback.css if you want the free Fraunces/Geist files under your control.

Titles render in a serif, not Ufficio. Expected unless you imported assets/fonts/ufficio.css (and hold a license). The token lists Ufficio first; without the @font-face it falls back to Fraunces / Georgia.

My app's reset and Diametral's overlap. Skip the bundle's reset: import css/tokens.css plus the css/components/*.css partials you need instead of css/diametral.css.

CSP blocks the esm.sh demo. The buildless CDN pattern pulls React from https://esm.sh and the CSS from a CDN. If your Content-Security-Policy is strict, allow those origins in script-src / style-src / connect-src — or just install from npm and bundle, which needs no external origins at runtime.