Rising Tide Developers
Reference

Design System

What @rising-tide/ui ships, what varies by brand, and how to extend it or fork it honestly.

Rising Tide ships a design system so you do not have to invent one, and it is built on the assumption that you will want to look like yourself. Nothing here is mandatory. An app that takes the tokens and none of the components is a normal, correct Rising Tide app.

The three layers, and what each costs

what it iswho takes it
@rising-tide/ui/tokensthe raw --rt-* valuesevery rt- app; no client app
@rising-tide/ui/themethe semantic mapping on top (--color-primary and friends)same
@rising-tide/ui/primitivescomponentsanyone who wants them

The split matters. Tokens are a look; primitives are mostly rules. A client app that declines /tokens and /theme because it has its own brand can still take /primitives, as long as it declares the token names those components read - see the contract below.

Starting a new app

Copy rt-sample. It is a real building app - browse smart products, open one, sign in - and it carries a /gallery route rendering every component this package ships. It is the reference because it is deployed and tested, not because it is documented.

Three things in it are not what they look like, and all three are load-bearing:

@stripe/stripe-js in an app that takes no payments. @privy-io/react-auth reaches @stripe/crypto, which requires it without declaring it. Omit it and next build fails with Module not found and an import trace naming Privy. Do not remove it because nothing imports it - nothing is supposed to.

The CSS import order. tokens, then theme, then shell.css, then safe-area.css. Out of order the app renders unstyled or half-styled and nothing errors.

Two @source lines. Tailwind v4 does not scan node_modules, so a class that appears only inside a shipped component is never generated. The shell bundle and the primitives bundle are separate and each needs its own line. A missing one leaves the component rendering unstyled, which reads as a design decision rather than a build gap.

The adoption contract

A primitive reads semantic token names. Most of them are the shadcn names your app already declares if it has a token bridge - primary, background, border, muted-foreground and so on. Measured against grammata, the client app that retints hardest: 17 of the 19 names were already there.

What is Rising-Tide-specific is small, and scoped to what you actually adopt:

you adoptyou also declare
Card, Inputnothing
Badge--color-depth-text
Button--color-ember-text, --color-depth-text, --rt-radius
Toasterthe four -fill / -foreground status pairs

Those are not taste. ember-text and depth-text are the -text calibration steps: the base accents are tuned as fills at 3:1 and measure 3.7-4.3:1 as type, which fails WCAG AA. The steps pass. Skipping them ships unreadable destructive and link labels.

Button's typeface is a token too - --rt-font-button, falling back to inherit. Take /theme and your buttons are mono, which is the house style. Do not, and they are set in your own body font. The one exception is the money variant, which stays mono unconditionally because its label is chain data.

Forking is supported

A shared component works when your brand is a retint. It does not work when your brand is a different treatment, and that distinction is real rather than a matter of discipline.

The worked example: grammata's two real button variants are primary (bg-ink text-paper italic font-serif) and quiet (a hairline-underlined control with no background). No token substitution produces either from a filled mono control, because the difference is structural. grammata forks, and that is the right answer.

What a fork must not lose are the rules. However the button looks, it should keep:

  • 44 CSS px of hit area on every size, released above the mobile breakpoint so a desktop console keeps its density. min-h-11 sm:min-h-<natural>, or min-h-[44px] - same thing.
  • A visible focus ring on every variant but link. This is the one rule with no legitimate exemption.
  • disabled:pointer-events-none. Opacity is not a disabled state; a faded button still fires, and on a protocol app that can spend money or deploy a contract.

Checking your extension

Opt in from your conformance config. Absent this, nothing about the design system runs - a protocol-facts install never mentions your buttons.

conformance.config.json
{
  "app": "your-app",
  "src": ["src"],
  "extends": "rising-tide-design-system",
  "designSystem": {
    "primitives": { "button": "src/components/ui/button.tsx" }
  }
}

npx rt-conform then reports which rules your button lost. It says nothing about your colours, your radius, your own variants, or the components we have never heard of. You may have things in your design system that have nothing to do with this protocol; that is the expected case.

If you would rather run it from a test than from the CLI, the same checks are a plain function:

import { checkButtonContract } from "@rising-tide/ui/testing";
import { buttonVariants } from "@/components/ui/button";

const failures = checkButtonContract(buttonVariants, {
  sizes: ["default", "sm", "lg", "icon"],
}).filter((r) => !r.ok);

What we will not do

Check whether your app looks like ours. The rules above are the complete set, and they exist because each one breaks something for a real person - a thumb that misses, a keyboard user who cannot see focus, a disabled button that fires anyway. Everything else is yours.