Skip to content

Chart

Bar chartTwo series side by side, in a card. The months are YearMonths: month-short writes the ticks (Jan), month the tooltip's label (January).
Thymeleaf source
<sl:card>
<sl:card-header>
<sl:card-title level="3">Bar Chart - Multiple</sl:card-title>
<sl:card-description>January - June 2024</sl:card-description>
</sl:card-header>
<sl:card-content>
<sl:chart th:data="${visitors}" label="Visitors, January to June 2024">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short" tick-margin="10"/>
<sl:chart-tooltip indicator="dashed" cursor="false" label-format="month"/>
<sl:chart-bar key="desktop" label="Desktop" radius="4"/>
<sl:chart-bar key="mobile" label="Mobile" radius="4"/>
</sl:chart>
</sl:card-content>
<sl:card-footer>
<p>Showing total visitors for the last 6 months</p>
</sl:card-footer>
</sl:card>
Rendered HTML
<div class="card">
<div class="card-header">
<div class="card-title" role="heading" aria-level="3">Bar Chart - Multiple</div>
<div class="card-description">January - June 2024</div>
</div>
<div class="card-content">
<sl-chart class="chart" label="Visitors, January to June 2024" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short" tick-margin="10"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" indicator="dashed" cursor="false" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-bar class="chart-bar" key="desktop" label="Desktop" radius="4"></sl-chart-bar>
<sl-chart-bar class="chart-bar" key="mobile" label="Mobile" radius="4"></sl-chart-bar>
<div class="chart-skeleton"></div>
</sl-chart>
</div>
<div class="card-footer">
<p>Showing total visitors for the last 6 months</p>
</div>
</div>

Recharts draws a chart in the browser, in the skin’s look. The template says what to draw, the controller gives the rows:

<sl:chart th:data="${visitors}" th:label="#{visitors.title}">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip/>
<sl:chart-bar key="desktop" th:label="#{visitors.desktop}"/>
<sl:chart-bar key="mobile" th:label="#{visitors.mobile}"/>
</sl:chart>
record MonthlyVisitors(YearMonth month, long desktop, long mobile) {}
@GetMapping("/visitors")
String visitors(Model model) {
model.addAttribute("visitors", repository.visitorsPerMonth()); // List<MonthlyVisitors>
return "visitors";
}
  • th:data takes a list of rows: records, objects or maps, or a Spring Data Page. They go into the page as JSON, written by Jackson.
  • The series parts (sl:chart-bar, sl:chart-line, sl:chart-area) name the property they draw in key, and carry the series’ label and color. The chart draws bars for bars only, lines for lines only, areas for areas only, and a composed chart for a mix.
  • The other parts add a grid, the axes, a tooltip and a legend. Every part is optional; their order is the order Recharts draws them in.
  • label names the chart for a screen reader. The chart takes the focus, and the arrow keys move the tooltip from one category to the next.

A series without a color takes the chart tokens in turn: the first var(--chart-1), the second var(--chart-2), up to --chart-5, then again from the first. A theme that redefines them recolours every chart. color takes any CSS colour, and color-dark a different one for the dark theme:

<sl:chart-line key="revenue" color="oklch(0.55 0.2 260)" color-dark="oklch(0.75 0.15 260)"/>

The colour becomes the CSS property --color-<key> on the chart, which is why a key is checked to be a CSS identifier: letters, digits, _ and -, not starting with a digit or -. A key that is not fails the render.

Stacked bars and a legendThe same stack id stacks the series; the radius rounds the bottom of the first and the top of the last.
Thymeleaf source
<sl:chart th:data="${visitors}" label="Visitors, January to June 2024">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short" tick-margin="10"/>
<sl:chart-tooltip hide-label/>
<sl:chart-legend/>
<sl:chart-bar key="desktop" label="Desktop" stack="a" radius="0,0,4,4"/>
<sl:chart-bar key="mobile" label="Mobile" stack="a" radius="4,4,0,0"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Visitors, January to June 2024" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short" tick-margin="10"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" hide-label="true" value-format="number"></sl-chart-tooltip>
<sl-chart-legend class="chart-legend"></sl-chart-legend>
<sl-chart-bar class="chart-bar" key="desktop" label="Desktop" radius="0,0,4,4" stack="a"></sl-chart-bar>
<sl-chart-bar class="chart-bar" key="mobile" label="Mobile" radius="4,4,0,0" stack="a"></sl-chart-bar>
<div class="chart-skeleton"></div>
</sl-chart>

