---
title: "Styling"
description: "How outputs draw. The rule that keeps packages portable: outputs are numbers, some numbers are decisions, and the sheet maps decisions to looks. Styling is…"
order: 43
section: "functions"
---

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

# Styling

How outputs draw. The rule that keeps packages portable: outputs are
numbers, some numbers are decisions, and the sheet maps decisions to
looks. Styling is declared in the sheet (or in the `output(...)` options
the sheet is derived from), never computed in the module, so charts
restyle without recompute and every runtime renders the same declaration.
kScript (legacy) styled per call, with size tiers, tooltips, z-order,
palettes, and per-element gradients; this page is the Indicator
vocabulary for each, and which of them have no form yet.

## Placement, per output

- `plot`: `line`, `bar`, `area`, `histogram`, `candle`, `shape`, `scatter`,
  or `""` (`none` in code) for a data-only output: computed, never drawn,
  the building block for gates and palettes below.
- `panel`: `overlay` (the price chart) or `lower` (its own pane). Put
  small-magnitude series (probabilities, oscillators) in `lower`; an
  odds-scale line on the price axis hugs the axis floor.
- `unit`: axis formatting (`price`, `%`, else an abbreviated label). This
  is kScript's `define(format=...)`, per output instead of per script.

## Static style, per output

`color` (or a `colors` palette), `width`, `opacity` (0..1), `line_style`
(`solid`, `dashed`, `dotted`), `description` (the legend text).

## Decisions to looks

- Per-bar coloring: emit the decision as an ordinary data-only output
  (`"plot": ""`), then on the styled output set `color_by: "<that output>"`
  plus a `colors` palette (at least 2 entries); each bar's floored value
  indexes the palette, and a missing or out-of-range index falls back to
  entry 0. That floor + entry-0 rule is the chart engine's `color_by`
  rule, and the host-implemented ladders below adopt it unchanged. An
  output cannot color itself. This is the port of `colorIndex`: the index
  expression becomes an output.
- Per-bar width: the color ladder's sibling. On the styled output set
  `width_by: "<data-only output>"` plus a `widths` ladder (1..10 entries,
  each 0.5..20); each bar's value indexes the ladder (floor; a finite
  out-of-range index clamps to entry 0; a non-finite value, `NaN` or
  infinite, keeps the static width). Both halves or neither, each refused
  by name without the other. An output cannot set its own width.
- Gated markers: a `plot: "shape"` output with `shape_where: "<gate output>"`
  renders only where the gate is nonzero. An output cannot gate itself.
- Shaded bands: top-level `fills: [{ "between": ["upper", "lower"], "color":
  "#94a3b8", "opacity": 0.15 }]`; both sides must be DIFFERENT rendered
  outputs. Sheet-only today: there is no declaration form, and the chart
  does not draw fills yet ([Drawing primitives](drawing-primitives.md)
  has the per-bar box that does).
- Banded ranges: top-level `ranges: [{ "upper": "band_hi", "lower":
  "band_lo", "color": "#94a3b8", "edge_width": 2, "edge_line_style":
  "dotted", "smooth": true }]`, or `range(upper, lower, options)` in
  code; both sides must be DIFFERENT rendered outputs. Instead of the
  static `color`, a `colors` palette plus `color_by` (a data-only output)
  tints the band per bar (floor; finite out-of-range clamps to entry 0; a
  non-finite value = no tint that bar); `color_by` requires `colors`.
  `colors` WITHOUT `color_by` is the band's sign palette: entry 0 tints
  spans where `upper` plots above `lower`, entry 1 where it plots below;
  a single entry tints everywhere, and entries past index 1 are unread.
  `edge_width` is an integer 1..10, `edge_line_style` one of
  `solid`/`dashed`/`dotted`, and `smooth` a boolean smoothing hint.
  Declare as many ranges as needed; the same pair may repeat with
  different options. Ranges ride the sheet for hosts that honor them;
  the chart lane does not draw them today.
- User style knobs: a param with `style: { "output": "<name>", "property":
  "color" | "width" | "opacity" | "lineStyle" }` never reaches the module
  (no accessor, its slot stays zero-filled), shows in the chart settings
  dialog, and redraws without recompute. `color` and `lineStyle` knobs
  take string defaults (`"#22c55e"`); `width` and `opacity` take numeric
  defaults. The bound output must be a rendered one. This is the port of
  kScript's style-only inputs (`type="color"`, a width slider), and it is
  sheet-only: no declaration form yet.

Every reference is cross-validated: `color_by`, `width_by`, `shape_where`,
`style.output`, and each `fills` and `ranges` side must name a declared
(and, where required, rendered) output, none of the three may name its own
output (the self-reference is refused by name), and a publish names the
broken field.

Styling dresses per-output SERIES. When the package should also put text,
tables, shaped marks, or free-standing objects (lines, boxes, polylines,
labels) on the chart, that is the renderer and drawing vocabulary:
top-level `renderers` and `drawings` arrays over named outputs and string
slots, in [Script definition](script-definition.md). Shapes that repeat on
EVERY bar (a zone behind the last few bars, a projection ray from each
bar) are the boxes and segments below.

