All posts

Build a React Crypto Price Chart with Candlesticks, Volume, and Indicators

A React crypto price chart usually needs more than a line chart. For trading interfaces, candlesticks, volume, time intervals, zoom, and technical indicators are often part of the core experience.

This guide shows how to build a React crypto candlestick chart with react-candlesticks, using public OHLCV/kline data from Binance. The example fetches crypto candles for pairs such as BTCUSDT and ETHUSDT, maps them into chart data, then renders candlesticks, volume bars, and optional indicators using React components.

React crypto price chart with candlesticks, volume, Bollinger Bands, SMA, and Stochastic indicators
A React crypto chart can combine price candles, volume, and optional indicator panels.

You can also try the live StackBlitz demo, view the GitHub example project, or browse more examples in the React Candlesticks docs.

What you will build

In this tutorial, you will create a React crypto chart with:

  • BTC, ETH, SOL, and BNB symbol selection
  • 15 minute, 1 hour, 4 hour, and 1 day candles
  • Candlestick, OHLC bar, and area chart modes
  • Volume bars synchronized with the price chart
  • SMA, EMA, and Bollinger Bands overlays
  • RSI and MACD indicator panels
  • Binance kline data mapped into a React chart data model

The result is a small crypto charting interface that you can build on for a more complete trading or market analysis app.

Install React Candlesticks

Install the package:

npm install react-candlesticks

The full source is available in the GitHub example project.

Import the stylesheet once in your app:

import 'react-candlesticks/style.css';

The chart is built with React components, so the main chart layout is expressed in JSX:

<Chart data={data} granularity="h1" theme="dark">
  <Panel heightRatio={3}>
    <Candlesticks />
    <SMA period={20} />
    <BollingerBands />
  </Panel>

  <Panel>
    <VolumeBars />
  </Panel>

  <Panel>
    <RSI />
  </Panel>
</Chart>

That composition model is useful when you want to build custom trading UIs in React rather than embed a fixed chart widget.

Fetch crypto candles from Binance

Binance calls candlestick data klines. Each kline contains open time, open, high, low, close, volume, and other fields.

For a React crypto candlestick chart, the important values are:

  • time
  • open
  • high
  • low
  • close
  • volume

react-candlesticks expects those values in a DataPoint shape. The example below converts Binance kline rows into chart data.

import type { ChartProps, DataPoint } from 'react-candlesticks';

type BinanceInterval = '15m' | '1h' | '4h' | '1d';
type Granularity = NonNullable<ChartProps['granularity']>;

const INTERVAL_TO_GRANULARITY: Record<BinanceInterval, Granularity> = {
  '15m': 'm15',
  '1h': 'h1',
  '4h': 'h4',
  '1d': 'd1',
};

type BinanceKline = [
  number, // open time
  string, // open
  string, // high
  string, // low
  string, // close
  string, // volume
  number, // close time
  string, // quote asset volume
  number, // number of trades
  string, // taker buy base asset volume
  string, // taker buy quote asset volume
  string, // unused
];

function toDataPoint(kline: BinanceKline): DataPoint {
  return {
    time: new Date(kline[0]).toISOString(),
    open: Number(kline[1]),
    high: Number(kline[2]),
    low: Number(kline[3]),
    close: Number(kline[4]),
    volume: Number(kline[5]),
  };
}

async function fetchCryptoCandles(
  symbol: string,
  interval: BinanceInterval,
  signal?: AbortSignal,
) {
  const params = new URLSearchParams({
    symbol,
    interval,
    limit: '300',
  });

  const response = await fetch(
    `https://api.binance.com/api/v3/klines?${params}`,
    { signal },
  );

  if (!response.ok) {
    throw new Error(`Binance request failed: ${response.status}`);
  }

  const rows = (await response.json()) as BinanceKline[];
  return rows.map(toDataPoint);
}

For production applications, you may want to route market data through your own API. That lets you cache popular symbol and interval combinations, handle provider failures, normalize data from multiple sources, and keep provider-specific logic away from your UI components.

