Getting started
Go from an empty shadcn project to a correctly formatted figure on screen. Install the theme first. Every component compiles against its tokens and type scale, and without them a component renders at the wrong sizes with no error.
1. Have a shadcn project#
Components install through the shadcn CLI into a React project that already has Tailwind and a components.json. If you don't 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 available as @lopifi/<name>, and its registryDependencies pull in whatever else it needs.
3. Install the theme first#
npx shadcn@latest add @lopifi/theme
This writes two files:
styles/ledger-tokens.css— the tokens: color (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. Use the CSS form of the theme that ships in @lopifi/theme (an @theme block with the same scales as the preset) and import it from your main stylesheet after @import "tailwindcss";. Don't load both the preset and the @theme file, since 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 here's 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 treats any other text-* class as a text color, 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. Decimals come from the currency, not a hard-coded 2.
7. Verify#
You're set up 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 value colors.
Troubleshooting#
The components render, but sizes and colors are wrong#
The preset isn't applied or the tokens aren't 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. Tailwind only generates classes it has seen. - Tailwind 4: the
@themefile is imported from the stylesheet that importstailwindcss.
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 aren't registered.
text-body (or another type class) disappears next to a color class#
Something is calling shadcn's cn from @/lib/utils instead of @/lib/ledger/cn. See step 5. The element renders at the inherited size with the right color.
Module not found: @base-ui-components/react, or a shadcn wrapper#
Some interactive components (popovers, dialogs, checkboxes) are built on Base UI or a shadcn primitive. The registry item lists the package under dependencies and the shadcn CLI installs it. If it didn't (a lockfile conflict, an offline install, a monorepo where the CLI ran in the wrong package), install it in the package that owns the component, or rerun add 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 causes:
- Locale or time zone. A figure or date formatted in one locale on the server and another on the client produces different text. Pass an explicit
localetoMoney, and format dates with an explicit time zone, wherever server and browser may disagree. - Relative time. "as of 2 minutes ago" depends on the current time, which differs between server and client. Render the absolute timestamp on the server, or render the relative form only after mount.
A warning about data-theme on <html> 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're relying on. The repo's CONTRIBUTING.md has the full checklist.