---
title: "Color functions"
description: "kScript (legacy) built colors at runtime: color.rgb, color.new, opacity, lighten, darken, blend, and a colorGradient that mapped a value onto a ramp per bar.…"
order: 49
section: "functions"
---

<!-- source: docs/indicators/functions/color-functions.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Color functions

kScript (legacy) built colors at runtime: `color.rgb`, `color.new`,
`opacity`, `lighten`, `darken`, `blend`, and a `colorGradient` that mapped
a value onto a ramp per bar. An Indicator never computes a color. Color
is declared, once, on the output, box, segment, range, or renderer that
carries it, and per-bar color is a ladder: a data-only output holds a
palette index and the declaration names the palette. This page is what
each color function becomes, and which have no form yet.

The reason is the same one behind every declaration: the chart restyles
a package without recompiling it, every host draws the same thing, and
the decision behind a color (a regime, a bucket) is a number a screen or
an alert can read.

```typescript
// Start from a base color, then knock it back to 40% opacity for a softer line.
output("sma20", line, overlay, { color: "#2563eb", opacity: 0.4, width: 2 });
```

## What is available

| kScript | Indicator |
| --- | --- |
| `color.rgb(r, g, b)` | a literal: `"rgb(37, 99, 235)"`, `"#2563eb"`, or `"hsl(221, 83%, 53%)"` |
| `color.new(color, transp)`, `opacity(color, percent)`, `transparency(...)` | the `opacity` option (0..1) on an output or box, or an 8-digit hex literal (`"#2563eb66"`) |
| `brightness`, `lighten`, `darken`, `blend` | **not in Indicators yet**; write the resulting color as a literal |
| `colorGradient(value, range, stops)` | a `colors` palette plus `color_by` over a bucketed output: a stepped gradient |
| `palette("viridis")` | a `colors` literal array; there are no named palettes |
| a hex string or a named constant anywhere a color is expected | the same strings; box fills need hex, `rgb()`, or `hsl()` because the fill carries the opacity ([Color constants](../core-concepts/color-constants.md)) |

Runtime color math (`lighten`, `darken`, `blend`, a continuous gradient)
waits on a color output channel; today color is declared and `color_by`
is a bucket ladder.

## Data-driven color with a ladder

`colorGradient` is the one to reach for when color should encode a
value. The Indicator form: normalize the value into `0 .. n - 1`, floor
it, write it to a `none` output, and put an `n`-entry `colors` palette on
the drawn output with `color_by` naming the index. Each bar's floored
value indexes the palette; a missing or out-of-range value falls back to
entry 0. The ramp is as fine as the palette is long, and the palette is
yours: five stops of a viridis ramp below.

```typescript
import { box, input, line, lower, none, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_heat, out_rsi, out_sma20, out_trend, out_trend_up } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Rsi, Sma } from "./sdk/ta";

param("period", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
// colorGradient(rsi, [0, 100], palette("viridis")) as a five-step ladder: cool when oversold, hot when overbought.
output("rsi", line, lower, { width: 2, color_by: "heat", colors: ["#440154", "#3b528b", "#21918c", "#5ec962", "#fde725"], description: "RSI colored by its own level" });
output("heat", none, lower, { description: "0..4: which fifth of 0..100 the RSI sits in" });
// opacity(color, 40) as a declared opacity.
output("sma20", line, overlay, { color: "#2563eb", opacity: 0.4, width: 2, description: "A faded average" });
// blend(green, red, weight) per regime as two literals and a 0/1 index.
output("trend", line, overlay, { width: 2, color_by: "trend_up", colors: ["#dc2626", "#16a34a"], description: "The average, red falling, green rising" });
output("trend_up", none, overlay, { description: "1 while the average rises" });
// An 8-digit hex fill: the last two digits are the alpha.
const bandHi = output("band_hi", none, overlay);
const bandLo = output("band_lo", none, overlay);
box("band", { top: bandHi, bottom: bandLo, color: "#2563eb", opacity: 0.12, borderWidth: 0 });

let rsi = new Rsi(14);
let sma = new Sma(20);
let strength: f64 = NaN;
let average: f64 = NaN;
let prevAverage: f64 = NaN;

export function init(): void {
  rsi = new Rsi(i32(p_period()));
  sma = new Sma(20);
}

export function state(): i32 {
  const close = in_close();
  strength = rsi.update(close);
  prevAverage = average;
  average = sma.update(close);
  return isNaN(strength) || isNaN(average) ? 0 : 1;
}

export function finalize(): void {
  // Bucket 0..100 into five steps; 100 itself lands in the last bucket.
  let bucket = Math.floor(strength / 20.0);
  if (bucket > 4.0) bucket = 4.0;
  out_rsi(strength);
  out_heat(bucket);
  out_sma20(average);
  out_trend(average);
  out_trend_up(!isNaN(prevAverage) && average >= prevAverage ? 1.0 : 0.0);
  out_band_hi(average * 1.01);
  out_band_lo(average * 0.99);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  sma.reset();
  strength = NaN;
  average = NaN;
  prevAverage = NaN;
}
```

