Getting started
From an empty shadcn project to a correctly formatted figure on screen. The order matters: the theme goes in first, because every component compiles against its tokens and its type scale, and a component installed without it renders with the wrong sizes and no error.
1. Have a shadcn project
The components install through the shadcn CLI, into a React project that already has Tailwind and a components.json. If you do not have one yet:
npx shadcn@latest init
2. Add the registry to components.json
The @lopifi namespace is a namespaced registry. Add it once:
{
"registries": {
"@lopifi": "https://lopifi.com/r/{name}.json"
}
}
Every item is then addressable as @lopifi/<name>, and each item's own registryDependencies pull in what it needs.
3. Install the theme first
npx shadcn@latest add @lopifi/theme
This writes two files:
styles/ledger-tokens.css— the tokens: colour (both themes), spacing, radius, elevation, motion, the two type families.ledger/preset.ts— the Tailwind preset: the same tokens as Tailwind scales, including the type scale (text-body,text-figure-md, …).
Import the tokens once, in your root layout or global stylesheet, before your own styles:
import "@/styles/ledger-tokens.css";
4. Wire Tailwind
Tailwind 3 — add the preset to tailwind.config.ts, and make sure content covers the folder the components were written to:
import type { Config } from "tailwindcss";
import ledger from "./ledger/preset";
export default {
presets: [ledger],
content: ["./src/**/*.{ts,tsx}"],
} satisfies Config;
Tailwind 4 — v4 is configured in CSS, not in a JS config. Use the CSS form of the theme that ships in the @lopifi/theme item (an @theme block carrying the same scales as the preset) and import it from your main stylesheet after @import "tailwindcss";. Do not load both the preset and the @theme file — they declare the same scales. If your v4 project still uses a JS config through @config, the v3 preset works there too.
5. Install @lopifi/cn
Most components pull it in for you, but it is worth knowing why it exists:
npx shadcn@latest add @lopifi/cn
It lands at lib/ledger/cn.ts. Components import this cn, not your project's @/lib/utils. shadcn's cn uses a tailwind-merge that only knows Tailwind's built-in text-sm…text-9xl; it files any other text-* class under text colour, so cn("text-body text-text-primary") silently drops the size. The Lopifi cn registers the type scale with tailwind-merge. If you add a font size to the preset, add it to TYPE_SCALE in cn.ts in the same change.
6. Your first component
npx shadcn@latest add @lopifi/money
import { Money } from "@/components/ledger/money";
export function Example() {
return (
<p>
<Money value={{ amount: 124056, currency: "USD" }} />{" "}
<Money accounting value={{ amount: -124056, currency: "USD" }} />{" "}
<Money value={{ amount: 1200, currency: "JPY" }} />{" "}
<Money value={null} />
</p>
);
}
The amount is an integer in minor units — 124056 is $1,240.56 — and the currency travels with it. The decimals come from the currency, not from a hard-coded 2.
7. Verify
You are set up correctly when all of these are true:
$1,240.56renders in the figure face (Geist Mono if your app loads it), with tabular digits.- The accounting negative renders as
($1,240.56), and the parentheses do not shift the digits when stacked in a right-aligned column against a positive. - JPY renders with no decimals.
nullrenders as an explicit "not available" state — never$0.00.- Setting
data-theme="dark"on<html>switches the surfaces and the value colours.
Troubleshooting
The components render, but sizes and colours are wrong
The preset is not applied, or the tokens are not loaded. Check that:
styles/ledger-tokens.cssis imported once, globally — not only inside a page.- Tailwind 3:
presets: [ledger]is in the config Tailwind actually reads, andcontentincludes the directory the components were written to. A class Tailwind never saw is a class that was never generated. - Tailwind 4: the
@themefile is imported from the stylesheet that importstailwindcss.
A quick test: an element with className="text-figure-md bg-canvas" should get a font size and a background. If it gets neither, the scales are not registered.
text-body (or another type class) disappears next to a colour class
A component, or your own code, is calling shadcn's cn from @/lib/utils instead of @/lib/ledger/cn. See step 5. The symptom is that the element renders at the inherited size with the right colour.
Module not found: @base-ui-components/react, or a shadcn wrapper
Some interactive components (popovers, dialogs, checkboxes) are built on Base UI or on a shadcn primitive. The registry item lists the package under dependencies and the shadcn CLI installs it; if it did not (a lockfile conflict, an offline install, a monorepo where the CLI ran in the wrong package), install it yourself in the package that owns the component, or run the add again from that package's root. If the missing import is @/components/ui/<name>, add the shadcn component of that name (npx shadcn@latest add <name>).
Hydration mismatch warnings
Usually one of two things:
- Locale or time zone. A figure or a date formatted on the server in one locale and on the client in another produces different text. Pass an explicit
localetoMoney(and format dates with an explicit time zone) wherever the server and the browser may disagree. - Relative time. "as of 2 minutes ago" is computed from the current time, which is different on the server and the client. Render the absolute timestamp on the server, or render the relative form only after mount.
If the warning is about data-theme on <html>, it comes from setting the theme before React hydrates; that attribute is expected to differ, so add suppressHydrationWarning to the <html> element only.
Anything else
Email hello@lopifi.com with the component, the smallest snippet that reproduces it, what you expected, and your React and Tailwind versions. A wrong accounting rule is as much a bug as a broken layout — say which document states it and cite the authority you are relying on. The repo's CONTRIBUTING.md has the full checklist.