Build the React crypto chart component

The main component owns the selected symbol, interval, chart type, and indicator state.

When the user changes the symbol or interval, the component fetches a new set of OHLCV candles and redraws the chart.

import 'react-candlesticks/style.css';
import './App.css';

import { useEffect, useMemo, useState } from 'react';
import type { ChartProps, DataPoint } from 'react-candlesticks';
import {
  Area,
  BollingerBands,
  Candlesticks,
  Chart,
  EMA,
  MACD,
  OhlcBars,
  Panel,
  RSI,
  SMA,
  VolumeBars,
} from 'react-candlesticks';

type BinanceInterval = '15m' | '1h' | '4h' | '1d';
type ChartType = 'candles' | 'ohlc' | 'area';
type OverlayIndicator = 'sma' | 'ema' | 'bollinger';
type PanelIndicator = 'rsi' | 'macd';
type Granularity = NonNullable<ChartProps['granularity']>;

const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'] as const;
const INTERVALS: BinanceInterval[] = ['15m', '1h', '4h', '1d'];
const CHART_TYPES: ChartType[] = ['candles', 'ohlc', 'area'];

const OVERLAY_INDICATORS: OverlayIndicator[] = ['sma', 'ema', 'bollinger'];
const PANEL_INDICATORS: PanelIndicator[] = ['rsi', 'macd'];

const INTERVAL_TO_GRANULARITY: Record<BinanceInterval, Granularity> = {
  '15m': 'm15',
  '1h': 'h1',
  '4h': 'h4',
  '1d': 'd1',
};

type BinanceKline = [
  number,
  string,
  string,
  string,
  string,
  string,
  number,
  string,
  number,
  string,
  string,
  string,
];

function toDataPoint(kline: BinanceKline): DataPoint {
  return {
    time: new Date(kline[0]).toISOString(),
    open: Number(kline[1]),
    high: Number(kline[2]),
    low: Number(kline[3]),
    close: Number(kline[4]),
    volume: Number(kline[5]),
  };
}

async function fetchCryptoCandles(
  symbol: string,
  interval: BinanceInterval,
  signal?: AbortSignal,
) {
  const params = new URLSearchParams({
    symbol,
    interval,
    limit: '300',
  });

  const response = await fetch(
    `https://api.binance.com/api/v3/klines?${params}`,
    { signal },
  );

  if (!response.ok) {
    throw new Error(`Binance request failed: ${response.status}`);
  }

  const rows = (await response.json()) as BinanceKline[];
  return rows.map(toDataPoint);
}

