Skip to content
KONIGI Patterns

Dashboards / Color / Red/green direction coloring

‹ 4 of 6 ›

Red/green direction coloring

People need to recognize upward and downward changes without relying on color alone.

Updated September 10, 2026

Problem

People may expect red and green to show price movement, but color alone does not make the direction readable for everyone.

Solution

Use the color convention familiar to the intended audience and pair it with a sign, arrow, or other clear direction cue. Choose colors that remain distinguishable on the actual background.

Make the mapping explicit and allow a preference when audiences use different conventions. Keep movement separate from good or bad status; a falling value is not always a problem.

Check the display without color to confirm that the information still makes sense.

Use when

The audience is familiar with directional color conventions and needs to scan frequent changes.

Don’t use when

The audience is general. Outside finance the red-green mapping carries less weight and costs the same accessibility, so a neutral encoding with explicit signs is usually better. And avoid it for anything where “up” is not obviously good or bad—up is good for revenue and bad for latency, and colouring both green because both rose is meaningless.

Trade-offs

Red and green can be difficult to distinguish, and their meanings vary across audiences. They may also conflict with health or error colors elsewhere. Supporting multiple mappings requires consistent labels and testing.

Checklist

  • Is direction encoded by anything other than hue?
  • Do the two colours differ in lightness as well as hue?
  • Does the display survive being viewed in greyscale?
  • Is the up/down colour mapping a user preference for markets that invert it?
  • Does red mean “down” and “error” anywhere on the same screen?
  • Is the neutral or unchanged state visually distinct from both?
  • Are gains and losses distinguishable at the smallest text size used?
  • Has this been checked with a colour vision deficiency simulator?
  • Is “up” unambiguously good or bad for this metric, and does the colour reflect that rather than the sign?
  • Would an arrow or sign make the colour redundant enough to soften it?

Compare

Bloomberg and the terminal tradition established the convention and pair it with dense numeric detail, so the colour is a fast path rather than the only path for a trained reader. Crypto exchanges inherited the convention wholesale and generally add explicit signs and percentages, which makes them accidentally more accessible than the terminals they copied. Trading platforms serving multiple markets expose the colour mapping as a setting, which is the correct structural answer to a convention that is not universal. Grafana has no notion of direction colouring, so a finance dashboard built on it uses value mappings and thresholds to reconstruct it, which at least forces someone to declare the rule explicitly.

Semantic status color expresses condition rather than movement. Delta indicator shows the size and basis of a change. Order book uses related color conventions.

Categorical series palette shares the need for distinguishable colors. KPI tile often presents colored changes to a wider audience.

Red/green direction colouring anatomy The same six price moves twice. On the left, direction is carried by hue alone, so every row looks identical. On the right, the same rows carry a glyph, a signed number, a lightness difference and a position either side of zero. The same six moves, twice 1 2 3 Ticker Move AAPL 1.24 MSFT 0.86 NVDA 2.05 AMZN 0.12 META 0.44 TSLA 1.91 Hue only Ticker Move AAPL +1.24 MSFT −0.86 NVDA +2.05 AMZN −0.12 META +0.44 TSLA −1.91 Glyph, sign, lightness, position 1 GLYPH AND SIGN Reading direction off a triangle is faster than reading it off a hue anyway. 2 LIGHTNESS A light green against a dark red survives greyscale, and so survives most CVD. 3 POSITION Above and below a zero line is structural. It needs no colour at all. Red means up in several Asian markets, so the mapping is a preference, not a constant.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Up is not always good. Churn rising, latency rising, cost rising—a token called --positive would have to lie about all three. These are named for direction and the metric decides what it means.

Tokens
--card--card-foreground--muted-foreground--border--direction-up--direction-down--direction-flat
TickerMove
  • AAPL+1.24
  • MSFT−0.86
  • NVDA+2.05
  • AMZN−0.12
  • META+0.44
  • TSLA−1.91

direction.tsThe metric says whether up is good. The colour follows that, not the sign, and the market mapping is a parameter.

/**
 * Direction is arithmetic. Whether it is good news is a property of the metric.
 *
 * Revenue up is good, churn up is bad, and both are "+12%". A token named for
 * sentiment forces the caller to lie about one of them, so the tokens are named
 * --direction-up / --direction-down and the polarity below decides which one a
 * given metric should wear.
 */
export type Polarity = "higher-is-better" | "lower-is-better" | "neutral";

/**
 * Which hue "up" wears is a market convention, not a constant. Several Asian
 * markets read red for up, so a platform serving both audiences keeps this as
 * a user preference and passes it through.
 */