The legend lists the series by label, in the order of the series parts, like the tooltip.

Line chart
Thymeleaf source
<sl:chart th:data="${visitors}" label="Visitors, January to June 2024" margin="12">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip cursor="false" label-format="month"/>
<sl:chart-line key="desktop" label="Desktop"/>
<sl:chart-line key="mobile" label="Mobile"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Visitors, January to June 2024" margin="12" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" cursor="false" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-line class="chart-line" key="desktop" label="Desktop"></sl-chart-line>
<sl-chart-line class="chart-line" key="mobile" label="Mobile"></sl-chart-line>
<div class="chart-skeleton"></div>
</sl-chart>

margin leaves room left and right of the plot, so the first and last tick labels fit. curve sets how a line runs between the points (monotone, natural, linear, step), dots puts a dot on each.

Actual and forecastA dashed series for a forecast, target or budget. The rows without a number (null) leave a gap, so the actual line stops at the last month with figures; join names the series the forecast continues, so it starts there without a number of its own, and the tooltip lists only the actual value for that month. The legend and the tooltip show the forecast dashed too.
Thymeleaf source
<sl:chart th:data="${sales}" label="Cumulative sales, January to December 2024" margin="12">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip cursor="false" label-format="month"/>
<sl:chart-legend/>
<sl:chart-line key="actual" label="Actual" color="var(--chart-1)"/>
<sl:chart-line key="forecast" label="Forecast" color="var(--chart-1)" dashed join="actual"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Cumulative sales, January to December 2024" margin="12" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" cursor="false" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-legend class="chart-legend"></sl-chart-legend>
<sl-chart-line class="chart-line" key="actual" label="Actual" color="var(--chart-1)"></sl-chart-line>
<sl-chart-line class="chart-line" key="forecast" label="Forecast" color="var(--chart-1)" dashed="true" join="actual"></sl-chart-line>
<div class="chart-skeleton"></div>
</sl-chart>

dashed draws a series dashed (on sl:chart-area only its line, the fill stays), for a forecast or a target next to actual figures. The legend and the tooltip show it dashed as well. A row without a number (null) leaves a gap, so an actual line stops at the last month with figures while the dashed one carries on. join (on sl:chart-line and sl:chart-area) names the series a dashed line continues (join="actual"): the line starts at that series’ last number, without a duplicate in your rows, and the tooltip does not list the forecast for that month.

Area chartThe tooltip's line indicator, nested beside the label when there is one series.
Thymeleaf source
<sl:chart th:data="${visitors}" label="Desktop visitors, January to June 2024" margin="12">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip indicator="line" cursor="false" label-format="month"/>
<sl:chart-area key="desktop" label="Desktop"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Desktop visitors, January to June 2024" margin="12" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" indicator="line" cursor="false" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-area class="chart-area" key="desktop" label="Desktop"></sl-chart-area>
<div class="chart-skeleton"></div>
</sl-chart>
Stacked areas
Thymeleaf source
<sl:chart th:data="${visitors}" label="Visitors, January to June 2024" margin="12">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip cursor="false" label-format="month"/>
<sl:chart-area key="mobile" label="Mobile" stack="a" color="var(--chart-2)"/>
<sl:chart-area key="desktop" label="Desktop" stack="a" color="var(--chart-1)"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Visitors, January to June 2024" margin="12" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" cursor="false" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-area class="chart-area" key="mobile" label="Mobile" color="var(--chart-2)" stack="a"></sl-chart-area>
<sl-chart-area class="chart-area" key="desktop" label="Desktop" color="var(--chart-1)" stack="a"></sl-chart-area>
<div class="chart-skeleton"></div>
</sl-chart>
Area with a forecastThe same on areas: the forecast's line is dashed, its fill stays, and join starts it at the last actual number.
Thymeleaf source
<sl:chart th:data="${sales}" label="Cumulative sales, January to December 2024" margin="12">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip indicator="line" cursor="false" label-format="month"/>
<sl:chart-legend/>
<sl:chart-area key="actual" label="Actual" color="var(--chart-1)"/>
<sl:chart-area key="forecast" label="Forecast" color="var(--chart-1)" fill-opacity="0.15" dashed join="actual"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Cumulative sales, January to December 2024" margin="12" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" indicator="line" cursor="false" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-legend class="chart-legend"></sl-chart-legend>
<sl-chart-area class="chart-area" key="actual" label="Actual" color="var(--chart-1)"></sl-chart-area>
<sl-chart-area class="chart-area" key="forecast" label="Forecast" color="var(--chart-1)" dashed="true" join="actual" fill-opacity="0.15"></sl-chart-area>
<div class="chart-skeleton"></div>
</sl-chart>

