You have an RSI line, a momentum histogram, and a volume ratio on the same chart, and the pane already looks crowded. Every time you want to show how strong a reading is, the instinct is to add another plot: a second line, a threshold band, a few shapes. Before long you are fighting TradingView's limit of 64 plots per script and squinting at a chart that tells you less than it did before.
There is a quieter option. Pine Script lets you paint the plot you already have with a color that changes smoothly with a value, so strength is read from hue instead of from another line. The function behind it is color.from_gradient(), and this guide covers how it works, where it goes wrong, and how to use it on lines, candles, backgrounds, and filled bands.
What color.from_gradient actually does
The function takes a value and a range, and returns a color somewhere between two end colors. Its signature in Pine Script v6 is color.from_gradient(value, bottom_value, top_value, bottom_color, top_color). When value equals bottom_value, you get bottom_color. When it equals top_value, you get top_color. Anywhere in between, the result is a linear blend of the two.
Two details matter in practice. First, values outside the range are clamped to the nearest end color, so a reading far below bottom_value is not "more red", it is the same red as a reading exactly at bottom_value. That is useful for oscillators with soft limits, and misleading if you pick a range that is too tight. Second, the returned color is a series value, which means it can be different on every bar. That is the whole point: one plot, one call, a color that follows the data.
Think of it as a mapping from a number to a color, not as a drawing command. Nothing is drawn by color.from_gradient() itself. You compute a color, then hand it to something that accepts a color: the color argument of plot(), barcolor(), bgcolor(), a label.new() call, or fill().
Your first gradient: an RSI line that shows its own strength
Start with the most common case, a bounded oscillator. RSI moves between 0 and 100, and the interesting zone for most readers sits between 30 and 70. Mapping that zone to a red-to-green gradient makes the line itself carry the information that you would otherwise draw with two hline() calls and some guesswork.
//@version=6
indicator("RSI Gradient Line", overlay = false)
len = input.int(14, "RSI length", minval = 2)
rsiValue = ta.rsi(close, len)
lineColor = color.from_gradient(rsiValue, 30, 70, color.red, color.green)
plot(rsiValue, "RSI", color = lineColor, linewidth = 2)
hline(50, "Midline", color = color.gray)Read it from the bottom up. plot() accepts a series color, so passing lineColor repaints the line segment on every bar. lineColor comes from color.from_gradient(), fed with the RSI value and a 30 to 70 range. Anything under 30 shows as pure red, anything over 70 as pure green, and the stretch around 50 is a muddy in-between shade that honestly reflects "nothing decisive here".
That muddy middle is the first thing people dislike, and it points at a design choice. A two-color gradient blends straight through the middle, and red mixed with green gives a brownish tone. If you want the neutral zone to look neutral, you need a three-stop gradient, which Pine does not have as a single call. We will build one below.
Choosing the range is the real work
The function is trivial. Choosing bottom_value and top_value is where indicators succeed or fail, because the range decides what "strong" means. There are three ways to set it, each with a different trade-off.
A fixed range suits indicators that are bounded by definition. RSI, Stochastic, and MFI live between 0 and 100, so a hard-coded 30 and 70 is stable and easy to explain. You can expose the numbers as inputs so the reader can tune them without touching the code.
A rolling range suits unbounded values such as rate of change, the distance from a moving average, or ATR. The idea is to measure the value against its own recent extremes, so that "strong" means strong for this symbol on this timeframe. The usual tools are ta.highest() and ta.lowest() over a lookback window, or a multiple of ta.stdev().
A normalized range sits in between. You convert the value to a percentile or a z-score first, then feed a fixed range to the gradient. This is the most robust choice when you want the same colors to mean roughly the same thing across different symbols, at the cost of one more calculation to explain.
Here is a rolling-range version applied to candle colors. It colors each bar by its rate of change relative to the largest move seen over the last 100 bars.
//@version=6
indicator("Momentum Gradient Candles", overlay = true)
len = input.int(20, "ROC length", minval = 2)
window = input.int(100, "Normalization window", minval = 10)
roc = ta.roc(close, len)
extent = math.max(ta.highest(math.abs(roc), window), 1e-10)
barcolor(color.from_gradient(roc, -extent, extent, color.red, color.lime))The -extent and extent pair makes the gradient symmetric around zero, so a zero rate of change lands in the middle of the blend and the two sides are treated equally. The math.max(..., 1e-10) guard exists for a reason. On a flat stretch of data the extent can be zero, which would make bottom_value equal top_value and leave the gradient with no range to interpolate over. A tiny floor keeps the call well defined. Early bars, before the lookback fills, produce na, and the bar simply keeps its default color, which is the correct behavior.
If you want to read more about why those early na bars appear and how to keep them from spreading through a calculation, the post on how Pine Script handles na covers it in detail.
Adding a neutral midpoint with two gradients
Since color.from_gradient() blends exactly two colors, a three-zone look needs two calls joined by a condition. Pick the midpoint, use one gradient on each side of it, and make both gradients start or end on the same neutral color so there is no visible jump at the boundary.
//@version=6
indicator("Three Zone RSI Gradient", overlay = false)
len = input.int(14, "RSI length", minval = 2)
v = ta.rsi(close, len)
zoneColor = v >= 50
? color.from_gradient(v, 50, 80, color.gray, color.green)
: color.from_gradient(v, 20, 50, color.red, color.gray)
plot(v, "RSI", color = zoneColor, linewidth = 2)
hline(50, "Midline", color = color.gray)
hline(70, "Upper", color = color.new(color.gray, 70))
hline(30, "Lower", color = color.new(color.gray, 70))Both branches meet at gray when the value is exactly 50, so the line fades out of red or green as it approaches the midline instead of turning brown. The outer limits are 20 and 80 here rather than 30 and 70, which gives the colors more room to build up before they saturate. That is a judgment call. Narrow ranges saturate fast and make everything look extreme; wide ranges keep most bars pale and make the strong readings stand out. If every bar on your chart is fully saturated, the gradient is telling you nothing.
Transparency belongs in the gradient
The colors you pass in can carry their own transparency, and the blend between them follows. That makes background shading practical, because a solid fill across the pane would bury the candles, while a gradient that goes from almost invisible to moderately visible reads as a heat map.
//@version=6
indicator("Volume Heat Background", overlay = true)
avgLen = input.int(50, "Average volume length", minval = 2)
ratio = volume / ta.sma(volume, avgLen)
heat = color.from_gradient(ratio, 0.5, 2.5,
color.new(color.blue, 92), color.new(color.orange, 45))
bgcolor(heat)The ratio compares each bar's volume to its fifty-bar average. At or below half the average, the background is a nearly transparent blue. At two and a half times the average, it is a clearly visible orange. Quiet bars fade into the chart and heavy bars announce themselves, with no extra plot and no shapes.
Remember that color.new(color, transp) takes a transparency where 0 is opaque and 100 is invisible. Passing the same transparency to both ends gives you a pure hue change; passing different values, as above, changes intensity as well. Mixing both channels in one gradient is powerful, but it also makes the result harder to read, so use it where the intensity genuinely matters.
Gradient fills between plots
fill() has its own gradient mode, and it does not use color.from_gradient(). Instead of a computed color, you give it a top value, a bottom value, and two colors, and the fill shades vertically between them. This is the right tool when the thing you want to color is an area, such as a channel or a band.
//@version=6
indicator("Gradient Band Fill", overlay = true)
len = input.int(20, "Length", minval = 2)
mult = input.float(2.0, "Deviation multiplier", minval = 0.5, step = 0.5)
basis = ta.sma(close, len)
dev = mult * ta.stdev(close, len)
upper = plot(basis + dev, "Upper", color = color.new(color.gray, 60))
lower = plot(basis - dev, "Lower", color = color.new(color.gray, 60))
fill(upper, lower, basis + dev, basis - dev,
color.new(color.red, 80), color.new(color.green, 80))The fill runs from a faint red at the upper edge to a faint green at the lower edge. It is a visual aid for where price sits inside the band, not a statement about what will happen next. Keep the transparencies high, around 75 to 90, so that candles stay readable underneath.
Where gradients mislead
A gradient is a visual encoding, and encodings can lie. A few failure modes come up again and again.
The first is the live bar. On a chart that is still forming, the color of the current bar is recomputed on every tick, so it can flicker through the whole gradient before the bar closes. If you take any decision from a color, look at closed bars, or gate your logic with barstate.isconfirmed. The post on barstate.isconfirmed explains the pattern.
The second is color vision. Red to green is the classic gradient and also the worst choice for readers with red-green color blindness. Blue to orange keeps strong contrast for almost everyone, and it also survives both light and dark chart themes better than red and green do.
The third is rolling normalization. A range built from the last 100 bars shifts as the window moves, so an old bar's color can differ from what the same value would produce today. On a historical bar the calculation uses the window available at that bar, which is consistent, but it means colors are comparable inside the chart only loosely. If you need stable meaning, use a fixed range.
The fourth is the style tab. Because the color is computed in code, the reader cannot change it from the indicator's Style settings. If you want users to customize the ends, expose them with input.color() and pass those inputs into the call.
//@version=6
indicator("Configurable Gradient", overlay = false)
lowColor = input.color(color.blue, "Weak color")
highColor = input.color(color.orange, "Strong color")
len = input.int(14, "Length", minval = 2)
v = ta.rsi(close, len)
plot(v, "RSI", color = color.from_gradient(v, 30, 70, lowColor, highColor), linewidth = 2)Gradients versus the other ways to show strength
It helps to be honest about when a gradient is the wrong tool. A gradient shows a continuous quantity that you want to scan across many bars at a glance. If what you actually need is a precise number on the latest bar, a table-based dashboard is better. If you need to mark discrete events, such as a cross or a pivot, shapes and labels from the drawing objects toolkit fit better, because an event is a yes or no, not a degree.
The gradient shines in the middle ground. A trend filter that is more or less in force, a volatility regime that drifts, an oscillator with soft zones: these are quantities where "how much" matters as much as "whether", and where an extra plot would add noise rather than information.
Building these without the trial and error
The code above is short, but the decisions around it are not: which range, which normalization, how to treat early na bars, how to keep the colors legible on a dark theme. Those are the questions that eat an evening when you iterate by hand.
PineScripter is an AI editor for Pine Script v6. You describe the indicator in plain English, for example "color the RSI line from red to green between 30 and 70, with a gray midpoint", and it writes the script, checks it for compile errors the way TradingView does, and fixes errors on its own. Follow-up requests such as "make the range wider" change only the affected lines, and the Code Changes view shows exactly which ones. You can also run the script on the built-in Chart to see the colors on real market data before you paste the code into TradingView. If you want to check the numbers behind an oscillator first, the free RSI calculator shows the value and the Pine Script that computes it. See the AI code editor for how that workflow looks.
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.
Frequently asked questions
What does color.from_gradient do in Pine Script?
It returns a color blended between two end colors based on where a value falls between a bottom value and a top value. Values outside the range take the nearest end color.
Can I use three colors in one gradient?
Not in a single call. Use two color.from_gradient calls joined by a condition, both meeting at the same neutral color at the midpoint.
Why does my gradient look brown in the middle?
Blending red and green directly passes through a muddy tone. Add a neutral midpoint with two gradients, or choose end colors such as blue and orange.
How do I color the background with a gradient?
Compute a color with color.from_gradient, using color.new to set transparency on the end colors, and pass it to bgcolor.
Does the color change on the live bar?
Yes. The color is recomputed on every tick of the forming bar, so judge colors on closed bars or gate logic with barstate.isconfirmed.
Describe your indicator in plain English and get Pine Script that compiles →
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.