export type Mapping = "western" | "inverted";

export function directionColor(delta: number, polarity: Polarity, mapping: Mapping = "western"): string {
  if (delta === 0 || polarity === "neutral") return "hsl(var(--direction-flat))";
  const good = polarity === "higher-is-better" ? delta > 0 : delta < 0;
  const up = mapping === "inverted" ? !good : good;
  return up ? "hsl(var(--direction-up))" : "hsl(var(--direction-down))";
}

/**
 * Colour is never the only carrier.
 *
 * The glyph goes with the sign, always, so the reading survives greyscale and
 * the roughly one man in twelve who cannot separate the two hues. In markets
 * that invert the convention this mapping is a user preference, not a constant.
 */
export const directionGlyph = (delta: number) => (delta === 0 ? "→" : delta > 0 ? "▲" : "▼");

MoveTable.tsxGlyph, sign, a bar either side of zero, and hue last. Every row carries direction three ways before colour gets a say.

import { directionColor, directionGlyph, type Mapping, type Polarity } from "./direction";

export type Move = { ticker: string; move: number };

type Props = {
  rows: Move[];
  /** For a price, higher is better from the holder's side. A cost ladder
   *  would pass lower-is-better and the same rows would swap colour. */
  polarity?: Polarity;
  /** The market convention, threaded through from user settings. */
  mapping?: Mapping;
};

/** Signed always, with a real minus. The sign is the cheapest carrier there is. */
const signed = (n: number) => `${n > 0 ? "+" : n < 0 ? "−" : ""}${Math.abs(n).toFixed(2)}`;

/**
 * Four carriers per row and hue is the fourth. The glyph and the sign go
 * with the arithmetic; the bar sits left or right of a zero line, which is
 * structural and needs no colour at all; the tokens differ in lightness, so
 * the hue survives greyscale when it is all that is left.
 */
export function MoveTable({ rows, polarity = "higher-is-better", mapping = "western" }: Props) {
  const max = Math.max(...rows.map((r) => Math.abs(r.move)), 0.01);
  return (
    <div className="rounded-lg border bg-card p-4">
      <div className="flex items-baseline border-b pb-2 text-[11px] uppercase tracking-wide text-muted-foreground">
        <span className="w-14">Ticker</span>
        <span className="w-[4.25rem] text-right">Move</span>
      </div>
      <ul className="mt-1 text-xs tabular-nums">
        {rows.map((r) => {
          const color = directionColor(r.move, polarity, mapping);
          const half = (Math.abs(r.move) / max) * 50;
          return (
            <li key={r.ticker} className="flex items-center gap-2 py-1.5">
              <span className="w-14 text-card-foreground">{r.ticker}</span>
              <span className="w-2.5 text-center text-[9px] leading-none" style={{ color }} aria-hidden="true">
                {directionGlyph(r.move)}
              </span>
              <span className="w-12 text-right" style={{ color }}>{signed(r.move)}</span>
              <span className="relative ml-2 h-2.5 flex-1" aria-hidden="true">
                <span className="absolute inset-y-[-2px] left-1/2 w-px bg-border" />
                <span
                  className="absolute h-full"
                  style={{ background: color, width: `${half}%`, [r.move >= 0 ? "left" : "right"]: "50%" }}
                />
              </span>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

demo.tsxHow it is called: six moves, three each way, in the western mapping.

import { MoveTable } from "./MoveTable";

/** Six price moves, three each way, in the western mapping. */
export default function Demo() {
  return (
    <div className="w-[320px]">
      <MoveTable
        rows={[
          { ticker: "AAPL", move: 1.24 },
          { ticker: "MSFT", move: -0.86 },
          { ticker: "NVDA", move: 2.05 },
          { ticker: "AMZN", move: -0.12 },
          { ticker: "META", move: 0.44 },
          { ticker: "TSLA", move: -1.91 },
        ]}
      />
    </div>
  );
}
What it renders. Identical markup in all three panes, with only the token values changing.

Examples

Screenshots are grouped by product and include the capture date. Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.

Kraken Pro

A trading interface with price charts, an order book, and market-depth displays.

Trade / BTC-USD September 11, 2026 Kraken Pro, signed out (public market data) dense · dark · desktop-web
The order book separates asks and bids by position as well as red and green. A labeled spread sits between them and includes both an absolute value and a percentage. Depth bars behind the rows help compare quantities that are difficult to scan as long decimal values. A price-increment control changes how orders are grouped into levels.