# The toolbar and your buttons

> Hide what you do not want, add your own buttons and menus, and keep it accessible.

Section: Customization. Source: https://ortexcharts.com/docs/toolbar

## The Toolbar Is a Set of Switches

`createChartShell` draws a toolbar above the chart and a drawing rail beside it. Every item
is independently switchable, and everything an item does is also a method on the returned
shell, so a host can hide the whole toolbar and still drive the chart from its own controls.

```ts
import { createChartShell } from "@ortex-charts/ui";

const shell = createChartShell(el, {
  symbol: "AAPL",
  datafeed,
  toolbar: { theme: false, screenshot: false, download: true },
  resolutions: ["5", "15", "60", "1D", "1W"],
  indicators: ["sma", "ema", "rsi", "macd", "bb"],
  drawingTools: ["trendline", "ray", "horizontalLine", "rectangle", "fibRetracement"],
});
```

_Live example: [custom-toolbar](https://ortexcharts.com/examples/custom-toolbar)_

## The Built-In Items

`ToolbarItem` is the union of the switch names. Everything is on by default except
`download`.

| Item | What it is | Shell method |
|---|---|---|
| `symbol` | Symbol button opening a search popover; Enter loads typed text | `setSymbol` |
| `resolution` | Segmented buttons labeled `1m 5m 15m 1h D W M` | `setResolution` |
| `ranges` | Period buttons: `1D 1W 1M 1Q 1Y 5Y 10Y` by default | `setRange` |
| `seriesType` | Candles, bars, line, area, baseline, plus Heikin-Ashi and hollow toggles | `setSeriesType` |
| `indicators` | Searchable list of the registry, plus the active list with settings, hide and remove | `addIndicator`, `indicators` |
| `overlays` | Host data overlays, as a menu or the "Add data to chart" modal | `setOverlay`, `overlays`, `openDataPicker` |
| `scale` | Auto, logarithmic and percent scale modes | `setScaleMode` |
| `fit` | Fit the whole history into view | — |
| `realtime` | Scroll to the latest bar | — |
| `replay` | Bar replay; **off by default** | `replay`, `startReplay`, `stopReplay` |
| `screenshot` | PNG download, or `onScreenshot` | — |
| `download` | CSV of the visible series; **off by default** | `exportCsv` |
| `theme` | Light and dark toggle | `setTheme` |
| `colorBlind` | Colour-blind safe palette, independent of light and dark | `setTheme` |
| `fullscreen` | Full screen and back | `toggleFullscreen` |

```ts
toolbar: { replay: true, download: true, theme: false }
```

Switching everything off leaves a bare chart that is still fully driven by the shell
methods, which is the supported way to build a completely custom interface without giving up
symbol binding, layouts and overlays.

`drawings: false` removes the drawing rail entirely. `indicators: false` removes the
indicator menu. `indicators: ["sma", "ema"]` limits which ones the menu offers without
affecting what `addIndicator` can do from code.

## Configuration as Data

Every switch is plain JSON, so a company's chart configuration can live in a checked-in file
with only the callbacks attached in code.

```json
{
  "resolutions": ["5", "15", "60", "1D", "1W"],
  "toolbar": { "theme": true, "colorBlind": true, "screenshot": false, "download": true },
  "indicators": ["sma", "ema", "rsi", "macd", "bb"],
  "drawingTools": ["trendline", "ray", "horizontalLine", "rectangle", "fibRetracement"],
  "dataPicker": "modal",
  "compare": true
}
```

```ts
import config from "./chart-config.json";

createChartShell(el, {
  ...config,
  datafeed,
  overlays,
  onLayoutChange: (layout) => api.saveChartLayout("stock-page", layout),
});
```

## Period Buttons

A price chart is read in periods — the last month, the last year — so the toolbar carries
them beside the resolutions. The two are different questions: a resolution is how coarse each
bar is, a period is how much of the timeline is on screen.

```ts
createChartShell(el, {
  datafeed,
  ranges: [
    { label: "1D", ms: 86_400_000, resolution: "5", title: "The last day, in five-minute bars" },
    { label: "1W", ms: 7 * 86_400_000, resolution: "60" },
    { label: "1M", ms: 30 * 86_400_000 },
    { label: "1Q", ms: 91 * 86_400_000 },
    { label: "1Y", ms: 365 * 86_400_000 },
    { label: "5Y", ms: 5 * 365 * 86_400_000, resolution: "1W" },
    { label: "10Y", ms: 10 * 365 * 86_400_000, resolution: "1W" },
  ],
  rangeDisplay: "auto",
});
```

`ranges` defaults to a day, a week, a month, a quarter, a year, five years and ten years,
with no resolution attached. `DEFAULT_RANGES` is exported if you want to extend that list
rather than replace it. Pass `false`, or `toolbar: { ranges: false }`, to leave the buttons
out.

Seven periods are seven clicks saved on a trading screen and seven buttons of clutter in a
sidebar, and the same chart is often both. `rangeDisplay` decides which it is:

| Value | What you get |
|---|---|
| `"auto"` (default) | Spelled out when the chart is wide enough, folded into one button when it is not, changing as the chart is resized |
| `"buttons"` | Always spelled out |
| `"menu"` | Always one button naming the current period |

Folded, the button says which period is showing, or `Period` when the reader has moved to a
view that is not one of them.

| Field | What it does |
|---|---|
| `label` | What the button says |
| `ms` | How much time it shows, ending at the last bar |
| `resolution` | Switch resolution with it, for spans where the two only make sense together |
| `title` | Tooltip and accessible name |

Two details worth knowing. Each period ends at the **last bar rather than at the clock**, so a
market that closed on Friday still shows its last month rather than a month of empty axis.
And the button matching the view is lit only while it matches: pan or zoom away and it stops
claiming to be what you are looking at.

A period shorter than the chart can zoom to settles wider than asked, because bar spacing is
capped — a week is seven daily bars, and seven bars do not stretch across a wide pane. That is
what the `resolution` field is for.

`shell.setRange(ms, resolution?)` does the same thing from your own control.

## One Row, Not Two

A toolbar that carries every control wraps onto a second row on anything narrower than a wide
desktop. That row costs the chart about forty pixels of height, and it costs them from the
part of the screen a reader is actually looking at. So by default it does not wrap: it gives
things up instead, in order of how little they are missed.

| Step | What goes | What is left |
|---|---|---|
| 1 | The words on buttons that read as icons | Candles, Indicators, Data and Drawing tools as icons, with the words as tooltips |
| 2 | The period buttons | One button naming the period showing |
| 3 | The view and export controls | One **More** button holding them as a palette |
| 4 | The middle resolutions | The shortest, the longest and the selected one, with a chevron holding the rest |

Each step is applied only if the one before it was not enough, and the whole thing runs again
whenever the chart is resized, so a chart that grows takes it all back. Everything given up
stays one click away.

```ts
createChartShell(el, {
  symbol: "AAPL",
  datafeed,
  toolbarFit: "auto",         // the default; "wrap" takes a second row instead
  extrasCollapsed: true,      // start with the tail folded, at any width
  drawingRailCollapsed: true, // the drawing tools folded to a thumb
});
```

`toolbarFit` is about what fits. `extrasCollapsed` and `drawingRailCollapsed` are about what
you want to start with, and they hold at every width. The two are kept apart on purpose:
folding because a chart is narrow is not the same as a reader choosing to fold, so a reader
who opens the tail keeps it open at every width that can hold it.

`extrasCollapsed` folds the right-hand end of the toolbar: the scale modes, fit, the legend
and drawing-rail toggles, realtime, replay, download, screenshot, theme, colour-blind and full
screen, behind a single **More** button. Opening it shows the same buttons as a small palette,
with an item that puts them back in the toolbar for good.

`drawingRailCollapsed` does the same for the drawing tools down the left edge, leaving a thumb
that opens them.

Both are starting positions, not locks. The reader's own choice is recorded in the layout and
outranks the option on every later visit, and a layout that has never carried the field, which is every
layout saved before you turned the option on, counts as no choice rather than as a vote for
the old behaviour.

```ts
shell.setExtrasCollapsed(false);   // and shell.isExtrasCollapsed()
shell.setDrawingRailCollapsed(true);
```

## Your Own Buttons

`ToolbarAction` adds a button or a dropdown. It is data plus callbacks, so it fits the same
configuration-file pattern.

```ts
interface ToolbarAction {
  id: string;
  label?: string;                       // icon-only when omitted
  icon?: IconName | string;             // a built-in name, or your own <svg …> markup
  title?: string;                       // tooltip and accessible name; defaults to label
  placement?: "start" | "middle" | "end";
  onClick?: (shell: ChartShell, event: MouseEvent) => void;
  menu?: Array<{ label: string; icon?: IconName; hint?: string; onSelect: (shell: ChartShell) => void }>;
  active?: boolean;                     // render as pressed
  disabled?: boolean;
}
```

```ts
createChartShell(el, {
  symbol: "AAPL",
  datafeed,
  actions: [
    {
      id: "api",
      label: "Download with API",
      icon: "api",
      placement: "end",
      onClick: (shell) => openApiDialog(shell.symbol(), shell.resolution()),
    },
    {
      id: "share",
      icon: "share",
      title: "Share",
      menu: [
        { label: "Copy link", onSelect: (s) => navigator.clipboard.writeText(linkFor(s.getLayout())) },
        { label: "Post to Slack", icon: "camera", onSelect: (s) => slack(s.chart.chart.takeScreenshot()) },
      ],
    },
  ],
});
```

`placement` decides where the button lands: `start` puts it with the symbol and resolutions,
`middle` with the chart-type menus, and `end` at the right, which is the default.

At runtime, `shell.addAction(action)` adds one and returns a function that removes it again,
which is how a button appears only in a particular application state.

```ts
const removeAlertButton = shell.addAction({
  id: "add-alert",
  icon: "plus",
  title: "Add alert at the last close",
  onClick: (s) => {
    const last = s.chart.main.lastIndex();
    const v = s.chart.main.valueAt(last);
    if (v && "close" in v) s.chart.alerts().add({ price: v.close, label: "New alert" });
  },
});

removeAlertButton();
```

### A Button That Carries State

`active` is read when the toolbar is built, so a button whose meaning changes — an
adjusted-prices toggle, a "follow the last price" switch — is updated with `setAction`
rather than by handing the shell a new definition. Rebuilding the options array, which is
what a React host does on every render, changes nothing: the shell is created once.

```ts
let adjusted = false;

createChartShell(el, {
  datafeed,
  actions: [
    {
      id: "adj",
      label: "ADJ",
      title: "Adjust prices for dividends",
      active: adjusted,
      onClick: (shell) => {
        adjusted = !adjusted;
        shell.setAction("adj", {
          active: adjusted,
          title: adjusted ? "Showing dividend-adjusted prices" : "Adjust prices for dividends",
        });
        void shell.binding?.reload();   // the feed reads the flag itself
      },
    },
  ],
});
```

`setAction(id, patch)` takes any of `active`, `disabled`, `label` and `title`, changes only
what the patch names, and does nothing for an id the toolbar does not have. `active` also
sets `aria-pressed`, so the state is announced rather than only drawn.

`binding.reload()` is the other half of that pattern: the same symbol fetched again because
something the feed reads for itself changed. It does not resolve the symbol a second time,
and it puts the reader back where they were looking instead of jumping to the right edge.

## Icons

`ICONS` is a record of 52 inline SVG path fragments and `icon(name)` builds an `<svg>`
element from one. They are 20-by-20, single stroke, and use `currentColor`, so they inherit
the button color and follow the theme.

```ts
import { ICONS, icon } from "@ortex-charts/ui";

Object.keys(ICONS);            // every available IconName
const el = icon("magnet");
```

The names, grouped by what they are for:

- **Series types:** `candles`, `bars`, `line`, `area`, `baseline`, `heikinAshi`, `hollow`.
- **Tools:** `cursor`, `crosshair`, `trendline`, `ray`, `horizontalLine`, `verticalLine`,
  `rectangle`, `parallelChannel`, `fibRetracement`, `measure`, `text`, `magnet`.
- **Editing:** `undo`, `redo`, `trash`, `clear`, `eye`, `eyeOff`, `settings`, `lock`,
  `plus`, `minus`, `check`, `close`, `chevron`.
- **Navigation and scale:** `fit`, `realtime`, `log`, `percent`, `auto`, `search`.
- **Replay:** `replay`, `play`, `pause`, `stepForward`, `stepBack`, `stop`.
- **Actions:** `camera`, `download`, `share`, `link`, `api`, `indicator`, `sun`, `moon`.

Passing your own markup instead of a name works and is the right answer for a brand icon:

```ts
{ id: "brand", icon: '<path d="M4 16 L10 6 L16 16 Z" fill="none" stroke="currentColor" stroke-width="1.6"/>' }
```

Use a 20-by-20 or 24-by-24 coordinate space and `currentColor` strokes so the icon matches
the built-ins at every size and in both themes.

## Menus and Dialogs

The same primitives the built-in toolbar uses are exported, so a host action can open a menu
or a form that looks exactly like the rest of the chrome rather than like a bolted-on
`window.prompt`.

```ts
import { openMenu, openDialog, openSymbolSearch } from "@ortex-charts/ui";

openMenu(shell.root, anchorButton, (menu) => {
  menu.head("Export");
  menu.item({ label: "CSV", icon: "download", onSelect: () => save(shell.exportCsv()) });
  menu.item({ label: "PNG", icon: "camera", onSelect: () => savePng(shell.chart.chart.takeScreenshot()) });
  menu.sep();
  menu.item({ label: "Include hidden series", checked: includeHidden, onSelect: () => { includeHidden = !includeHidden; return false; } });
});
```

Returning `false` from `onSelect` keeps the menu open, which is what a toggle wants.

```ts
openDialog(shell.root, {
  title: "Alert settings",
  fields: [
    { key: "price", label: "Price", type: "number", value: 182.5, step: 0.01 },
    { key: "repeat", label: "Repeat", type: "boolean", value: false },
    { key: "channel", label: "Notify by", type: "select", value: "email", options: ["email", "push", "none"] },
  ],
  submitLabel: "Create",
  onSubmit: (values) => createAlert(values),
});
```

`openDialog` returns a function that closes it. Fields are `number`, `select` or `boolean`;
this is the same generator the indicator settings dialog uses, which is why an indicator you
[register yourself](https://ortexcharts.com/docs/indicators) gets a proper form without writing one.

## Keyboard and Accessibility

What is implemented:

- **The chart is focusable.** Arrow keys scroll, with Shift for a larger step; `+` and `-`
  zoom about the center; Home resets the time scale.
- **Drawings respond to keys** on a focused chart: Escape cancels a tool or clears the
  selection, Delete and Backspace remove the selected drawing, and Ctrl or Cmd with Z and Y
  undo and redo. See [Drawing tools](https://ortexcharts.com/docs/drawings).
- **Toolbar buttons carry `aria-pressed`** so a screen reader announces the active chart
  type, scale mode and tool.
- **Menus are `role="menu"`** with `aria-haspopup` and `aria-expanded` on the button that
  opens them, arrow-key navigation between items, `aria-checked` on toggles, Escape to
  close, and focus returned to the anchor.
- **Dialogs are `role="dialog"` with `aria-modal`**, an accessible name from the title, a
  focus trap, Enter to submit and Escape to cancel.
- **Icon-only buttons carry a `title` and an accessible name**, which is why `ToolbarAction`
  falls back from `title` to `label`.
- **Reduced motion** is respected in the chrome CSS.

What is not implemented, stated plainly:

- **There is no screen-reader description of the data.** The canvas is not annotated, and
  there is no textual summary of the series, the visible range or the values at the
  crosshair.
- **There is no focusable data table** as an alternative representation, which is the
  approach the Highcharts accessibility module takes.
- **There is no high-contrast theme** shipped, though one is a [theme
  object](https://ortexcharts.com/docs/customization) away.
- **Keyboard support does not extend to creating or editing drawings**, only to canceling
  and deleting them.

If accessibility conformance is a procurement requirement for you, these are the gaps to
weigh, and the CSV export plus your own table is the honest interim answer.

## Mobile

Below 640 pixels the toolbar drops its labels, the resolution strip scrolls horizontally,
and the drawing rail becomes a horizontal bar under the chart. Coarse pointers get 32-pixel
targets. Pinch zoom, drag panning and long-press crosshair are in the engine rather than in
the chrome, so they work on a bare `createFinancialChart` too.

Full screen uses the Fullscreen API on the shell root, and falls back to a fixed-position
layer where the API is missing, which is the case on iOS Safari. Escape leaves either.

## Driving the Chart Without the Toolbar

Every method behind the buttons is public, so switching the toolbar off is not a loss of
functionality.

```ts
const BARE = {
  symbol: false, resolution: false, seriesType: false, indicators: false, overlays: false,
  scale: false, fit: false, realtime: false, screenshot: false, theme: false, fullscreen: false,
} as const;

const shell = createChartShell(el, { symbol: "AAPL", datafeed, toolbar: BARE, drawings: false });

await shell.setSymbol("MSFT");
await shell.setResolution("60");
shell.setSeriesType("line");
shell.setScaleMode("logarithmic");
shell.addIndicator("rsi", { length: 21 });
await shell.setOverlay("si", true);
shell.setTheme("light");
await shell.toggleFullscreen();
const csv = shell.exportCsv();
const layout = shell.getLayout();
```

See [the shell and layouts](https://ortexcharts.com/docs/shell) for the full surface, including overlays, layouts
and per-series placement.