The `heat` output is a metric: `om metric series` on it returns the
bucket per bar, which is the kind of value a screen ranks by.

## Tinting bars by value

The same ladder drives a background tint through `render.bgcolor`, the
port of `plotBgColor` and `barcolor`: a `where` gate says which bars get
a tint, and `color_by` with `colors` picks it per bar. A non-finite index
means no tint on that bar (the static `color` never substitutes), so a
`NaN` bucket is a clean way to leave a bar alone.

```typescript
import { input, line, lower, none, ohlcv, output, param, render } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_extreme, out_rsi, out_zone } from "./gen/outputs";
import { p_overbought, p_oversold, p_period } from "./gen/params";
import { Rsi } from "./sdk/ta";

param("period", 14, { min: 2, max: 100, description: "RSI Period" });
param("overbought", 70, { min: 50, max: 100, description: "Overbought Level" });
param("oversold", 30, { min: 0, max: 50, description: "Oversold Level" });
input("close", ohlcv.close);
output("rsi", line, lower, { color: "#2962ff", width: 2, description: "Relative strength index" });
output("zone", none, lower, { description: "0 oversold, 1 overbought: the tint ladder index; NaN in between" });
output("extreme", none, lower, { description: "1 on bars in either zone: the tint gate" });
// plotBgColor(...) per condition: one declaration, a gate, and a two-entry ladder.
render.bgcolor("zones", { where: "extreme", color_by: "zone", colors: ["rgba(76, 175, 80, 0.3)", "rgba(244, 67, 54, 0.3)"] });

let rsi = new Rsi(14);
let overbought: f64 = 70.0;
let oversold: f64 = 30.0;
let value: f64 = NaN;

export function init(): void {
  rsi = new Rsi(i32(p_period()));
  overbought = p_overbought();
  oversold = p_oversold();
}

export function state(): i32 {
  value = rsi.update(in_close());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  const hot = value > overbought;
  const cold = value < oversold;
  out_rsi(value);
  out_zone(cold ? 0.0 : hot ? 1.0 : NaN);
  out_extreme(hot || cold ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  value = NaN;
}
```

kScript's five nested `plotBgColor` branches (extreme oversold, oversold,
neutral, overbought, extreme overbought) are a five-entry palette and a
bucket that lands in `0 .. 4`; the module above keeps two for clarity.

## Transforms that have no form

`brightness`, `lighten`, `darken`, and `blend` mixed a color at runtime.
**Not in Indicators yet.** Compute the color you want ahead of time and
write it as a literal; a `blend(a, b, 0.5)` that depended on a per-bar
weight becomes a ladder over a few blended literals, and `blend.amount`
becomes the bucket. There is no color type in the module: a param cannot
hold a color, a string slot could carry one but no renderer reads a
color from a slot, and every `colors` entry is a string literal the
extractor reads without running the code.
