@tangle-network/brand
The authoring owner of shared Tangle color, typography, radius, motion, shadow and status decisions. Generic components live in @tangle-network/ui; product composition stays in its product repository. See the brand guidelines for visual, copy and component standards.
Install
pnpm add @tangle-network/brand
Use
Tailwind v4
@import "tailwindcss";
@import "@tangle-network/brand/styles";
The standard import supplies canonical tokens, the Tailwind theme, named themes and globals. Brand remains dark-default. It does not import the optional ladder/system styles or load fonts.
For finer control, use the exported filenames:
@import "tailwindcss";
@import "@tangle-network/brand/styles/tokens.css";
@import "@tangle-network/brand/styles/theme.css";
@import "@tangle-network/brand/styles/named-themes.css";
@import "@tangle-network/brand/styles/globals.css";
Mode contract
| Scope | Behavior |
|---|---|
Unpinned root; [data-sandbox-ui] |
Canonical dark baseline |
.dark or data-theme="dark" |
Explicit dark, including a nested dark island |
.light or data-theme="light" |
Explicit light, including a nested light island |
data-sandbox-theme="vault" or "dawn" |
Existing legacy light names; an explicit .dark on that same element takes precedence |
aubergine / aubergine-light, arena / arena-light, super / super-light, tangle-dark / tangle-light |
Existing named dark/light pairs, applied with data-theme |
agents / agents-light |
Agent Builder: white light pages, cards separated by shadow, and a mauve neutral ladder. Surfaces, text and borders only; accent, status and category stay canonical. Generated by scripts/gen-ladders.mjs |
.dark[data-theme="intelligence"] |
Existing dark-only product surface; .light keeps the identity and uses canonical light |
[data-theme="hospitality"] + .dark[data-theme="hospitality"] |
Hospitality's sage-and-forest product theme in both modes. Set it on the document root. Without .dark it is a complete light scope, including before hydration; .dark selects the dark baseline and its dark ladder. Status tones stay canonical |
.dark[data-theme="website"] + [data-theme="website"] |
The public site's indigo-night planes and indigo-paper light. Set class="dark" data-theme="website" on the document root; a section with data-theme="website" and no .dark is a complete light island. Accent, status, category and syntax tones stay canonical |
Set one coherent mode on each boundary; do not author contradictory .light/.dark/named-mode markers. Named light variants keep their existing palettes; tangle-light is not silently retuned to the neutral base light palette. Each known boundary receives a complete baseline before named overrides, so nested scopes do not inherit opposite-mode syntax, status or already-resolved HSL aliases.
--background, --card, --input, and the other shadcn aliases remain HSL channel triples: consume them as hsl(var(--card)). Full-color aliases such as --bg-card, --text-primary and --syntax-keyword remain full CSS colors. --input is the border/off-track, not the recessed field fill (--bg-input). Tailwind's variable-dependent registrations use @theme inline; the named-theme bridge keeps precompiled var(--color-*) consumers scoped too.
@tangle-network/ui's existing useTheme() / ThemeToggle control the document root, not individual islands. useTheme() retains theme and setTheme and also exposes resolvedTheme. With no valid stored preference or explicit initial mode, it follows the system. The hook validates the existing theme storage key, tolerates denied storage, synchronizes mounted controls and cross-tab events, and updates its resolved state on system changes. It sets both mode classes, updates plain mode attributes, switches known named pairs within the same family, and never replaces an unpaired product identity with light or dark.
SSR uses a deterministic dark placeholder for the control without accessing browser APIs; hydration then resolves the client preference. This is not an app-level pre-paint theme script. Apps needing a preference-correct first paint still own their server/cookie or pre-paint initialization. Custom, unpaired themes retain ownership of how their CSS responds to the mode classes.
CodeBlock leaves --syntax-* references in the rendered styles, so the browser resolves them where the code lives and recolors on class, attribute, stylesheet or inline-variable changes without a React rerender. An omitted light prop inherits; light={true} / light={false} explicitly pins a local light/dark scope. Line numbers use the syntax-comment role, not a second palette.
Existing light-default consumers
The Agent App light-default contract is an explicit exception to Brand's default, not a second palette authoring source. Use the generated, opt-in shared-token projection:
@import "tailwindcss";
@import "@tangle-network/brand/styles/legacy-light.css";
@import "@tangle-network/brand/styles/theme.css";
/* Import globals.css separately only when the host already intends those globals. */
Choose this instead of styles, tokens.css and named-themes.css; do not stack the two default contracts. The projection contains canonical declarations with a zero-specificity light root default, keeps all explicit mode selectors, and omits --radius, which the legacy consumer deliberately leaves to its host. It includes no global component rules, keyframes or automatic ladder/system imports.
This export is the shared migration boundary, not a claim that Agent App has already migrated and not a replacement for that package's entire stylesheet. Its component-specific variables, neutral-ramp API, keyframes and structural rules remain consumer-owned until a separate, explicit migration reconciles them. Remove duplicate shared palette declarations in that migration rather than placing competing global token systems on the same page.
The only authoring inputs are src/styles/tokens.css and named-themes.css. Never edit legacy-light.css by hand:
pnpm --filter @tangle-network/brand gen:compat
pnpm --filter @tangle-network/brand check:compat
Brand's build rejects a stale artifact. The main test suite also runs the generation contract: every projected declaration is compared with the canonical source, and color, named-color, typography, radius and motion mutations must change the artifact. The package export is checked too.
Colour ladders (opt in)
@import "@tangle-network/brand/styles";
@import "@tangle-network/brand/styles/ladders.css";
@import "@tangle-network/brand/styles/system.css";
These existing optional styles remain separate. ladders.css defines twelve twelve-step Radix color ramps and role/domain aliases. system.css maps the token families to those ladders and adds semantic roles. It changes every surface of an app that loads it; neither normal Brand nor the compatibility export opts a consumer in.
Both files are generated by scripts/gen-ladders.mjs from its declarations and scripts/radix-ramps.json. Run pnpm --filter @tangle-network/brand gen:ladders after changing those sources. A consumer temporarily copying them ahead of a release copies the generated files verbatim.
Logo
import { Logo, TangleKnot } from "@tangle-network/brand";
<Logo size="lg" />
<Logo size="md" suffix="Sandbox" />
<TangleKnot size={48} />
Palette and typography
Renderers that cannot read CSS variables (PDF, canvas, WebGL, email, the theme-color meta) import palettes from @tangle-network/brand: resolved hex values for light, dark, websiteLight and websiteDark, generated from the stylesheets by pnpm gen:palette. Pages styled with CSS keep using the variables.
The base surface ladder is the one GTM proved out: dark is indigo-lifted #0a0a14 / #191826 / #221f33 / #2c2942; light is a cool #eceef3 canvas, white paper cards and overlays, and #f1f2f7 nested wells. The cast stays below 0.1 chroma and ink stays achromatic, so indigo still reads as the interaction accent, not the base field. Existing named products may supply deliberate surface overrides.
Chrome radii are 6/8/10/12px in both base modes; the composer has its own 26px role. Inter (variable, then static) leads body and display stacks, with Geist fallbacks; Geist Mono leads code with JetBrains Mono and system fallbacks. Runtime font/radius registrations retain their public override names; tests keep Tailwind registration defaults aligned with the canonical declarations.
Existing --transition-* values are preserved. The shared --duration-*, --ease-* and --motion-* vocabulary comes from the existing Agent App contract. It does not apply animations automatically. Reduced motion zeroes those durations and re-resolves composites at theme boundaries.
Focus uses --focus-border, --focus-halo, their danger variants, and --border-strong, derived from the local ring/border palette. UI's focusField / focusRing consume them. globals.css supplies the existing fallback for controls without their own focus treatment.
Fonts
Fonts are referenced, not bundled or remotely fetched. Consumers own loading, privacy and network fallbacks, and an app that loads nothing renders in the OS fallback (DejaVu, Segoe, SF), whose widths and weights the type scale was not tuned for. Load Inter in the app entry. The variable build is one file that renders every weight the scale uses, including in-between weights such as 650 that static files round to the nearest cut:
pnpm add @fontsource-variable/inter @fontsource/geist-mono
import "@fontsource-variable/inter";
import "@fontsource/geist-mono/400.css";
import "@fontsource/geist-mono/500.css";
The --font-sans and --font-display stacks name "Inter Variable" (the family that package registers) ahead of "Inter", so static @fontsource/inter imports keep working. Omitted families fall back through the --font-* stacks. Keep external font imports out of library CSS: they can break reordered CSS imports and should not impose a font download on every consumer.
Policy and development
Shared tokens land here, not in copied app palettes. Additions require a cross-app reason. Version the package with semver; breaking public token changes require a major release. Shared UI and actual consuming apps are the stress-test surfaces.
pnpm install --frozen-lockfile
pnpm --filter @tangle-network/brand check:compat
pnpm typecheck
pnpm build
pnpm test
For actual browser cascade checks, serve the repository with any local static server and open packages/brand/scripts/theme-browser.html. Repeat with light/dark color preferences and reduced motion. The page reports assertions for both default contracts, all named modes, nested scopes, aliases and immediate code-color changes. This CSS fixture does not substitute for the React SSR/hydration tests or a consuming app build.
For local consumer iteration, use workspace linking or a file: dependency, then run that consumer's real build and browser path before release.