BloxwapChart SDK
Guides

Header, control rail and scale buttons

Add a TradingView-style top bar or a vertical control rail with timeframes, chart types, indicators, and price-scale toggles.

The chart header is an optional top bar from @bloxwap/chart/ui. It holds the symbol, a timeframe switcher that drives your datafeed, a chart-type menu, an Indicators button, Auto / % / Log price-scale toggles, and slots for your own controls. It is a normal-flow element: put its container above the chart. Nothing else depends on it, so skip it if you keep your own header.

Add the header

<div id="chart-header"></div>
<div id="stage" style="position: relative; height: 480px">
  <canvas id="chart"></canvas>
</div>
import { createChartHeader, createIndicatorsDialog, BLOXWAP_HEADER_THEME } from '@bloxwap/chart/ui';

const indicators = createIndicatorsDialog({ chart, document, theme: 'dark' });
const header = createChartHeader({
  chart,
  document,
  container: document.querySelector<HTMLElement>('#chart-header')!,
  symbol: 'BTC',
  datafeed,                                   // picking 1h calls datafeed.setSymbol('BTC', 3_600_000)
  onTimeframeChange: (tf) => save(tf.label),
  onIndicators: () => indicators.openPicker(), // the Indicators button only shows with this
  slots: { left: [assetPicker], right: [screenshotButton] },
  flyouts: toolbar.flyouts,                   // optional: share menus with the drawing toolbar
  tokens: BLOXWAP_HEADER_THEME,               // optional: the bloxwap.pro look on dark
});

header.setSymbol('ETH');   // label and symbol for later picks; does not call the datafeed
header.destroy();

The layout is [symbol] [timeframes] [chart type] [Indicators] [left slot] … [Auto % Log] [right slot].

Options

OptionDefaultNotes
chart, document, containerRequiredThe bar is appended to container
symbolNoneSymbol passed to the datafeed; also the label unless symbolLabel is set
symbolLabelsymbolText or an element. No symbol and no label hides it
timeframesDEFAULT_TIMEFRAMES1m 5m 15m 1h 4h 1d 3d 1w 1M; [] hides the switcher
intervalMs900_000 (DEFAULT_HEADER_INTERVAL_MS, 15m)Interval selected at start
datafeedNoneAny object with setSymbol(symbol, intervalMs)
onTimeframeChangeNoneCalled after the user picks a different timeframe
chartTypesDEFAULT_CHART_TYPESCandles, hollow candles, Heikin Ashi, bars, line, area; [] hides the menu
onChartTypeChangeNoneCalled after a pick, which already set series.type
onIndicatorsNoneShows the Indicators button, which calls it
scaleButtonstrueAuto / % / Log toggles
slotsNone{ left?, right? } arrays of your elements
flyoutsIts ownShare the drawing toolbar's Flyouts so only one menu is open at a time
theme'dark'setTheme('light') switches later
compact'auto'Collapse the timeframes into a dropdown, and the Indicators button to its icon, when the bar overflows; true or false forces it
tokensNoneCustom properties set inline on the bar and its menus: one set for both themes, or { dark?, light? }

DEFAULT_TIMEFRAMES uses Hyperliquid's intervals. 1M spans 30 days (2_592_000_000 ms). Pass your own HeaderTimeframe[] of { label, intervalMs } to change them. An interval outside the list shows a generated label such as 2h.

Working with a datafeed

When the datafeed reports its own symbol and intervalMs, as the paging datafeed does, the header treats them as the truth:

  • It starts from the datafeed's symbol and interval.
  • A timeframe pick reloads the symbol the datafeed has loaded.
  • With subscribeState, it follows switches made elsewhere, such as your asset picker calling datafeed.setSymbol.

For a datafeed without subscribeState, call header.refresh() after switching elsewhere.

Picking a timeframe does not touch timeAxis.intervalMs or timeScale.intervalMs. The paging datafeed sets both on every load. With another datafeed, update them in onTimeframeChange if your chart relies on them.

Handle

