Skip to main content

Theming

CSS Variables

Overview

In addition to configuration properties, our components also rely on CSS variables to supply the values of various SVG attributes such as fill, stroke, opacity, etc. You can override these variables to further customize your Unovis components.

Every variable has the following naming convention: --vis + label + attribute. For example, the variable named --vis-area-cursor would apply to the Area component's cursor property.

Note that while our variables follow this convention, it does not guarantee that the value you wish to override is available. Be sure to check the corresponding doc page of the component you want to customize to see the available CSS variables.

Basic Example

Variables can be overridden in your CSS style declarations. Consider the default configuration for sankey, which looks like this:

Loading...

Now consider the following style declaration. After adding custom-sankey to the container element of the Sankey component, we will see the following result:

Loading...
Loading...

Dark Theme Usage

Our library offers dark theme support which takes effect when the class theme-dark is added to the document's body element. Every component has a dark version of each color variable labeled with the prefix --vis-dark. You can opt not to override these if you want to use our default dark theme values, or override them like so:

Loading...

Global Variables

The majority of our variables exist on a component level, but there are a few global CSS variables:

Loading...
note

Unless overridden explicitly, --vis-color-main corresponds to the first color in the default color palette

Label Styling

Font

The font for labels across all of our components is defined by the --vis-font-family variable. The default font, Inter, is not imported by default, but you can easily import it yourself from Google Fonts.

To use a different font, simply redefine the --vis-font-family CSS variable:

Loading...
Loading...

Large Sizing

A common theming scenario is the "large size" theme, for when you want larger font sizes for the labels in your charts. We offer two variations in the form of css classes that you can import directly from @unovis/ts:

import { styleLargeSize } from '@unovis/ts' // ~1.3x larger
import { styleExtraLargeSize } from '@unovis/ts' // 2x larger

Just add either one to your container's class list to the effects. Consider the following example of a labeled Scatter chart:

className: styleLargeSize

className: styleExtraLargeSize

note

When using this theme, the following components have caveats:

  • Scatter: If the labelPosition property is set to Position.Center, point labels will try fit to the point's size. In this case, you will instead need to update the pointSize property to render larger labels.

  • Timeline Additionally, you may need to adjust the rowHeight property to accommodate larger labels.

Color Palette

Many of our components use the default color palette for visualizations. You can import the array of hex values directly from unovis/ts:

import { colors, colorsDark } from '@unovis/ts'

The dark theme palette is slightly different from the regular one. These colors are also defined directly in our CSS variables, labeled --vis-color0, --vis-dark-color0, --vis-color1, --vis-dark-color1, etc. The full palette looks like this:

Light

Loading...

Dark

Loading...

Palette Editor

You can tweak and preview your desired palette using the example StackedBar component below. If you like the result, just copy and paste the corresponding style declaration in the dropdown below.

tip

Alternatively, you can provide a custom color palette in global scope using the UNOVIS_COLORS variable:

window.UNOVIS_COLORS = [...]
// or
globalThis.UNOVIS_COLORS = [...]

This needs to be done before the library is imported, i.e. in your top level JS file or HTML.

Synchronizing Colors Across Charts

By default, every component assigns colors to its series by index: the first series uses --vis-color0, the second --vis-color1, and so on (see Color Palette). That's fine for a single chart, but as soon as a dashboard has several charts that share categories, the same category tends to land at a different index in each chart — and therefore gets a different color. Color synchronization fixes this by mapping a stable color key to a color through a shared color function, so a category keeps the same color across every chart, legend, and tooltip.

There are three pieces:

  • colorKeys — a component property: an array of string keys, one per y accessor, that labels each series (e.g. ['aws', 'azure', 'github']).
  • colorFunction — a function of type (key: string | number) => string, accepted by the container (where it applies to every component inside) and by BulletLegend. Given a key, it returns a color.
  • UnovisColorScale — the default color function, used when you don't provide your own.

How a color is resolved

For each element, Unovis resolves the color in this order:

  1. The component's own color accessor, if it returns a value — a per‑datum override that always wins.
  2. Otherwise, if the series has a colorKey, the color function is called with that key.
  3. Otherwise, the color function is called with the series index.
  4. If nothing matches, the element gets no explicit color.

