Skip to content

Styling

You style a chart with plain CSS, written next to the slat it styles.

CSS for a slat

Wrap the slat function in slat() and give it css. Put classes on the slat’s root and its blocks, and style those classes.

Jupiter
95
Saturn
146
Uranus
28
Neptune
16
import { Chart, Plot, Bar, Label, slat } from "@bezda/rhp";

const Planet = slat(
  {
    thickness: 44,
    css: `
      .slat:hover {
        background: color-mix(in srgb, var(--rhp-ink) 8%, transparent);
        border-radius: 10px;
      }
      .name { font-size: 14px; font-weight: 600; }
      .bar {
        --rhp-start-radius: 0px;
        --rhp-end-radius: 12px;
        background: linear-gradient(
          var(--rhp-toward-end),
          var(--rhp-series-1),
          var(--rhp-series-5)
        );
      }
      .value {
        font-size: 16px;
        font-weight: 800;
        --rhp-label-gap: 10px;
      }
    `,
  },
  (d) => (
    <div class="slat">
      <Label edge="start" class="name">
        {d.planet}
      </Label>
      <Bar to={d.moons} class="bar" />
      <Label at={d.moons} class="value">
        {d.moons}
      </Label>
    </div>
  ),
);

export default function Moons() {
  return (
    <Chart scale={[0, 150]}>
      <Plot
        planet={["Jupiter", "Saturn", "Uranus", "Neptune"]}
        moons={[95, 146, 28, 16]}
      >
        {Planet}
      </Plot>
    </Chart>
  );
}

That CSS only reaches this chart’s slats. It can’t leak into your page or into other charts, so you can use short class names like .slat or .value without worrying. It works in both directions: your page’s CSS can’t reach into a chart either, unless you turn that off to draw faster.

Anything CSS can do to an element, it can do here, because a block and a slat are ordinary elements. A gradient on a bar, a web font on a label, an image behind a cell: all of it behaves the way it does anywhere else on your page.

Using someone else’s look

A slat’s CSS can be a list, which is joined in order:

import { NIGHT } from "./looks.js";

const Row = slat({ ...NIGHT, css: [NIGHT.css, own] }, (d) => …);

So a look is an ordinary object you can export, import, publish or paste: its CSS, and the layout that goes with it (thickness, inset, room). Use it as it comes with slat(NIGHT, (d) => …), or put your own CSS after it, where it wins.

For a look to land on a chart it wasn’t written for, it has to aim at something both charts have. Class names are no good for that, because each chart picks its own. What every chart does have is what rhp writes on each block:

SelectorWhat it reaches
.rhp-bar, .rhp-dot, .rhp-tick, .rhp-cell, .rhp-area, .rhp-lineevery block of that kind
.rhp-label[data-rhp-edge="start"]the name beside a slat, in the gutter
.rhp-label[data-rhp-edge="end"]a label in the gutter after the slat
.rhp-label[data-rhp-at]a label at a value on the scale
.rhp-plot[data-rhp-overlap]a Plot whose slats share one space

Here one look is used by two slats built from different blocks, a plain bar and a bar with a dot at each end, and neither one needed a line of CSS of its own:

Oslo
4°
Madrid
12°
Cairo
19°
Oslo
4°
Madrid
12°
Cairo
19°
import { Chart, Plot, Bar, Dot, Label, slat } from "@bezda/rhp";

// One look for both slats, aimed at what rhp writes rather than at class names.
const NIGHT = {
  thickness: 38,
  room: { start: 76, end: 46 },
  css: `
    .rhp-label[data-rhp-edge="start"] { font-weight: 600; }
    .rhp-bar, .rhp-dot { --rhp-radius: 99px; }
    .rhp-label[data-rhp-at] {
      font-weight: 700;
      font-variant-numeric: tabular-nums;
      color: var(--rhp-muted);
    }
  `,
};

const Warm = slat(NIGHT, (d) => (
  <div>
    <Label edge="start">{d.city}</Label>
    <Bar to={d.high} />
    <Label at={d.high}>{d.high}°</Label>
  </div>
));