## Text size, tooltips, alignment, z-order, palettes

The kScript styling vocabulary, item by item:

| kScript | Indicator |
| --- | --- |
| size tiers (`"tiny"` 8 to `"huge"` 24) on text | `size`, an integer pixel count 6..64, on `render.text` and `render.label` |
| `tooltip` on plots, cells, drawings | **not in Indicators yet**; `description` is the legend's long text |
| horizontal and vertical alignment words | **not in Indicators yet** on text; the nine anchors (`top_left` ... `bottom_right`) are a table's `position` |
| `zOrder` / `set_zorder` | **not in Indicators yet**; declaration order is draw order |
| `palette("viridis")`, a fresh mutable array | a `colors` literal array on the output, box, range, or bgcolor; bucket the driving value into its index ([Color functions](color-functions.md)) |
| per-element `glow`, `opacity`, gradients | `opacity` per output or box; a `color_by` ladder per bar; no glow, no continuous gradient |

Value-driven styling, kScript's pattern of computing the style from the
data per element, survives as the ladder: normalize the driving value,
floor it into a bucket, write the bucket to a `none` output, and index a
palette with it. The palette is finite, so the gradient is stepped, and
the decision is a metric anyone can read.

## Boxes and segments

Per-bar shapes over outputs the package already computes, declared as the
top-level `boxes` and `segments` arrays (at most 16 entries each). They are
sheet-only and ABI-neutral like `fills` and `ranges`: no new wire data (the
referenced outputs already stream), legal on both runtime contracts,
rendered by chart hosts and ignored by scalar surfaces. Every referenced
output must be declared and MAY be data-only (`plot: ""`), which is the
usual shape: compute the coordinates, never draw them as lines.

On every bar row `i`:

- nothing is drawn when any referenced output is non-finite on that row, or
  when the optional `when` gate output is `0` or non-finite;
- a box spans bars `i + x_from` .. `i + x_to` inclusive and prices
  `min(top, bottom)` .. `max(top, bottom)`;
- a segment is the straight line from `(i + x_from, y_from)` to
  `(i + x_to, y_to)`.

`x_from` and `x_to` are bar OFFSETS relative to the current bar (negative =
past, positive = ahead; default `0`): an integer in -500..500, or the name
of an output whose per-bar value, truncated to an integer, is the offset.
Offsets past the loaded range clamp to its edge. The forming (last) bar
re-evaluates on every tick: the host replaces that bar's shapes, never
stacks them. When `color` is absent the shape takes the referenced output's
color (`top` for a box, `y_from` for a segment), and when `panel` is absent
it takes that output's panel. Names follow the output grammar and share one
namespace with outputs, renderers, and drawings.

Box fields: `name`, `top`, `bottom`, `x_from`, `x_to`, `when`, `panel`
(`overlay` or `lower`), `color`, `border_color`, `opacity` (0..1, default
0.2), `border_width` (0..10, default 1). Segment fields: `name`, `y_from`,
`y_to`, `x_from`, `x_to`, `when`, `panel`, `color`, `width` (0.5..20,
default 1), `line_style` (`solid`, `dashed`, or `dotted`).

A box on every bar: the last five bars' high-low range, tinted only while
the range is expanding. Three data-only outputs carry the coordinates and
the gate; the sheet turns them into a zone:

```json
{
  "id": "range-box",
  "abi_version": "wrun-1",
  "warmup_bars": 5,
  "params": [],
  "inputSources": {
    "high": { "source": "ohlcv", "field": "high" },
    "low": { "source": "ohlcv", "field": "low" }
  },
  "inputs": [
    { "index": 0, "name": "high" },
    { "index": 1, "name": "low" }
  ],
  "outputs": [
    { "index": 0, "name": "range_hi", "plot": "" },
    { "index": 1, "name": "range_lo", "plot": "" },
    { "index": 2, "name": "expanding", "plot": "" }
  ],
  "boxes": [
    { "name": "range_zone", "top": "range_hi", "bottom": "range_lo", "x_from": -4, "x_to": 0, "when": "expanding", "panel": "overlay", "color": "#f59e0b", "opacity": 0.15, "border_color": "#f59e0b", "border_width": 1 }
  ]
}
```

A segment on every bar: a dotted projection from each bar's anchor line to
a target level, its length set per bar by a data-only output (`reach`, an
output-valued offset):

```json
{
  "id": "projection-segment",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [
    { "index": 0, "name": "anchor", "plot": "line", "panel": "overlay", "color": "#38bdf8" },
    { "index": 1, "name": "target", "plot": "" },
    { "index": 2, "name": "reach", "plot": "" }
  ],
  "segments": [
    { "name": "projection", "y_from": "anchor", "y_to": "target", "x_from": 0, "x_to": "reach", "color": "#38bdf8", "width": 1, "line_style": "dotted" }
  ]
}
```