On an area, dashed dashes the line and the fill stays; a lower fill-opacity on the forecast sets it apart.

The tooltip shows the hovered category and every series’ label and value. indicator is the mark before each label: dot (the default), line or dashed. hide-label leaves out the category, hide-indicator the marks, and cursor="false" the highlight of the hovered category.

Instead of formatting functions, the parts take a named format, written with Intl in the page’s language (<html lang>, from Spring’s locale):

Format Writes For
number 1,234.5 numbers; the tooltip’s values by default
integer 1,235
compact 1.2K a value axis
percent 25% for 0.25
currency €1,234.50, with currency="EUR"
day Apr 1 dates
month-short Apr
month April
month-year Apr 2024
date Apr 1, 2024
time 10:00 AM

format on an axis writes its ticks, label-format and value-format on the tooltip its label and values. A format is an enum: a typo fails the render. A value of another kind (a date format on a name) is shown as it is.

FormatsDays as LocalDates (day on the axis, date in the tooltip), a value axis in compact numbers and the values in euros, all in the page's language.
Thymeleaf source
<sl:chart th:data="${revenue}" label="Revenue per day, 1 to 10 April 2024">
<sl:chart-grid/>
<sl:chart-x-axis key="day" format="day"/>
<sl:chart-y-axis format="compact"/>
<sl:chart-tooltip label-format="date" value-format="currency" currency="EUR"/>
<sl:chart-bar key="revenue" label="Revenue" radius="4"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Revenue per day, 1 to 10 April 2024" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="day" format="day"></sl-chart-x-axis>
<sl-chart-y-axis class="chart-y-axis" format="compact"></sl-chart-y-axis>
<sl-chart-tooltip class="chart-tooltip" label-format="date" value-format="currency" currency="EUR"></sl-chart-tooltip>
<sl-chart-bar class="chart-bar" key="revenue" label="Revenue" radius="4"></sl-chart-bar>
<div class="chart-skeleton"></div>
</sl-chart>

For a label no format writes, format it in Java and give the tooltip that property in label-key: it then shows that property of the hovered row as its label.

The dates are read by their Java type, as Jackson writes them, never by the browser’s date parser (which reads 2024-04-01 as midnight UTC, the evening before in America):

  • LocalDate (2024-04-01) and YearMonth (2024-04) are calendar dates: shown as they are, in every time zone.
  • LocalDateTime (2024-04-01T10:00:00) is a time on the clock: shown as written.
  • Instant, OffsetDateTime and ZonedDateTime (2024-04-01T08:00:00Z) are moments: shown in the chart’s time zone. That is the request’s (Spring’s LocaleContextHolder, which a LocaleResolver that knows the user’s zone sets; the server’s otherwise), or the one time-zone names.
Instants in a time zoneInstants (written with Z) are shown in the chart's time zone: the request's, or time-zone. The first point is 06:00 UTC, so 08:00 in Brussels.
Thymeleaf source
<sl:chart th:data="${requests}" label="Requests per hour, 1 April 2024" time-zone="Europe/Brussels" margin="12">
<sl:chart-grid/>
<sl:chart-x-axis key="hour" format="time"/>
<sl:chart-tooltip label-format="time" value-format="integer"/>
<sl:chart-line key="requests" label="Requests" dots/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Requests per hour, 1 April 2024" margin="12" time-zone="Europe/Brussels">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="hour" format="time"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" label-format="time" value-format="integer"></sl-chart-tooltip>
<sl-chart-line class="chart-line" key="requests" label="Requests" dots="true"></sl-chart-line>
<div class="chart-skeleton"></div>
</sl-chart>

For series known only at run time, put th:each on a series part:

