Chart

Interactive charts themed with shadcn CSS variables and rendered on Chart.js canvas.

Requires JavaScript Requires Chart.js
Monthly visitors
Month DesktopMobile
January18680
February305200
March237120
April73190
May209130
June214140
chart/default
<% data = {
  labels: ["January", "February", "March", "April", "May", "June"],
  datasets: [
    { key: :desktop, label: "Desktop", data: [186, 305, 237, 73, 209, 214] },
    { key: :mobile, label: "Mobile", data: [80, 200, 120, 190, 130, 140] }
  ]
} %>
<% config = {
  desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
  mobile: { label: "Mobile", color: "hsl(var(--chart-2))" }
} %>

<%= render Shadcn::Chart.new(
  type: :bar,
  data: data,
  config: config,
  aria_label: "Monthly visitors by device"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Monthly visitors</caption>
      <thead>
        <tr>
          <th class="text-left">Month</th>
          <th class="text-right">Desktop</th>
          <th class="text-right">Mobile</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |month, index| %>
          <tr>
            <td><%= month %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %></td>
            <td class="text-right"><%= data[:datasets][1][:data][index] %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>

Installation

Add the component, install the Chart.js peer dependency, and register the opt-in Chart controller entry:

rails generate shadcn:add chart
npm install chart.js@^4.5.1
# importmap apps can pin the dynamic import instead:
./bin/importmap pin chart.js/auto
import { registerShadcnControllers } from "shadcn-rails-stimulus"
import { registerShadcnChartController } from "shadcn-rails-stimulus/chart"

registerShadcnControllers(application)
registerShadcnChartController(application)

The all-in-one Stimulus entry intentionally excludes Chart so apps that do not render charts never resolve chart.js/auto.

Usage

Pass Chart.js-style data plus a config hash keyed by dataset key, dataset label, or pie label.

<% data = {
  labels: ["January", "February", "March"],
  datasets: [
    { key: :desktop, label: "Desktop", data: [186, 305, 237] },
    { key: :mobile, label: "Mobile", data: [80, 200, 120] }
  ]
} %>
<% config = {
  desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
  mobile: { label: "Mobile", color: "hsl(var(--chart-2))" }
} %>

<%= render Shadcn::Chart.new(
  type: :bar,
  data: data,
  config: config,
  aria_label: "Monthly visitors by device"
) do |chart| %>
  <% chart.with_fallback do %>
    <table>
      <caption>Monthly visitors by device</caption>
      <thead>
        <tr>
          <th>Month</th>
          <th>Desktop</th>
          <th>Mobile</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |month, index| %>
          <tr>
            <td><%= month %></td>
            <td><%= data[:datasets][0][:data][index] %></td>
            <td><%= data[:datasets][1][:data][index] %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>

Examples

Line

chart/line
<% data = {
  labels: ["January", "February", "March", "April", "May", "June"],
  datasets: [
    { key: :desktop, label: "Desktop", data: [186, 305, 237, 73, 209, 214] },
    { key: :mobile, label: "Mobile", data: [80, 200, 120, 190, 130, 140] }
  ]
} %>
<% config = {
  desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
  mobile: { label: "Mobile", color: "hsl(var(--chart-2))" }
} %>

<%= render Shadcn::Chart.new(
  type: :line,
  data: data,
  config: config,
  aria_label: "Monthly visitor trends by device"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Monthly visitor trends by device</caption>
      <thead>
        <tr>
          <th class="text-left">Month</th>
          <th class="text-right">Desktop</th>
          <th class="text-right">Mobile</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |month, index| %>
          <tr>
            <td><%= month %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %></td>
            <td class="text-right"><%= data[:datasets][1][:data][index] %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>
app/code_examples/chart/line.txt
<% data = {
  labels: ["January", "February", "March", "April", "May", "June"],
  datasets: [
    { key: :desktop, label: "Desktop", data: [186, 305, 237, 73, 209, 214] },
    { key: :mobile, label: "Mobile", data: [80, 200, 120, 190, 130, 140] }
  ]
} %>
<% config = {
  desktop: { label: "Desktop", color: "hsl(var(--chart-1))" },
  mobile: { label: "Mobile", color: "hsl(var(--chart-2))" }
} %>

<%= render Shadcn::Chart.new(
  type: :line,
  data: data,
  config: config,
  aria_label: "Monthly visitor trends by device"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Monthly visitor trends by device</caption>
      <thead>
        <tr>
          <th class="text-left">Month</th>
          <th class="text-right">Desktop</th>
          <th class="text-right">Mobile</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |month, index| %>
          <tr>
            <td><%= month %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %></td>
            <td class="text-right"><%= data[:datasets][1][:data][index] %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>

Area

chart/area
<% data = {
  labels: ["January", "February", "March", "April", "May", "June"],
  datasets: [
    { key: :organic, label: "Organic", data: [320, 401, 380, 520, 610, 750] },
    { key: :paid, label: "Paid", data: [180, 230, 280, 310, 390, 420] }
  ]
} %>
<% config = {
  organic: { label: "Organic", color: "hsl(var(--chart-1))" },
  paid: { label: "Paid", color: "hsl(var(--chart-2))" }
} %>

<%= render Shadcn::Chart.new(
  type: :area,
  data: data,
  config: config,
  aria_label: "Monthly acquisition volume by channel"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Monthly acquisition volume by channel</caption>
      <thead>
        <tr>
          <th class="text-left">Month</th>
          <th class="text-right">Organic</th>
          <th class="text-right">Paid</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |month, index| %>
          <tr>
            <td><%= month %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %></td>
            <td class="text-right"><%= data[:datasets][1][:data][index] %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>
app/code_examples/chart/area.txt
<% data = {
  labels: ["January", "February", "March", "April", "May", "June"],
  datasets: [
    { key: :organic, label: "Organic", data: [320, 401, 380, 520, 610, 750] },
    { key: :paid, label: "Paid", data: [180, 230, 280, 310, 390, 420] }
  ]
} %>
<% config = {
  organic: { label: "Organic", color: "hsl(var(--chart-1))" },
  paid: { label: "Paid", color: "hsl(var(--chart-2))" }
} %>

<%= render Shadcn::Chart.new(
  type: :area,
  data: data,
  config: config,
  aria_label: "Monthly acquisition volume by channel"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Monthly acquisition volume by channel</caption>
      <thead>
        <tr>
          <th class="text-left">Month</th>
          <th class="text-right">Organic</th>
          <th class="text-right">Paid</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |month, index| %>
          <tr>
            <td><%= month %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %></td>
            <td class="text-right"><%= data[:datasets][1][:data][index] %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>

Pie

chart/pie
<% data = {
  labels: ["Subscriptions", "Services", "Licensing", "Support"],
  datasets: [
    { label: "Revenue", data: [52, 26, 14, 8] }
  ]
} %>
<% config = {
  subscriptions: { label: "Subscriptions", color: "hsl(var(--chart-1))" },
  services: { label: "Services", color: "hsl(var(--chart-2))" },
  licensing: { label: "Licensing", color: "hsl(var(--chart-3))" },
  support: { label: "Support", color: "hsl(var(--chart-4))" }
} %>

<%= render Shadcn::Chart.new(
  type: :pie,
  data: data,
  config: config,
  aria_label: "Revenue mix by product line"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Revenue mix by product line</caption>
      <thead>
        <tr>
          <th class="text-left">Product line</th>
          <th class="text-right">Revenue share</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |label, index| %>
          <tr>
            <td><%= label %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %>%</td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>
app/code_examples/chart/pie.txt
<% data = {
  labels: ["Subscriptions", "Services", "Licensing", "Support"],
  datasets: [
    { label: "Revenue", data: [52, 26, 14, 8] }
  ]
} %>
<% config = {
  subscriptions: { label: "Subscriptions", color: "hsl(var(--chart-1))" },
  services: { label: "Services", color: "hsl(var(--chart-2))" },
  licensing: { label: "Licensing", color: "hsl(var(--chart-3))" },
  support: { label: "Support", color: "hsl(var(--chart-4))" }
} %>

<%= render Shadcn::Chart.new(
  type: :pie,
  data: data,
  config: config,
  aria_label: "Revenue mix by product line"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Revenue mix by product line</caption>
      <thead>
        <tr>
          <th class="text-left">Product line</th>
          <th class="text-right">Revenue share</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |label, index| %>
          <tr>
            <td><%= label %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %>%</td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>

Donut

chart/donut
<% data = {
  labels: ["Subscriptions", "Services", "Licensing", "Support"],
  datasets: [
    { label: "Revenue", data: [52, 26, 14, 8] }
  ]
} %>
<% config = {
  subscriptions: { label: "Subscriptions", color: "hsl(var(--chart-1))" },
  services: { label: "Services", color: "hsl(var(--chart-2))" },
  licensing: { label: "Licensing", color: "hsl(var(--chart-3))" },
  support: { label: "Support", color: "hsl(var(--chart-4))" }
} %>

<%= render Shadcn::Chart.new(
  type: :donut,
  data: data,
  config: config,
  aria_label: "Revenue mix by product line as a donut chart"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Revenue mix by product line</caption>
      <thead>
        <tr>
          <th class="text-left">Product line</th>
          <th class="text-right">Revenue share</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |label, index| %>
          <tr>
            <td><%= label %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %>%</td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>
app/code_examples/chart/donut.txt
<% data = {
  labels: ["Subscriptions", "Services", "Licensing", "Support"],
  datasets: [
    { label: "Revenue", data: [52, 26, 14, 8] }
  ]
} %>
<% config = {
  subscriptions: { label: "Subscriptions", color: "hsl(var(--chart-1))" },
  services: { label: "Services", color: "hsl(var(--chart-2))" },
  licensing: { label: "Licensing", color: "hsl(var(--chart-3))" },
  support: { label: "Support", color: "hsl(var(--chart-4))" }
} %>

<%= render Shadcn::Chart.new(
  type: :donut,
  data: data,
  config: config,
  aria_label: "Revenue mix by product line as a donut chart"
) do |chart| %>
  <% chart.with_fallback do %>
    <table class="w-full text-sm">
      <caption class="mb-2 font-medium">Revenue mix by product line</caption>
      <thead>
        <tr>
          <th class="text-left">Product line</th>
          <th class="text-right">Revenue share</th>
        </tr>
      </thead>
      <tbody>
        <% data[:labels].each_with_index do |label, index| %>
          <tr>
            <td><%= label %></td>
            <td class="text-right"><%= data[:datasets][0][:data][index] %>%</td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% end %>
<% end %>

API Reference

API Reference

Prop Type Default Description
type Symbol/String required One of :bar, :line, :area, :pie, or :donut.
data Hash required Chart.js-compatible labels and datasets.
config Hash {} Series labels and colors. Colors default to --chart-1 through --chart-5.
aria_label String "Chart" Accessible name for the chart region.
class_name String nil Additional CSS classes for the chart container.

Slots

Slot Description
with_fallback Visually hidden fallback content, usually a data table for assistive technology.

Accessibility

  • The chart container renders with role="img" and an accessible label.
  • The canvas is hidden from assistive technology, since canvas content is not meaningfully exposed.
  • Use with_fallback to provide a screen-reader-only table with the same data.