export default function App() {
  const [symbol, setSymbol] = useState<(typeof SYMBOLS)[number]>('BTCUSDT');
  const [interval, setInterval] = useState<BinanceInterval>('1h');
  const [chartType, setChartType] = useState<ChartType>('candles');

  const [overlays, setOverlays] = useState<Record<OverlayIndicator, boolean>>({
    sma: true,
    ema: false,
    bollinger: true,
  });

  const [panels, setPanels] = useState<Record<PanelIndicator, boolean>>({
    rsi: false,
    macd: true,
  });

  const [data, setData] = useState<DataPoint[]>([]);
  const [status, setStatus] = useState('Loading candles...');

  useEffect(() => {
    const controller = new AbortController();

    setStatus('Loading candles...');

    fetchCryptoCandles(symbol, interval, controller.signal)
      .then((candles) => {
        setData(candles);
        setStatus('');
      })
      .catch((error: unknown) => {
        if (error instanceof DOMException && error.name === 'AbortError') {
          return;
        }

        setStatus('Unable to load crypto data. Try another interval or symbol.');
      });

    return () => controller.abort();
  }, [symbol, interval]);

  const granularity = INTERVAL_TO_GRANULARITY[interval];

  const hasPanelIndicator = useMemo(
    () => Object.values(panels).some(Boolean),
    [panels],
  );

  const toggleOverlay = (indicator: OverlayIndicator) => {
    setOverlays((current) => ({
      ...current,
      [indicator]: !current[indicator],
    }));
  };

  const togglePanel = (indicator: PanelIndicator) => {
    setPanels((current) => ({
      ...current,
      [indicator]: !current[indicator],
    }));
  };

  return (
    <main className="crypto-dashboard">
      <div className="toolbar" aria-label="Crypto chart controls">
        <label>
          Symbol
          <select
            value={symbol}
            onChange={(event) => {
              setSymbol(event.target.value as typeof symbol);
            }}
          >
            {SYMBOLS.map((item) => (
              <option key={item} value={item}>
                {item}
              </option>
            ))}
          </select>
        </label>

        <label>
          Granularity
          <select
            value={interval}
            onChange={(event) => {
              setInterval(event.target.value as BinanceInterval);
            }}
          >
            {INTERVALS.map((item) => (
              <option key={item} value={item}>
                {item}
              </option>
            ))}
          </select>
        </label>

        <label>
          Chart type
          <select
            value={chartType}
            onChange={(event) => {
              setChartType(event.target.value as ChartType);
            }}
          >
            {CHART_TYPES.map((item) => (
              <option key={item} value={item}>
                {item}
              </option>
            ))}
          </select>
        </label>

        <div className="toggle-group" aria-label="Overlay indicators">
          {OVERLAY_INDICATORS.map((indicator) => (
            <button
              key={indicator}
              type="button"
              className={overlays[indicator] ? 'is-active' : ''}
              onClick={() => toggleOverlay(indicator)}
            >
              {indicator}
            </button>
          ))}
        </div>

        <div className="toggle-group" aria-label="Panel indicators">
          {PANEL_INDICATORS.map((indicator) => (
            <button
              key={indicator}
              type="button"
              className={panels[indicator] ? 'is-active' : ''}
              onClick={() => togglePanel(indicator)}
            >
              {indicator}
            </button>
          ))}
        </div>
      </div>

      <section
        className="chart-wrap"
        aria-label={`${symbol} ${interval} React crypto candlestick chart`}
      >
        {status ? <p className="chart-status">{status}</p> : null}

        {data.length > 0 ? (
          <Chart
            data={data}
            granularity={granularity}
            theme="dark"
            initialScrollToLatest
          >
            <Panel heightRatio={3}>
              {chartType === 'candles' ? <Candlesticks /> : null}
              {chartType === 'ohlc' ? <OhlcBars /> : null}
              {chartType === 'area' ? <Area /> : null}

              {overlays.sma ? <SMA period={20} /> : null}
              {overlays.ema ? <EMA period={50} /> : null}
              {overlays.bollinger ? <BollingerBands /> : null}
            </Panel>

            <Panel>
              <VolumeBars />
            </Panel>

            {hasPanelIndicator && panels.rsi ? (
              <Panel>
                <RSI />
              </Panel>
            ) : null}

            {hasPanelIndicator && panels.macd ? (
              <Panel>
                <MACD />
              </Panel>
            ) : null}
          </Chart>
        ) : null}
      </section>
    </main>
  );
}

The price panel changes layer based on chartType. Overlay indicators stay in the main price panel, while RSI and MACD render as separate panels below volume. That keeps the chart layout clear and makes it easier to add more indicators later.

Add the page styles

The chart needs a parent element with a real height. The rest of the CSS keeps the controls compact and gives active indicators a clear selected state.

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
}

.crypto-dashboard {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  min-height: 100vh;
  background: #151515;
  color: #e5e7eb;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: end;
  padding: 14px;
  border-bottom: 1px solid rgb(148 163 184 / 24%);
  font-size: 12px;
}

.toolbar label {
  display: grid;
  gap: 5px;
  color: #94a3b8;
  font-size: 12px;
}

.toolbar select,
.toggle-group button {
  min-height: 34px;
  border: 1px solid rgb(148 163 184 / 35%);
  border-radius: 4px;
  background: #111827;
  color: #f8fafc;
  font: inherit;
}