Series from the modelth:each on a series part, for series known at run time. Each takes the next chart colour.
Thymeleaf source
<sl:chart th:data="${browsers}" label="Visitors per browser, January to June 2024">
<sl:chart-grid/>
<sl:chart-x-axis key="month" format="month-short"/>
<sl:chart-tooltip label-format="month"/>
<sl:chart-legend/>
<sl:chart-line th:each="browser : ${series}" th:key="${browser.key}" th:label="${browser.label}"/>
</sl:chart>
Rendered HTML
<sl-chart class="chart" label="Visitors per browser, January to June 2024" time-zone="UTC">
<script type="application/json">
</script>
<sl-chart-grid class="chart-grid"></sl-chart-grid>
<sl-chart-x-axis class="chart-x-axis" key="month" format="month-short"></sl-chart-x-axis>
<sl-chart-tooltip class="chart-tooltip" label-format="month" value-format="number"></sl-chart-tooltip>
<sl-chart-legend class="chart-legend"></sl-chart-legend>
<sl-chart-line class="chart-line" key="chrome" label="Chrome"></sl-chart-line>
<sl-chart-line class="chart-line" key="safari" label="Safari"></sl-chart-line>
<sl-chart-line class="chart-line" key="firefox" label="Firefox"></sl-chart-line>
<div class="chart-skeleton"></div>
</sl-chart>

A chart is as wide as its container and 16:9. For a fixed height, give it one in your CSS, with aspect-ratio: auto:

.visitors-chart { aspect-ratio: auto; height: 250px; }

On a dir="rtl" page the category axis runs from right to left and the value axis stands on the right; the arrow keys follow what you see.

  • A chart that htmx swaps in draws itself; one it swaps out is cleaned up.
  • Before htmx saves a page for Back, the charts are put back to their skeletons, so the saved page holds none of the inline styles Recharts sets (a strict Content-Security-Policy would report them when the page comes back). Back draws them again.

These Recharts features have no counterpart yet: pie, radar and radial charts (pie charts are planned), horizontal bars, gradients, custom tooltip and legend content, icons in the tooltip and legend, reference lines and brushes. For anything else, sl-chart is a custom element: an application can write its own.

AttributeValuesDefaultDescription
th:dataobjectrequiredThe rows: a list (or a Spring Data Page) of objects, records or maps, one per category or point in time. Each row holds the category (month) and a number per series (desktop, mobile). Serialised with Jackson: dates as ISO strings, which the named date formats read by their Java type.
labelstringrequiredThe chart's accessible name, such as its title: what a screen reader announces when the chart takes the focus.
marginnumber—Space in pixels left and right of the plot, so the first and last tick labels fit (12 suits a line or area chart).
time-zonestring—The time zone (an IANA id such as Europe/Brussels) instants in the data are shown in: Instant, OffsetDateTime and ZonedDateTime. Defaults to the request's (Spring's LocaleContextHolder). A LocalDate or LocalDateTime is shown as written, in any zone.

Every prop can also be set from an expression: th:data="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
verticalboolean—Vertical lines too, one per category.
horizontalbooleantrueHorizontal lines, one per value tick.

Every prop can also be set from an expression: th:vertical="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
keystringrequiredThe property of the rows that holds the category (month) or the date.
formatnumber integer compact percent currency day month-short month month-year date time—How the ticks are written, with Intl in the page's language. Numbers: number, integer, compact (1.2K), percent (0.25 is 25%), currency (with currency). Dates: day (Apr 1), month-short (Apr), month (April), month-year (Apr 2024), date, time. Without one, the values as they are.
currencystring—The ISO 4217 code (EUR) for format="currency".
tick-marginnumber8Space in pixels between the ticks and the plot.
min-tick-gapnumber5The least space in pixels between two ticks; Recharts leaves out ticks that would come closer, as on a long time axis.

Every prop can also be set from an expression: th:key="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
formatnumber integer compact percent currency day month-short month month-year date time—How the ticks are written, as on sl:chart-x-axis: compact (1.2K) and currency suit a value axis.
currencystring—The ISO 4217 code (EUR) for format="currency".
tick-marginnumber8Space in pixels between the ticks and the plot.
tick-countnumber5How many ticks Recharts aims for.

Every prop can also be set from an expression: th:format="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
indicatordot line dasheddotThe mark before each series' label, in its colour.
cursorbooleantrueHighlight the hovered category (a band behind bars, a line over lines and areas).
hide-labelboolean—Leave out the label at the top (the category).
hide-indicatorboolean—Leave out the marks before the series' labels.
label-keystring—The property of the rows to take the label from instead of the axis category: a series key gives that series' label, any other property its value in the hovered row, such as a date the server formatted.
label-formatnumber integer compact percent currency day month-short month month-year date time—How the label is written, as the ticks of sl:chart-x-axis (month, date, ...). Without one, as it is.
value-formatnumber integer compact percent currency day month-short month month-year date timenumberHow the values are written: number in the page's language, or currency, percent, ...
currencystring—The ISO 4217 code (EUR) for a currency format.

Every prop can also be set from an expression: th:indicator="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
vertical-alignbottom topbottomBelow the plot, or above it.