MemberPurpose
elementThe bar element
symbol, intervalMs, compactCurrent state
setSymbol(symbol, label?)Sets the symbol for later picks (unless the datafeed reports its own) and the label; does not call the datafeed
setInterval(ms)Marks an interval selected; calls neither the datafeed nor onTimeframeChange
setChartType(type)Applies series.type and updates the menu; does not call onChartTypeChange
setTheme(theme)Switch between the dark and light tokens, and to that theme's tokens set
refresh()Re-read the datafeed's symbol and interval and re-fit the bar
destroy()Remove the bar, its menus, and every listener

The chart-type button and scale toggles follow series and priceAxis changes as soon as they render, through subscribeConfigChange: the toolbar's Alt+P and Alt+L, the settings card, the context menu, and your own updateConfig calls all show at once.

Every control is its own tab stop, and dropdowns work from the keyboard: arrows, Home, and End move through entries, Escape closes the menu (from anywhere on the page, so a menu opened with a click or tap closes too) and returns focus to its button, and Tab closes it and continues from its button. Menus stack above the settings card and the indicators dialog.

Control rail

createControlRail puts the same controls in a vertical rail beside the drawing toolbar, so the plot starts at the top of the card. This is the layout bloxwap.pro and the playground use on wide screens. It holds a timeframe menu, the chart-type menu, the Indicators button, and the Auto / % / Log toggles, then your own controls.

<div style="display: flex; height: 480px">
  <div id="controls" style="display: flex"></div>
  <div id="tools" style="width: 52px"></div>
  <div id="stage" style="position: relative; flex: 1"><canvas id="chart"></canvas></div>
</div>
import { createControlRail } from '@bloxwap/chart/ui';

const rail = createControlRail({
  chart,
  document,
  container: document.querySelector<HTMLElement>('#controls')!,
  datafeed,                                    // picking 1h reloads the loaded symbol at 3_600_000
  flyouts: toolbar.flyouts,                    // one menu open at a time with the drawing rail
  onIndicators: () => indicators.openPicker(),
  slots: { top: [undoButton, redoButton], bottom: [settingsButton] },
});

rail.slots.bottom.append(snapshotButton);     // slots are live containers
rail.setTheme('light');
rail.destroy();

The layout, top to bottom, is [timeframe] [chart type] [Indicators] — [Auto % Log] — [top slot] … [bottom slot]. Dividers only appear between groups that are shown. The top slot scrolls with the built-in controls, and the rail adds paging arrows when they overflow. The bottom slot stays pinned at the foot.

It takes the header's symbol, timeframes, intervalMs, datafeed, onTimeframeChange, chartTypes, onChartTypeChange, onIndicators, scaleButtons, flyouts, and theme options, with the same defaults and the same datafeed rules. slots is { top?, bottom? }. The handle has element, slots (the top and bottom containers), symbol, intervalMs, setSymbol, setInterval, setChartType, setTheme, refresh, and destroy, which behave like the header's.

Menus open to the right of their button, on click or mouse hover like the drawing rail's, and work from the keyboard like the header's dropdowns. The rail uses the shared toolbar tokens (--cts-accent, --cts-accent-soft, and so on) rather than --cts-header-*: set them on .cts-theme to restyle it along with the drawing rail. A pressed scale toggle takes the accent color on the soft accent fill.

To switch layouts by width, as the playground does, create both. Hide the rail's container below a breakpoint and the header's above it, then move your own buttons between rail.slots and the header's slots.right container. Both follow the datafeed, so they stay in step.

Scale buttons on the chart

TradingView shows A, %, and L toggles at the foot of the price axis. createScaleButtons adds the same strip above the time axis:

import { createScaleButtons } from '@bloxwap/chart/ui';

const buttons = createScaleButtons({ chart, document, overlay: stage }); // stage: positioned, wraps the canvas
buttons.destroy();
OptionMeaning
chart, document, overlayRequired. overlay is a positioned element wrapping the canvas, such as the toolbar's overlay
canvasThe canvas, when it doesn't sit at the overlay's top-left corner
themeDefault 'dark'
tokensCustom properties set inline, for example BLOXWAP_HEADER_THEME; setTheme swaps per-theme sets