So colorKeys together with a shared color function means same key → same color, regardless of the chart type or the order in which series appear.

Defining a color function

A color function is just (key) => color. There are two common ways to build one.

A D3 ordinal scale assigns colors from a range to keys in first‑seen order:

import { Scale } from '@unovis/ts'

const color = Scale.scaleOrdinal()
.range(['#ff8cfd', '#126b7e', '#ff5450', '#23cc00', '#0000ff'])

An explicit color map gives you full control over which key maps to which color, with a fallback for unknown keys:

const colorMap = { aws: '#f0a8b4', google: '#7eb8d4', github: '#b89ef0' }
const color = (key) => colorMap[key] ?? '#cccccc'

Putting it together

Pass the same color function to every container and legend, and give each component a colorKeys array aligned with its y accessors:

import { VisXYContainer, VisGroupedBar, VisArea, VisAxis, VisBulletLegend } from '@unovis/react'
import { Scale } from '@unovis/ts'

const color = Scale.scaleOrdinal().range(['#4d8cfd', '#ff6b7e', '#00c19a', '#f4b83e'])
const keys = ['aws', 'azure', 'github']
const y = keys.map(k => (d) => d[k])

// Legend, charts, and tooltips all share the same color function and keys
<VisBulletLegend items={keys.map(name => ({ name, colorKey: name }))} colorFunction={color} />

