Color
Use color for direction, category, and quantity, with visible signs, labels, or shapes carrying the same meaning. tradecn's themes provide separate light and dark palettes. Choose the direction convention your desk expects; the research below informs those choices without establishing one best palette for every reader or a full trading session.
import { useId } from "react"
interface Palette {
background: string
foreground: string
up: string
down: string
stale: string
expiring: string
}
// The six colors used below, included so the comparison can be copied on its own.
const themes: Array<{ name: string; light: Palette; dark: Palette }> = [
{
name: "tradecn-amber",
light: { background: "oklch(0.99 0.004 85)", foreground: "oklch(0.2 0.01 85)", up: "oklch(0.45 0.13 245)", down: "oklch(0.56 0.19 45)", stale: "oklch(0.52 0.13 75)", expiring: "oklch(0.53 0.2 350)" },
dark: { background: "oklch(0.14 0.005 85)", foreground: "oklch(0.94 0.03 85)", up: "oklch(0.66 0.12 240)", down: "oklch(0.78 0.18 45)", stale: "oklch(0.8 0.15 80)", expiring: "oklch(0.74 0.17 350)" },
},
{
name: "tradecn-slate",
light: { background: "oklch(0.985 0.003 250)", foreground: "oklch(0.2 0.02 250)", up: "oklch(0.52 0.13 165)", down: "oklch(0.46 0.19 40)", stale: "oklch(0.52 0.13 75)", expiring: "oklch(0.53 0.2 350)" },
dark: { background: "oklch(0.16 0.01 250)", foreground: "oklch(0.95 0.008 250)", up: "oklch(0.77 0.14 165)", down: "oklch(0.66 0.19 40)", stale: "oklch(0.8 0.15 80)", expiring: "oklch(0.74 0.17 350)" },
},
{
name: "tradecn-slate-east",
light: { background: "oklch(0.985 0.003 250)", foreground: "oklch(0.2 0.02 250)", up: "oklch(0.46 0.19 40)", down: "oklch(0.52 0.13 165)", stale: "oklch(0.52 0.13 75)", expiring: "oklch(0.53 0.2 350)" },
dark: { background: "oklch(0.16 0.01 250)", foreground: "oklch(0.95 0.008 250)", up: "oklch(0.66 0.19 40)", down: "oklch(0.77 0.14 165)", stale: "oklch(0.8 0.15 80)", expiring: "oklch(0.74 0.17 350)" },
},
]
// Machado, Oliveira and Fernandes (2009), severity 1; these matrices operate in linear RGB.
// https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html
const SIMULATIONS: Array<{ id: string; label: string; matrix?: string }> = [
{ id: "none", label: "As drawn" },
{ id: "protanopia", label: "Protanopia", matrix: "0.152286 1.052583 -0.204868 0 0 0.114503 0.786281 0.099216 0 0 -0.003882 -0.048116 1.051998 0 0 0 0 0 1 0" },
{ id: "deuteranopia", label: "Deuteranopia", matrix: "0.367322 0.860646 -0.227968 0 0 0.280085 0.672501 0.047413 0 0 -0.011820 0.042940 0.968881 0 0 0 0 0 1 0" },
{ id: "tritanopia", label: "Tritanopia", matrix: "1.255528 -0.076749 -0.178779 0 0 -0.078411 0.930809 0.147602 0 0 0.004733 0.691367 0.303900 0 0 0 0 0 1 0" },
{ id: "grayscale", label: "Grayscale" },
]
const marks = [
{ token: "up", text: "+0.25 up" },
{ token: "down", text: "−0.31 down" },
{ token: "stale", text: "stale" },
{ token: "expiring", text: "0:09 left" },
] as const
function filterFor(id: string, prefix: string): string | undefined {
if (id === "none") return undefined
if (id === "grayscale") return "grayscale(1)"
return `url(#${prefix}-${id})`
}
export default function ColorDemo() {
const prefix = useId()
return (
<div className="w-fit max-w-full space-y-3 text-xs">
<svg aria-hidden="true" width="0" height="0" className="absolute">
<defs>{SIMULATIONS.filter((s) => s.matrix).map((s) => (
<filter key={s.id} id={`${prefix}-${s.id}`} colorInterpolationFilters="linearRGB">
<feColorMatrix type="matrix" values={s.matrix} />
</filter>
))}</defs>
</svg>
<div role="region" aria-label="Theme color comparisons" tabIndex={0} className="max-w-full overflow-x-auto">
<div className="space-y-4">
{themes.map((theme) => (
<table key={theme.name} className="w-[38rem] border-separate border-spacing-2 text-left">
<caption className="text-left font-medium">{theme.name}</caption>
<thead><tr>
<th scope="col" className="font-medium">Mode</th>
{SIMULATIONS.map((s) => <th key={s.id} scope="col" className="font-medium">{s.label}</th>)}
</tr></thead>
<tbody>{(["light", "dark"] as const).map((mode) => (
<tr key={mode}>
<th scope="row" className="font-normal">{mode === "light" ? "Light" : "Dark"}</th>
{SIMULATIONS.map((s) => (
<td key={s.id}>
<div className="flex flex-col gap-0.5 rounded px-2 py-1.5 whitespace-nowrap lining-nums tabular-nums" style={{ background: theme[mode].background, color: theme[mode].foreground, filter: filterFor(s.id, prefix) }}>
{marks.map(({ token, text }) => <span key={token} style={{ color: theme[mode][token] }}>{text}</span>)}
</div>
</td>
))}
</tr>
))}</tbody>
</table>
))}
</div>
</div>
<p className="max-w-[38rem] text-muted-foreground">Simulations help spot potential confusion. They do not reproduce every reader's vision or establish accessibility; keep the signs and words, and test with readers.</p>
</div>
)
}
What the research says
About one man in twelve has a color vision deficiency, and the common kind makes red and green harder to distinguish (National Eye Institute). Okabe and Ito report red-green deficiency in about one in twelve Caucasian men, one in twenty Asian men, and one in twenty-five African men. They warn about red and green of similar lightness and recommend combining color with shapes, positions, and patterns (Color Universal Design). The direction colors here adapt their palette, including blue, bluish green, and vermilion.
Color also affects interpretation. Bazley, Cronqvist, and Mormann found effects of red on risk preferences, return expectations, and trading decisions across eight experiments, with muted effects in China, where red is associated with prosperity (paper). The university summary describes more persistent pessimism about falling stocks when the information was red rather than black or blue. This supports considering the audience's associations; it does not explain the origin of a market convention. tradecn-slate-east documents red-for-up examples in mainland China, Japan, and Taiwan.
WCAG 2.2's contrast minimum is 4.5 to 1 for ordinary text, or 3 to 1 for large text: at least 18 point, or 14 point bold. scripts/themes.test.ts checks these narrower sets at 4.5 to 1 in both modes:
| Check | Coverage |
|---|---|
| Text | The 13 declared foreground/surface pairs, including page, card, popover, button, muted, accent, and sidebar text. |
| Marks | primary, up, down, stale, expiring, link-1 through link-4, panel-sync, destructive, and ring, each on the background and card. |
| Tints | Foreground text over up-soft, down-soft, flat-soft, stale-soft, and expiring-soft, and over primary and destructive at 15 and 12 percent, each on the background and card. |
| Grid tints | up, down, stale, expiring, destructive, and primary over muted at 50 percent, a grid's focused column, and 60 percent, the depth ladder's mid row, on the background. |
A tint is blended with the surface the way a browser blends it, in gamma-encoded sRGB. Every item puts foreground text on a tint, and state-colored text inside a tinted part takes the foreground too: a state color on a tint can fall below 4.5 to 1 in light mode, and every state also says itself another way, by a sign, a word, or a description. scripts/color.test.ts scans the items for a state color beside a tint. A selected grid row is marked by a bar on its first cell rather than filled, and a focused size cell on the depth ladder's mid row is ringed rather than tinted a second time, for the same reason. A cell's fill flash and a new row's arrival highlight are the exceptions: state-colored text on them can read below 4.5 to 1 while they last.
These calculations do not check every component state, chart color, or consumer override. Check the rendered combinations your app uses.
Dark mode is not a general legibility advantage. Piepenbrock and colleagues found better visual acuity and proofreading with dark text on a light background in both younger and older adults. That study does not establish which trading palette is most comfortable over a full session. Let readers choose their mode; both palettes are included here.
Radix Colors provides separate light and dark versions of its 12-step color scales. tradecn similarly adjusts the marks' lightness for each background while keeping their hue close across modes.
For categories such as chart overlays, the item defaults for chart-1 through chart-8 adapt Okabe and Ito's orange, sky blue, bluish green, yellow, blue, vermilion, reddish purple, and black, with the page's foreground standing in for black. An item installs the tokens it uses and preserves existing values, including a shadcn project's first five chart colors; a theme sets all eight with its own assignments. price-chart uses these tokens and prints overlay labels beside colored dots. Matching a legend entry to its line still depends on color; a named legend alone does not provide a distinct visible pattern for each line.
For magnitude, use a perceptually uniform ramp such as viridis, designed for common color vision deficiencies and an ordered grayscale conversion (the viridis vignette). A signed quantity can use a diverging ramp around a meaningful midpoint: commonly light in a light palette, or dark in a dark palette (Matplotlib's colormap guidance). Avoid relying on a red/green distinction. tradecn ships no heat map or magnitude-ramp token; depth-ladder prints numeric sizes in separate Bid and Ask columns.
The themes
| Theme | Surfaces | Primary | Up | Down | For |
|---|---|---|---|---|---|
tradecn-amber |
Warm paper by day, near-black by night | Amber | Blue | Vermilion | The site's default: the terminal look with a day side |
tradecn-slate |
Cool near-neutral, two sides | Blue | Bluish green | Vermilion | Desks that expect green for up |
tradecn-slate-east |
Slate | Blue | Vermilion | Bluish green | Desks that expect red for up |
Each theme sets every tradecn token in both modes, replaces the palette variables, and resets tradecn's typography tokens to their defaults. It also adds the numeric base rules and accessible-font remap described in Typography. Themes add no runtime mode switcher.
Themes enable variable overwrites for the whole install. Before installing a theme, commit your stylesheet and review the changes with --diff followed by its tailwind.css path from components.json, such as --diff src/index.css. A bare --diff shows no theme CSS. Remove both the flag and its path when applying the theme.
The black, monospace tradecn-terminal and its green/red variant, tradecn-terminal-classic, were removed at 1.0. They reused one palette in both modes; the original terminal also gave up and down the same OKLCH lightness. Existing installations keep their stylesheet values until you replace them.
The direction pair
Items installed without a theme use bluish green for up and vermilion for down. Their light marks match Slate; their dark marks are tuned for shadcn's surfaces and differ from Slate's. Amber is tradecn.dev's default theme and changes up to blue when installed. East swaps Slate's up/down colors and soft tints. These are direction conventions, not judgments about whether a move benefits a position.
Tests require at least 1.3 to 1 between up and down using the repository's luminance calculation. The current ratios are:
| Palette | Light | Dark |
|---|---|---|
| Amber | 1.45 to 1 | 1.33 to 1 |
| Slate and Slate East | 1.54 to 1 | 1.72 to 1 |
| Item defaults | 1.54 to 1 | 1.56 to 1 |
These gaps do not guarantee distinguishability in print or with color vision deficiencies. All are below the 3 to 1 threshold for lightness as an additional visual distinction in WCAG's use-of-color guidance. Both direction colors separately clear 4.5 to 1 on each theme's page and card; the item defaults are tested on shadcn's reference surfaces.
Keep direction visible in a sign, arrow, or word. A ticket's side button says Buy or Sell; the default noninteractive sparkline includes its direction in its accessible name when it has data. A flash cell exposes data-direction, but its displayed content is caller-supplied: pair an absolute price with a signed change or another visible direction cue. A DOM attribute or screen-reader description alone does not provide that visible cue.
Rule 15 of the item contract requires another channel alongside direction color. scripts/color.test.ts inventories source files using direction tokens and looks for each declared channel's source pattern. It does not prove that every rendered use supplies an adequate visual alternative. The demo above compares all three themes in both modes, with signed direction labels and separate status words. Its copyable source includes the six palette values each swatch uses; no repository data file is needed. Scroll the comparison horizontally on a narrow screen.
The protanopia, deuteranopia, and tritanopia views apply Machado, Oliveira, and Fernandes's matrices at severity one in linear RGB; grayscale uses the browser's CSS filter. These views help spot potential confusion. They do not reproduce every reader's vision, establish accessibility, or replace testing with readers.
Light and dark are two palettes
Your app controls the dark class to select a theme's palette. The up/down marks keep their hue while changing lightness for the background. up-soft, down-soft, and expiring-soft use 18 percent opacity, stale-soft uses 16 percent, and flat-soft uses 14 percent. A tint blends with the surface underneath it.
The items' default light marks (up, down, stale, expiring, panel-sync, and the four link-* colors) match Slate's and clear 4.5 to 1 on white and oklch(0.97 0 0). Their dark counterparts are tested on shadcn's oklch(0.145 0 0) background and oklch(0.205 0 0) card. Installing an item without a theme preserves your existing --up; installing a theme replaces it.
What we do not recommend
- Red and green of similar lightness as the only distinction: these can be hard to distinguish with common color vision deficiencies (Okabe and Ito).
- Direction by hue alone: add a visible sign, arrow, or word (WCAG, use of color).
- A rainbow ramp for magnitude: uneven lightness can create apparent boundaries and obscure the order in grayscale (the viridis vignette).
- Reusing a palette without checking the other mode: evaluate contrast against each background (Radix Colors).
- Pure white on pure black as tradecn's default: the dark themes use off-white on near-black. Slate's foreground/background OKLCH lightness values are 0.95/0.16; Amber's are 0.94/0.14. This is a design preference, not a measured reduction in eye strain.
What the research does not settle
The cited studies do not compare these trading-screen palettes over a full session or establish a trader-wide preference for dark mode. The palette adaptations, warm versus cool surfaces, and off-white text on near-black remain design choices. Validate readability, visible direction cues, and mode preferences with the people using your screen.
Sources
Color vision
Behavior and convention
- Bazley, Cronqvist, and Mormann, "Visual Finance: The Pervasive Effects of Red on Investor Behavior"
- The University of Kansas summary of Visual Finance
Contrast and mode
- WCAG 2.2, Understanding Success Criterion 1.4.3, contrast minimum
- WCAG 2.2, Understanding Success Criterion 1.4.1, use of color
- Piepenbrock and colleagues, "Positive display polarity is advantageous for both younger and older adults"
- Nielsen Norman Group, dark mode versus light mode, a research overview
Palettes and ramps
This page is docs/color.md at v2.0.0.