The strip follows the axis to either side, tracks resizes and sub-panes, and hides while the price axis is hidden. It does nothing on plain scroll or zoom frames. Call buttons.refresh() if the canvas moves inside the overlay without resizing.

The toggles edit existing config:

  • Auto toggles priceAxis.autoScale.
  • % and Log switch priceAxis.mode to 'percent' or 'logarithmic', or back to 'regular', so they exclude each other. They also release a locked price-to-bar ratio, like Alt+P and Alt+L. Indexed mode lights neither.

For your own controls, use toggleScale(chart, 'auto' | 'percent' | 'log'), scaleToggleState(chart.getConfig().priceAxis), and SCALE_TOGGLES (labels, letters, and titles). Toggle groups already follow each rendered priceAxis change; call syncScaleToggles(chart) only to update them inside a chart.batch.

Theming

Set these custom properties on the header, on the scale strip, or on any ancestor. Each falls back to the toolbar token shown, so dark and light follow .cts-theme and .cts-light automatically.

PropertyFalls back toStyles
--cts-header-bg--cts-panelBar and on-chart button fill
--cts-header-text--cts-idleIdle text and icons
--cts-header-text-active--cts-hoverHovered, selected, and symbol text
--cts-header-hover-bg--cts-accent-softHover and open-menu fill
--cts-header-active-bg--cts-panel-raisedSelected timeframe fill
--cts-header-accent--cts-accentPressed scale toggles, focus ring
--cts-header-on-accent--cts-on-accentText on pressed toggles
--cts-header-radius--cts-radius-mdButton corners
--cts-header-height40pxBar height

BLOXWAP_HEADER_TOKENS reproduces the bloxwap.pro chrome: a #171717 surface, #a1a1a1 idle and #fafafa active text, a #262626 hover fill, the #00ff3f accent, pill buttons, and borderless menus. Pass it as tokens. Tokens are set inline, so they beat the theme classes, and they are also set on each header menu, so they reach menus in a shared toolbar portal.

A flat set like BLOXWAP_HEADER_TOKENS applies in both themes. For different looks per theme, pass { dark?, light? } (ThemedHeaderTokens): setTheme applies the matching set and removes the other's properties, leaving other inline styles alone. A theme without a set keeps the default .cts-theme or .cts-light look. BLOXWAP_HEADER_THEME is { dark: BLOXWAP_HEADER_TOKENS }, the bloxwap.pro look on dark and the default light tokens on light:

const header = createChartHeader({ chart, document, container, tokens: BLOXWAP_HEADER_THEME, theme: 'dark' });
const strip = createScaleButtons({ chart, document, overlay: stage, tokens: BLOXWAP_HEADER_THEME });
header.setTheme('light'); // bloxwap tokens cleared from the bar and its menus
strip.setTheme('light');

To use CSS instead, put the --cts-header-* properties on .cts-header, .cts-scale-buttons, and the toolbar tokens for the menus on .cts-theme .cts-header-menu:

.cts-header, .cts-scale-buttons {
  --cts-header-bg: #171717; --cts-header-text: #a1a1a1; --cts-header-text-active: #fafafa;
  --cts-header-hover-bg: #262626; --cts-header-active-bg: #262626;
  --cts-header-accent: #00ff3f; --cts-header-on-accent: #0a0a0a; --cts-header-radius: 9999px;
}
.cts-theme .cts-header-menu {
  --cts-panel: #171717; --cts-panel-raised: #262626; --cts-edge: transparent;
  --cts-idle: #a1a1a1; --cts-hover: #fafafa; --cts-accent: #00ff3f; --cts-accent-soft: #262626; --cts-on-accent: #0a0a0a;
}

HEADER_CSS and injectHeaderStyles(doc) are exported for shadow-DOM and server-rendered hosts. The injected style element carries the data-chart-ts-header attribute (HEADER_STYLE_MARKER).

See theming and presets for the chart colors and the rest of the UI tokens.

Edit on GitHub

On this page