.toolbar select {
  padding: 0 10px;
}

.toggle-group {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.toggle-group button {
  padding: 0 10px;
  text-transform: uppercase;
}

.toggle-group button.is-active {
  border-color: #38bdf8;
  background: #075985;
}

.chart-wrap {
  position: relative;
  min-height: 0;
}

.chart-status {
  position: absolute;
  inset: 16px auto auto 16px;
  z-index: 1;
  margin: 0;
  color: #cbd5e1;
}

Why map Binance intervals to chart granularity?

The data provider and the chart library use different names for the same idea.

Binance intervals use values such as:

'15m' | '1h' | '4h' | '1d'

react-candlesticks granularities use values such as:

'm15' | 'h1' | 'h4' | 'd1'

Keeping that mapping explicit makes the chart easier to maintain. If you later add 5 minute candles, weekly candles, or a different data provider, you only need to update one small mapping rather than spread provider-specific values throughout your React components.

Why use panels for volume and indicators?

A crypto trading chart often combines several different types of data. Price candles, volume, overlays, and oscillator indicators are easier to read when they are not all forced onto one scale.

Some layers belong on the price scale. For example, SMA, EMA, and Bollinger Bands are plotted against price, so they work well as overlays in the main price panel.

Other indicators need their own vertical scale. RSI and MACD are easier to read in separate panels because they do not share the same units as price.

The example uses this split:

<Chart data={data} granularity={granularity} theme="dark">
  <Panel heightRatio={3}>
    <Candlesticks />
    <SMA period={20} />
    <BollingerBands />
  </Panel>

  <Panel>
    <VolumeBars />
  </Panel>

  <Panel>
    <RSI />
  </Panel>

  <Panel>
    <MACD />
  </Panel>
</Chart>

This keeps the chart declarative: the JSX describes the chart structure, and React state controls which layers are rendered.

Why use React Candlesticks for a crypto chart?

react-candlesticks is designed for React trading interfaces where the chart is part of a wider application.

The chart is composed from React components, while rendering is handled with Canvas for performance. That means you can build the surrounding app with normal React state, props, routing, and UI components, while still rendering large financial datasets efficiently.

For a simple hosted widget, a full charting platform may be enough. For a custom React trading UI, a component-based chart API can be easier to integrate and extend.

Production notes for crypto market data

The Binance endpoint is convenient for a tutorial because it returns OHLCV candles directly. Real applications usually need a few extra pieces around the request.

For production, consider:

  • caching popular symbol and interval combinations
  • handling provider rate limits
  • retrying temporary failures
  • normalizing data if you support more than one provider
  • validating response data before rendering
  • using your own API layer instead of calling the provider directly from the browser
  • using a WebSocket stream if you need live candle updates after the initial REST load

For a basic historical crypto price chart, a REST request is often enough. For live trading dashboards, you will usually want an initial candle load followed by streaming updates.

Next steps

You now have a React crypto price chart that loads OHLCV data, changes symbol, changes interval, switches chart type, and renders volume and technical indicators.

From here, you could add:

  • live candle updates with a WebSocket stream
  • a watchlist beside the chart
  • saved chart preferences
  • custom themes
  • alert lines
  • drawings
  • more indicators
  • comparison views for multiple crypto pairs

Try the live demo, view the GitHub example project, follow the getting started guide, browse more examples, or read the candlestick chart with volume and indicators tutorial.

npm install react-candlesticks

FAQ

Does this work with Next.js?

Yes. For Next.js apps, make sure the chart is rendered as a client component because it uses browser rendering. You can read the separate Next.js candlestick chart guide for a focused example.

Is this only for Binance data?

No. Binance is used here because the public kline endpoint is convenient for a tutorial. You can use any provider if you map its data into the DataPoint shape with time, open, high, low, close, and optionally volume.

Can I use this for stocks as well as crypto?

Yes. The chart components are not crypto-specific. They can be used for stocks, ETFs, futures, forex, or other OHLCV datasets if you provide the appropriate data.