Every prop can also be set from an expression: th:vertical-align="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
keystringrequiredThe property of the rows that holds this series' numbers (desktop): letters, digits, _ and -, as it also names the colour property --color-&lt;key&gt;.
labelstring—The series' name in the tooltip and the legend, typically a message (th:label="#{visitors.desktop}"). Defaults to the key.
colorstring—Its colour, any CSS colour (var(--chart-3), #2563eb). Defaults to the chart tokens in turn: the first series var(--chart-1), the second var(--chart-2), and so on (after the fifth, again from the first).
color-darkstring—A different colour in the dark theme; color is then the light one (the two go into light-dark()).
radiusstring—The bars' corner radius in pixels: 4 for every corner, or four numbers (top-left, top-right, bottom-right, bottom-left), as 4,4,0,0 for the top bar of a stack.
stackstring—Stack this series on the others with the same stack id (any name, such as a) instead of drawing them side by side.

Every prop can also be set from an expression: th:key="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
keystringrequiredThe property of the rows that holds this series' numbers (desktop): letters, digits, _ and -, as it also names the colour property --color-&lt;key&gt;.
labelstring—The series' name in the tooltip and the legend, typically a message (th:label="#{visitors.desktop}"). Defaults to the key.
colorstring—Its colour, any CSS colour (var(--chart-3), #2563eb). Defaults to the chart tokens in turn: the first series var(--chart-1), the second var(--chart-2), and so on (after the fifth, again from the first).
color-darkstring—A different colour in the dark theme; color is then the light one (the two go into light-dark()).
curvemonotone natural linear step step-before step-aftermonotoneHow the line runs between the points: smooth (monotone, natural), straight (linear) or in steps.
dotsboolean—A dot on every point.
dashedboolean—Draw the line dashed, as a forecast or target next to actual figures. The legend and the tooltip show the series dashed as well. A row without a number (null) leaves a gap, so an actual line can stop where a dashed one takes over (see join).
joinstring—The key of the series this dashed line continues (actual). The chart draws the dashed line from that series' last number to its own first one, so the rows need no duplicated value, and the tooltip does not list this series at that last row. Needs dashed.
stroke-widthnumber2The line's width in pixels.

Every prop can also be set from an expression: th:key="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
keystringrequiredThe property of the rows that holds this series' numbers (desktop): letters, digits, _ and -, as it also names the colour property --color-&lt;key&gt;.
labelstring—The series' name in the tooltip and the legend, typically a message (th:label="#{visitors.desktop}"). Defaults to the key.
colorstring—Its colour, any CSS colour (var(--chart-3), #2563eb). Defaults to the chart tokens in turn: the first series var(--chart-1), the second var(--chart-2), and so on (after the fifth, again from the first).
color-darkstring—A different colour in the dark theme; color is then the light one (the two go into light-dark()).
curvenatural monotone linear step step-before step-afternaturalHow the line runs between the points: smooth (natural, monotone), straight (linear) or in steps.
stackstring—Stack this series on the others with the same stack id (any name, such as a).
dashedboolean—Draw the line on top of the area dashed (the fill stays as it is). The legend and the tooltip show the series dashed as well.
joinstring—The key of the series this dashed area continues (actual), as on sl:chart-line: it starts at that series' last number, and the tooltip does not list this series at that last row. Needs dashed.
fill-opacitynumber0.4The opacity of the fill, from 0 to 1.

Every prop can also be set from an expression: th:key="${...}". Any other attribute passes through to the rendered element, and class is appended.

The chart is the one component that is no Alpine component: Recharts is a React library, and Shadleaf runs it on Preact (preact/compat), which makes the chart’s script some 50 KB (gzip) smaller.

  • JavaScript is required. Without it, the chart’s skeleton stays where the chart would be.
  • It loads when it is needed. Every Shadleaf script (shadleaf.assets.alpine bundled, csp or external) holds a loader of about 1 KB. It loads the chart’s script (about 125 KB gzip) when the first chart comes near the viewport, so a page without charts never loads it. It needs no Alpine.
  • It works under a strict Content-Security-Policy, without 'unsafe-inline' or 'unsafe-eval': the rows are a JSON data block (<script type="application/json">), which never runs, and the colours are set through CSSOM rather than a <style> element.
  • The server renders <sl-chart class="chart"> holding the data block, one element per part (<sl-chart-bar key="desktop" label="Desktop">) and the skeleton. The script mounts the chart beside them and marks the chart data-ready once it has drawn.