Code first, both declare from output HANDLES: `output(...)` returns one,
so bind it to a `const` and pass it. `from` / `to` take integer literals or
handles, and the option names are camelCase (`borderColor`, `borderWidth`,
`yFrom`, `yTo`, `lineStyle`), landing in the sheet as the snake_case fields
above. A complete package: a moving average with a one-percent band drawn
as a zone behind the last five bars while the average rises, plus a dotted
ray from each bar's average to the band top three bars ahead:

```typescript
import { box, input, line, none, ohlcv, output, overlay, param, segment } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_mid, out_rising } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 1, max: 200 });
input("close", ohlcv.close);
const mid = output("mid", line, overlay, { color: "#38bdf8" });
const bandHi = output("band_hi", none);
const bandLo = output("band_lo", none);
const rising = output("rising", none);
box("band_zone", { top: bandHi, bottom: bandLo, from: -4, to: 0, when: rising, opacity: 0.15, borderWidth: 0 });
segment("mid_ray", { yFrom: mid, yTo: bandHi, from: 0, to: 3, lineStyle: "dotted" });

let sma = new Sma(20);
let value: f64 = NaN;
let prev: f64 = NaN;

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

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

export function finalize(): void {
  out_mid(value);
  out_band_hi(value * 1.01);
  out_band_lo(value * 0.99);
  out_rising(!isNaN(prev) && value > prev ? 1.0 : 0.0);
  emitRow();
}

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

## A worked example

Bollinger-style bands with a regime-colored (and regime-widened) midline, a
shaded band, a banded range, and a width knob:

```json
{
  "id": "trend-paint",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [
    { "name": "period", "default": 20, "min": 2, "max": 400 },
    { "name": "band_width", "default": 2, "min": 0.5, "max": 4, "description": "Stdev multiples" },
    { "name": "line_width", "default": 3, "style": { "output": "mid", "property": "width" } }
  ],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [
    { "index": 0, "name": "mid", "plot": "line", "panel": "overlay", "color_by": "regime", "colors": ["#ef4444", "#22c55e"], "width_by": "regime", "widths": [1, 3] },
    { "index": 1, "name": "band_hi", "plot": "line", "panel": "overlay", "opacity": 0.6 },
    { "index": 2, "name": "band_lo", "plot": "line", "panel": "overlay", "opacity": 0.6 },
    { "index": 3, "name": "regime", "plot": "" }
  ],
  "fills": [{ "between": ["band_hi", "band_lo"], "color": "#94a3b8", "opacity": 0.15 }],
  "ranges": [{ "upper": "band_hi", "lower": "band_lo", "color": "#94a3b8", "edge_width": 1, "edge_line_style": "dotted" }]
}
```

The module computes four numbers per bar and never mentions a color:

```typescript
import { in_close } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_mid, out_regime } from "./gen/outputs";
import { p_band_width, p_period } from "./gen/params";
import { Sma, Stdev } from "./sdk/ta";

let sma = new Sma(20);
let stdev = new Stdev(20);
let mult: f64 = 2.0;
let mid: f64 = NaN;
let sd: f64 = NaN;
let regime: f64 = 0.0;
let close: f64 = NaN;

export function init(): void {
  const period = i32(p_period());
  sma = new Sma(period);
  stdev = new Stdev(period);
  mult = p_band_width();
}

export function state(): i32 {
  close = in_close();
  mid = sma.update(close);
  sd = stdev.update(close);
  regime = close > mid ? 1.0 : 0.0;
  return isNaN(mid) || isNaN(sd) ? 0 : 1;
}

export function finalize(): void {
  out_mid(mid);
  out_band_hi(mid + mult * sd);
  out_band_lo(mid - mult * sd);
  out_regime(regime);
  emitRow();
}

export function reset(): void {
  sma.reset();
  stdev.reset();
  mid = NaN;
  sd = NaN;
  regime = 0.0;
  close = NaN;
}
```

Note the style knob (`line_width`) has no `p_` accessor and the module never
reads it: the chart applies it to the `mid` line directly. The `regime`
output computes on every bar but never draws; it exists so the palette
(and the `widths` ladder) on `mid` has a decision to index.

In code-first workspaces the `output(...)` options carry `color`, `colors`,
`width`, `opacity`, `line_style`, `color_by`, `shape_where`, `width_by`, and
`widths`, ranges declare as `range(upper, lower, options?)`, boxes and
segments as `box(name, options)` / `segment(name, options)` over output
handles, and declaration order sets the indexes
([Script definition](script-definition.md)). Top-level `fills` and
style-knob params have no declaration form yet, and a generated sheet
refuses hand edits, so a package that needs either stays metadata-first
for now.

## Where the rules live

Every styling reference is validated by the same schema at author, build,
install, and publish, and every refusal names the field
(`outputs.0.color_by`, `boxes.0.color`, `ranges.0.edge_width`); the
messages are in [Common errors](../faq/common-errors.md). The chart
engine's rendering of a declaration is the same on every host that draws
it, because the declaration, not the module, is what it reads.