const Range = slat(NIGHT, (d) => (
  <div>
    <Label edge="start">{d.city}</Label>
    <Bar from={d.low} to={d.high} thick="6px" />
    <Dot at={d.low} size="13px" />
    <Dot at={d.high} size="13px" />
    <Label at={d.high}>{d.high}°</Label>
  </div>
));

const CITIES = ["Oslo", "Madrid", "Cairo"];
const LOW = [-4, 3, 9];
const HIGH = [4, 12, 19];

export default function OneLook() {
  return (
    <>
      <Chart scale={[-10, 30]} ticks={[-10, 0, 10, 20, 30]} format={(v) => v + "°"}>
        <Plot city={CITIES} high={HIGH}>{Warm}</Plot>
      </Chart>
      <Chart scale={[-10, 30]} ticks={[-10, 0, 10, 20, 30]} format={(v) => v + "°"}>
        <Plot city={CITIES} low={LOW} high={HIGH}>{Range}</Plot>
      </Chart>
    </>
  );
}

A look aims at what every chart has. What is particular to one chart, which of its two bars is the track and which is the progress, stays in that chart’s own CSS, where the person who built it can see it.

The variables rhp reads

A few CSS variables change how rhp draws a block. Their names follow the chart’s scale rather than the screen, saying start and end instead of left and right, so the same rule works whether the chart is horizontal or vertical.

VariableWhat it does
--rhp-radiusrounds a Bar’s corners, one length (2px by default). For different corners, use the next two or plain border-radius.
--rhp-start-radius, --rhp-end-radiusround one end of a Bar. The end is where its value is; the start is where it begins.
--rhp-gapleaves space at the start of a Bar: a gap between stacked pieces
--rhp-label-gapthe space between a Label and its value (or the chart, for edge labels)
--rhp-label-sizea Label’s font size (12px by default)
--rhp-tick-widtha Tick’s width (2px by default)
--rhp-cell-gapthe space around a Cell (1px by default)
--rhp-toward-enda direction to use in gradients: linear-gradient(var(--rhp-toward-end), …) fades from a Bar’s start to its end

Horizontal and vertical

rhp charts can turn: pass orientation="vertical" to the Chart. The variables above follow the chart as it turns. For anything else, :horizontal and :vertical target one direction:

.name:vertical { font-size: 11px; }
.bar:horizontal > img { max-width: 300px; }

Put them on the slat or on a block. For an element inside a block, put them on the block: .bar:vertical .icon.

Jupiter
95
Saturn
146
Uranus
28
Neptune
16
import { createSignal } from "solid-js";
import { Chart, Plot, Bar, Label, slat } from "@bezda/rhp";

const Planet = slat(
  {
    css: `
      .bar { --rhp-start-radius: 0px; --rhp-end-radius: 8px; }
      .value { font-weight: 700; }
      .name:vertical { font-size: 11px; }
    `,
  },
  (d) => (
    <div>
      <Label edge="start" class="name">
        {d.planet}
      </Label>
      <Bar to={d.moons} class="bar" />
      <Label at={d.moons} class="value">
        {d.moons}
      </Label>
    </div>
  ),
);

export default function Moons() {
  const [orientation, setOrientation] = createSignal("vertical");
  const turn = () =>
    setOrientation(
      orientation() === "vertical" ? "horizontal" : "vertical",
    );
  return (
    <>
      <div class="demo-controls">
        <button onClick={turn}>Turn the chart</button>
      </div>
      <Chart scale={[0, 150]} orientation={orientation()}>
        <Plot
          planet={["Jupiter", "Saturn", "Uranus", "Neptune"]}
          moons={[95, 146, 28, 16]}
        >
          {Planet}
        </Plot>
      </Chart>
    </>
  );
}

Colors

Inside a slat’s CSS, the theme’s colors are variables:

  • var(--rhp-ink) for text, var(--rhp-muted) for quieter text, var(--rhp-grid) for lines,
  • var(--rhp-series-1) to var(--rhp-series-6) for data,
  • var(--rhp-color) for the block’s own color.