<VisXYContainer data={data} colorFunction={color}>
<VisGroupedBar x={d => d.x} y={y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>

<VisXYContainer data={data} colorFunction={color}>
<VisArea x={d => d.x} y={y} colorKeys={keys} />
<VisAxis type="x" />
</VisXYContainer>

A few things to keep in mind:

  • BulletLegend items carry their key in colorKey; pass the same colorFunction so the bullets match the charts.
  • The crosshair tooltip picks up colorKeys from the components automatically, so its markers stay in sync too.
  • Keep colorKeys the same length as the y accessor array.

The example below uses one shared ordinal scale. The second chart lists its series in a different order and the legend, bars, and area all agree on the color of every category:

Overriding the default palette in code

When you don't pass a color function, components fall back to UnovisColorScale — a D3 ordinal scale whose range is the --vis-color* CSS variables. You can re‑range it once at startup to change the default palette everywhere, without touching CSS:

import { UnovisColorScale } from '@unovis/ts'

UnovisColorScale.range(['#f0a8b4', '#7eb8d4', '#8ed49a', '#ffdc88', '#b89ef0'])
warning

UnovisColorScale is global: re‑ranging it affects every chart on every page loaded afterwards. If you only need to recolor the palette (and not sync by key), prefer overriding the --vis-color* CSS variables instead.

Patterns

Patterns — stripes, dots, cross‑hatching, and so on — add a second visual channel on top of color. They're useful for color‑blind‑safe palettes, grayscale printing, or simply telling series apart in a dense chart. Unovis offers two ways to apply them:

  • the pattern accessor on a component, for explicit per‑series (or per‑datum) control;
  • the theme-patterns body class, which applies a default pattern palette automatically, keyed by series index.

The built‑in pattern definitions are injected into the page for you the first time the library loads, so there's nothing to import or register before using them.

The pattern accessor

Most data components accept a pattern accessor: Area, Line, Scatter, GroupedBar, StackedBar, Timeline, Donut, NestedDonut, Sankey, and ChordDiagram. It resolves to one of the built‑in pattern ids, exposed as two enums:

EnumApplies toValues
FillPatternTypeSolid shapes (areas, bars, scatter points, donut/sankey segments…)StripesDiagonal, Dots, StripesVertical, Crosshatch, Waves, Circles
LinePatternTypeThe Line componentCircle, Triangle, Diamond, Arrow, Square, Star

Under the hood, a fill pattern is applied as an SVG mask over the shape, so it's cut out of the series' own fill and automatically takes the series color. A line pattern combines an SVG marker repeated along the path with a stroke-dasharray; the marker inherits the line's color.

The accessor follows the component's data shape:

  • For multi‑series components (where y is an array of accessors), the accessor receives the series index as its second argument — return one pattern per series.
  • For per‑datum components like Scatter points or Donut segments, it receives the datum and its index, so you can vary the pattern by value.
import { VisXYContainer, VisStackedBar, VisLine, VisScatter, VisAxis } from '@unovis/react'
import { FillPatternType, LinePatternType } from '@unovis/ts'

const fillPatterns = [FillPatternType.StripesDiagonal, FillPatternType.Dots, FillPatternType.Crosshatch]
const linePatterns = [LinePatternType.Triangle, LinePatternType.Diamond, LinePatternType.Square]

// A fill pattern per series (StackedBar, Area, GroupedBar, …)
<VisStackedBar x={d => d.x} y={[y0, y1, y2]} pattern={(_d, i) => fillPatterns[i]} />

// A line pattern per series (Line)
<VisLine x={d => d.x} y={[y0, y1, y2]} pattern={(_d, i) => linePatterns[i]} />

// A fill pattern per data point (Scatter)
<VisScatter x={d => d.x} y={d => d.y} pattern={d => fillPatterns[Math.round(d.y) % fillPatterns.length]} />

Fill patterns per series — a stacked bar where each series gets a different FillPatternType (stripes-diagonal, dots, crosshatch):

Loading...

Line patterns per series — each line gets a different LinePatternType (triangle, diamond, square):

Loading...
note

An explicit pattern accessor always takes precedence over the theme-patterns fallback described below — the automatic styles only target shapes that don't already carry a pattern. You can therefore enable theme-patterns globally and still override individual charts with the accessor.

Automatic patterns with theme-patterns

When document.body has the class theme-patterns we automatically apply patterns of two types:

Fill Patterns

Applied automatically to solid shapes (most cases), keyed by series index. Each index maps to a CSS variable (--vis-pattern-fill0, --vis-pattern-fill1, …) that you can override with any SVG mask reference.

The default fill‑pattern palette looks like:

Loading...
Default CSS Variables:
--vis-pattern-fill0: var(--vis-pattern-fill-stripes-diagonal);
--vis-pattern-fill1: var(--vis-pattern-fill-dots);
--vis-pattern-fill2: var(--vis-pattern-fill-stripes-vertical);
--vis-pattern-fill3: var(--vis-pattern-fill-crosshatch);
--vis-pattern-fill4: var(--vis-pattern-fill-waves);
--vis-pattern-fill5: var(--vis-pattern-fill-circles);

Line Patterns

For the Line component and when BulletLegend's bulletShape property is set to "line". Each series index maps to a marker variable and a dash‑array variable. You can customize these patterns by assigning any combination of the following variable types:

  • Prefixed --vis-pattern-marker: accepts SVG defs containings marker elements
  • Variables with the prefix --vis-pattern-dasharray to a valid value for the stroke-dasharray property. The default palette looks like:
Loading...
Default CSS Variables:
--vis-pattern-marker0: var(--vis-pattern-marker-circle);

--vis-pattern-marker1: var(--vis-pattern-marker-triangle);
--vis-pattern-dasharray1: 9 1;

--vis-pattern-marker2: var(--vis-pattern-marker-diamond);
--vis-pattern-dasharray2: 2;

--vis-pattern-marker3: var(--vis-pattern-marker-arrow);
--vis-pattern-dasharray3: 2 3 8 3;

--vis-pattern-marker4: var(--vis-pattern-marker-square);
--vis-pattern-dasharray4: 6;

--vis-pattern-marker5: var(--vis-pattern-marker-star);
--vis-pattern-dasharray5: 1 6;

Customizing the pattern palette

The automatic palette is driven by indexed CSS variables — --vis-pattern-fill{i} for fills, and --vis-pattern-marker{i} / --vis-pattern-dasharray{i} for lines. Override them to point at your own SVG <defs>, or to a different dash array. (These variables back the theme-patterns fallback only; charts that use the pattern accessor ignore them.)

To override default patterns use the following table for reference.

CSS Variable PrefixTypeAccepted ValueExample
--vis-pattern-fillFillSVG mask from a <defs> elementurl(#my-pattern-fill)
--vis-pattern-markerLineSVG marker from a <defs> elementurl(#my-line-marker)
--vis-pattern-dasharrayLineCSS stroke-dasharray property5 10

Bordered Segments

For charts with multiple data layers, it might be preferable to have a visual separation of elements. You can do this by manipulating the stroke and stroke-width variables to create a bordered segment effect.

For the following components, the stroke property by default is either none or the same color as its fill. You can tweak the variables accordingly to create the desired effect:

:root  {
--stroke: #fff;
--stroke-dark: #292b34;

/* Area */
--vis-area-stroke-width: 1px;
--vis-area-stroke-color: var(--stroke);
--vis-dark-area-stroke: var(--stroke-dark);

/* Donut */
--vis-donut-segment-stroke-width: 1px;

/* StackedBar */
--vis-stacked-bar-stroke-width: 1px;
--vis-stacked-bar-stroke-color: var(--stroke);
--vis-dark-stacked-bar-stroke: var(--stroke-dark);

/* Timeline */
--vis-timeline-line-stroke-width: 1px;
}

Area

Loading...

Donut

Loading...

Stacked Bar

Loading...

Timeline

Loading...

Gradient Fills with SVG defs

Use the svgDefs property on the container to inject custom SVG definitions — gradients, patterns, clip paths — that any component can reference by id.

Start by building your defs string, then pass the string to svgDefs on the container and reference each gradient by id in the color array. The example below defines one vertical <linearGradient> per series.

component.tsx
import { VisXYContainer, VisAxis, VisAxis, VisArea } from '@unovis/react'

function Component(props) {
const data: DataRecord[] = props.data
const svgDefs = `
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
`
const x = (d: DataRecord) => d.x
const y = [
(d: DataRecord) => d.y,
(d: DataRecord) => d.y1,
(d: DataRecord) => d.y2
]
const color = [
`url(#area-grad-0)`,
`url(#area-grad-1)`,
`url(#area-grad-2)`
]
const lineColor = [`#3b82f6`, `#ef4444`, `#f59e0b`]

return (
<VisXYContainer svgDefs={svgDefs} data={data}>
<VisAxis type="x"/>
<VisAxis type="y"/>
<VisArea
x={x}
y={y}
color={color}
line={true}
lineWidth={1}
lineColor={lineColor}
/>
</VisXYContainer>
)
}
Loading...
note

Alternatively, you can place the defs in a hidden <svg> element anywhere on the same page, and the url(#id) references will still resolve.

<VisArea ... color={gradientColors}/>
<svg style="position:absolute;width:0;height:0">
<defs>
<linearGradient id="area-grad-0" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.4" />
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef4444" stop-opacity="0.4" />
<stop offset="100%" stop-color="#ef4444" stop-opacity="0.02" />
</linearGradient>
<linearGradient id="area-grad-2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.4" />
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.02" />
</linearGradient>
</defs>
</svg>

SVG Filters (glow, bevel, 3D)

The same svgDefs property can hold custom SVG <filter> definitions for advanced visual effects — glow, bevel, inner shadow, or neon styling — that aren't possible with plain CSS. Combine it with the attributes config, available on every component, to apply a filter to the rendered shapes by their selector.

Because the filter is built on SourceGraphic, it acts as a pure rendering overlay: it enhances what's already drawn without touching the chart's colors.

import { StackedBar } from '@unovis/ts'

// A bevel filter (inner shadow + top highlight), clipped to each shape.
const svgDefs = `
<filter id="bevel" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
<feOffset in="blur" dx="0" dy="2" result="offsetBlur"/>
<feComposite in="SourceAlpha" in2="offsetBlur" operator="out" result="innerShadowMask"/>
<feFlood flood-color="black" flood-opacity="0.4" result="shadowColor"/>
<feComposite in="shadowColor" in2="innerShadowMask" operator="in" result="shadow"/>
<feMerge result="merged">
<feMergeNode in="SourceGraphic"/>
<feMergeNode in="shadow"/>
</feMerge>
<feComposite in="merged" in2="SourceGraphic" operator="in"/>
</filter>`

<VisXYContainer data={data} svgDefs={svgDefs}>
<VisStackedBar
x={d => d.x}
y={accessors}
attributes={{ [StackedBar.selectors.bar]: { filter: 'url(#bevel)' } }}
/>
</VisXYContainer>

The filter is set per shape (.bar, .point, .line, .area, .segmentArc, …) rather than on a wrapping group, so effects like inner shadow and bevel that rely on each shape's own SourceGraphic render correctly. See the Visual Effects (SVG Filters) gallery example for glow and bevel applied to bars, lines, and scatter points.