The page sets these colors through the theme, described in colors and themes.

Without the guard

What keeps your page’s CSS out of a chart is a set of rules rhp adds for its own elements: the guard. It undoes what your page might set on every div or span, such as margins, padding, borders, fonts and transitions. That takes time: without it, a chart draws 18 to 28% faster in Chrome and Edge, about 30% faster in Safari, and up to 16% faster in Firefox.

When your page’s CSS leaves a chart’s elements alone, the chart can go without it:

<Chart noCssGuard>…</Chart>

Or every chart on the page:

import { noCssGuard } from "@bezda/rhp";
noCssGuard(); // where your app starts, before the first chart, on the server and in the browser

The prop and noCssGuard() are in rhp 2.1.0 and later.

Use noCssGuard() on a page made for its charts, such as a poster or an article. Use the Chart prop for one large chart on a page of small ones: that chart draws as fast as with noCssGuard(). Once one chart says noCssGuard, the charts that keep the guard draw 2 to 6% slower, so when every chart on the page can go without it, call noCssGuard() instead.

Without the guard, rhp’s own CSS works as before: it places and colors every block and keeps hidden slats hidden. But your page’s CSS now reaches whatever rhp doesn’t set itself, and CSS like this changes a chart:

Your page’s CSSWhat it does to a chart without the guard
margins, padding or borders on every div or spanmoves blocks, labels and grid lines off their places
a width or height on every div, or a minimum or maximum oneresizes bars and slats
fonts and text styles on every span: letter-spacing, text-transform, line-heightchanges labels and axis numbers, and moves them
::before and ::after boxes on every div, as a clearfix addsputs extra boxes in slats and in the chart’s grid
a background, a shadow, rounded corners or an outline on every divdraws them on rhp’s elements
a transition on every elementanimates the axis numbers and the names beside slats when they change
an animation on every elementruns it on every block, label and grid line
a rule for a class that a slat uses tooreaches that element: a page’s .note rule, written for a note under the chart, colors a slat’s Label that has the class note

The 92 example charts on this site and the 20 in the slat gallery were drawn without the guard under 12 popular stylesheets. Tailwind (its browser build and its preflight), normalize.css, modern-normalize, sanitize.css, Pico, Water.css, Simple.css and new.css changed none of them. Bootstrap 5, Bulma and Milligram changed a few, each only through a class name a slat used too: Bootstrap’s .row and .mark, Bulma’s .box, .tag, .icon and .progress, and Milligram’s .row. So with a framework like these, give your slats class names it doesn’t use.

The browser’s own styles reach a chart without the guard too, on a slat whose root isn’t a div. An <svg> root cuts off what it draws outside its box, an <a> root is blue and underlined, a <button> or <input> root has the browser’s border, background and font, and a <p>, <ul>, <h3> or <figure> root has margins that move it off its place. A div, li, span, section, label or table root looks the same either way. Set what you need in the slat’s CSS, such as margin: 0 or overflow: visible, or use a div.

To check a page, draw its charts with and without noCssGuard. If they look the same at rest, when you point at them and when their data changes, your page’s CSS leaves them alone.

A page that links rhp’s stylesheet itself (see server rendering) links @bezda/rhp/rhp-no-css-guard.css instead and calls noCssGuard(). A chart inside another chart’s slat goes without the guard only when the chart around it does too.

Things to know

  • Hover whatever should react. .slat:hover .bar { … } reacts anywhere along the slat, which is an easy thing to point at and lets one rule light up several pieces at once. Hover a block instead when the block is the thing being pointed at: one layer of a stack, one book on a shelf, one cell of a heatmap.
  • Put your own transitions on elements inside blocks. rhp moves blocks with transitions of its own, and a transition set on a block replaces them, so the block would jump to new values. Fade a label’s text, not the label.
  • The page can still style the chart’s box. Its width, margin, background and border are yours to set on the Chart, with class or style.