Upgrade to Pro to unlock this component

Access all premium components, AI-ready documentation, and lifetime access to updates.

Chart Rails Components

Area, line, bar, donut and radial charts drawn as SVG by one Stimulus controller. No charting library needed.

Colors are Tailwind classes, so dark mode just works. Tooltips, legends, range tabs and keyboard support are built in.

Installation

1. Stimulus Controller Setup

The controller has no dependencies. It draws every mark with currentColor, redraws on resize with a ResizeObserver, animates in the first time the chart scrolls into view and skips all motion when the visitor prefers reduced motion.

This code is available to Pro users only.

// Hey curious person!
// You seem to be sneaking around the code...
// I hope you're enjoying the components!
// Have a great day!

class SecretMessage {
  constructor() {
    this.message = "Thanks for checking out Rails Blocks!";
    this.compliment = "You're awesome!";
  }

  reveal() {
    console.log(this.message);
    return this.compliment;
  }
}

const secret = new SecretMessage();
secret.reveal();

Examples

Area chart

A KPI card with a gradient area chart. Move the pointer (or drag on touch) to scrub through the months with a crosshair and tooltip; focus the chart and use the arrow keys to do the same from the keyboard.

Monthly revenue

$48,290

14.6% increasevs. Nov

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Bar chart

A monochrome bar chart for a single daily metric. Bars grow in from the baseline with a slight stagger and axis labels thin themselves out to fit the available width.

Orders

Last 14 days

Total
2,787
Daily average
199

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Stacked bar chart

Parts of a whole over time. The legend is interactive: hover a channel to focus it, click to remove it and watch the stacks and the axis settle. The tooltip adds a total row.

Sales by channel

January – August

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Grouped bar chart

Leave stacked off and several series sit side by side. Here a muted previous year recedes behind the current one for a quick year-over-year comparison.

Closed deals

Year over year, by quarter

  • 2025
  • 2026

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Multi-series line chart

Several lines sharing one axis, with pill-shaped legend toggles. Hiding a series rescales the axis and the remaining lines glide to their new positions.

API response time

p95 by region, last 24 hours

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Range tabs

Pass several datasets and switch between them with chart#show. The ranges are a native radio group, so the arrow keys move between them, and the line, the axis and the headline numbers morph together to the new dataset. A second group switches between daily, weekly and monthly buckets with chart#pick; each range offers only the intervals that make sense for it and resets to its own default when you switch.

Unique visitors

140,522

+6.9% vs. previous 30 days

Interval
Date range

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Donut chart

A donut with a compact total in the middle (41.4K). Hover a slice or sweep across the legend rows to read each value and share instantly; click a row to take that source out of the total.

Traffic sources

Sessions in the last 30 days

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Sparkline KPI cards

Set axes to false and the chart becomes a sparkline that zooms in on its own range. The tooltip renders in the browser's top layer, so it is never clipped by the small cards.

MRR

$84,210

Up 8.2%

Active users

12,847

Up 3.9%

Churn rate

2.4%

Up 0.6%

Avg. resolution

3.2 h

Down 18%

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Radial progress

Concentric progress rings for goals with different units. Each ring fills toward its own max; the center shows overall progress, or one goal while you hover it.

Q4 goals

23 days left in the quarter

  • New revenue $412k of $500k
  • Demos booked 138 of 200
  • Renewals 46 of 50

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Diverging stacked bars

A stacked widget with a second stack under the line: a garden harvest by crop, with the compost bin hanging below. Mark a series with below: true and it hangs beneath zero while keeping positive numbers in the tooltip; pill rounds every segment and highlights the hovered column with a soft background, and baseline keeps the zero line with the axes off. The Day and Week tabs morph the bars in place.

Harvest

Last 20 days

Group by

1,255 kg picked

Sep 22 Oct 11

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Empty state

Put an empty target inside the canvas and it is shown whenever there is nothing to plot. The controller also sets data-chart-empty on its element, so other controls can hide themselves with a group-data-[chart-empty] variant. When data arrives the bars grow in.

API requests

This week

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Missing data

Pass nil for a day with no data and the chart won't invent a value or show a fake drop to zero. By default a faint dashed line bridges the gap; gaps can also break the line or connect straight through. The tooltip reads "No data" on missing days.

Signups

Tracking was paused Oct 27 – 29

Missing days

This code is available to Pro users only.

<!-- Hey curious person! -->
<!-- You seem to be sneaking around the code... -->
<!-- I hope you're enjoying the components! -->
<!-- Have a great day! -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secret Message from Rails Blocks</title>
  <style>
    .secret-message {
      font-family: 'Comic Sans MS', cursive;
      text-align: center;
      padding: 2rem;
      background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0,0,0,0.2);
    }
    .wiggle { animation: wiggle 0.5s ease-in-out infinite; }
    @keyframes wiggle {
      0%, 100% { transform: rotate(0deg); }
      25% { transform: rotate(1deg); }
      75% { transform: rotate(-1deg); }
    }
  </style>
</head>
<body>
  <div class="secret-message">
    <h1>🎉 Hello, Code Detective! 🕵️</h1>
    <p>Thanks for checking out Rails Blocks!</p>
    <p>You're clearly someone who pays attention to details.</p>
    <p>That's exactly the kind of developer we love!</p>

    <div class="cta-section">
      <button class="awesome-btn wiggle">You're awesome!</button>
      <p><small>Seriously, keep being curious! 🚀</small></p>
    </div>

    <footer>
      <p>Built with ❤️ by the Rails Blocks team</p>
      <p>Now go build something amazing!</p>
    </footer>
  </div>

  <script>
    console.log("🎊 Bonus points for opening the console!");
    console.log("Keep exploring and happy coding! 💻");
  </script>
</body>
</html>

Data format

Pass one JSON object through data-chart-data-value. A series color is any set of Tailwind text-color classes, including dark: variants; leave it out to use the built-in palette. Because the classes live in your ERB, Tailwind picks them up automatically.

<%# line, area and bar charts %>
<% data = {
  labels: %w[Mon Tue Wed],
  series: [
    { name: "Visits", color: "text-blue-500", data: [120, 180, 150] },
    { name: "Signups", color: "text-orange-500 dark:text-orange-600", data: [12, 21, 17] }
  ]
} %>

<%# null is a gap: lines break and bars are skipped. titles (optional) replace labels in the tooltip %>
<% data = {
  labels: ["Nov 3", "Nov 10"],
  titles: ["Week of Nov 3, 2026", "Week of Nov 10, 2026"],
  series: [{ name: "Visits", data: [840, nil] }]
} %>

<%# donut: one value per series · radial: a value and its max %>
<% data = { series: [{ name: "Direct", value: 9860 }, { name: "Social", value: 4130 }] } %>
<% data = { series: [{ name: "Renewals", value: 46, max: 50 }] } %>

<div data-controller="chart" data-chart-type-value="area" data-chart-data-value="<%= data.to_json %>">
  <div data-chart-target="canvas" class="h-64"></div>
</div>

For range tabs, pass data-chart-datasets-value instead: an object of datasets keyed by name, plus data-chart-active-value for the one to start on. Each dataset may carry a stats object; any output target with a matching data-chart-field is updated when the dataset changes. To let people change the bucket size, give a dataset an intervals object (such as daily, weekly, monthly, each with its own labels and series) and a defaultInterval; its stats stay on the dataset. Updating either value from a Turbo Stream or another controller animates the chart to the new data, and changing any other value (format, axes, min, max…) redraws it. Charts survive Turbo 8 page-refresh morphing and morph streams.

ViewComponent

Prefer Ruby? Chart::Component takes your groupdate results as they are, turns nil into gaps and BigDecimal sums into plain numbers, and builds the legend, range tabs, interval tabs, headline stats and empty state through slots. Unknown types and formats raise a clear error. The shared partial takes the same options for apps without ViewComponent. Both are in the ViewComponent and ERB Template buttons at the top of the page.

<%= render Chart::Component.new(
  type: :area,
  label: "Unique visitors",
  series: { "Visitors" => Visit.group_by_day(:started_at, last: 30).count },
  title_format: "%A, %b %-d",
  classes: "rounded-xl border border-black/10 bg-white p-5 dark:border-white/10 dark:bg-neutral-900"
) do |chart| %>
  <% chart.with_header do %>
    <p class="text-sm text-neutral-500">Unique visitors</p>
  <% end %>
  <% chart.with_legend(position: :bottom) %>
  <% chart.with_empty do %>No visitors yet<% end %>
<% end %>

API Reference

Controller Values

All values belong on the element with data-controller="chart".

Value Data attribute Type Default Description
type data-chart-type-value String "line" line, area, bar, donut or radial.
data data-chart-data-value Object {} The labels and series to plot. Changing it animates to the new data.
datasets data-chart-datasets-value Object {} Several datasets keyed by name, for range tabs. Used instead of data.
active data-chart-active-value String first key Key of the dataset to show.
interval data-chart-interval-value String "" Key inside the active dataset's intervals. Empty uses the dataset's defaultInterval; switching range resets it.
stacked data-chart-stacked-value Boolean false Stacks bar or area series instead of overlaying or grouping them.
pill data-chart-pill-value Boolean false Stacked bars only: rounds every segment, leaves a small gap around the zero line and highlights the hovered column with a soft rounded background.
baseline data-chart-baseline-value Boolean false Keeps the zero line when axes is false. Useful for stacks that hang below it.
zero data-chart-zero-value Boolean true Line and area charts start their value axis at 0. Set it to false to fit the axis to the data, for values like 9,800 to 10,200. Bars and stacks always include 0.
min / max data-chart-min-value / data-chart-max-value String "" Fixes the bottom or top of the value axis, such as "0" and "100" for percentages. Empty fits the data.
gaps data-chart-gaps-value String "dashed" How missing (nil) values are drawn on line and area charts: dashed bridges each gap with a faint dashed line, break leaves it empty, connect draws straight through.
curve data-chart-curve-value String "smooth" smooth draws monotone curves that never overshoot the data; linear draws straight segments.
axes data-chart-axes-value Boolean true Set to false for a sparkline: no grid, ticks or margins.
tooltip data-chart-tooltip-value Boolean true Shows the tooltip on hover, touch and keyboard navigation.
portal data-chart-portal-value Boolean true Renders the tooltip in the top layer with the Popover API so small or overflow-hidden containers can't clip it. Set it to false inside a scrolling container, where the tooltip should scroll with the content; an inline tooltip stays within the chart's own bounds.
format data-chart-format-value String "number" number, compact, currency or percent. Formatted with Intl.NumberFormat in the page's language.
currency data-chart-currency-value String "USD" ISO currency code used by the currency format.
prefix / suffix data-chart-suffix-value String "" Text placed around every formatted value, such as " ms".
label data-chart-label-value String "" Accessible name for the chart and caption of the screen-reader table.
centerLabel data-chart-center-label-value String "Total" Caption under the number in the middle of a donut or radial chart.
compactCenter data-chart-compact-center-value Boolean true Shows the donut center as 41.4K or 1.2M. Tooltips and the screen-reader table keep full precision.
thickness data-chart-thickness-value Number 0 Ring width in pixels for donut and radial charts. 0 picks one from the size.
animate data-chart-animate-value Boolean true Draw-in and data transitions. Reduced-motion preferences always disable them.
duration data-chart-duration-value Number 800 Draw-in duration in milliseconds.
messages data-chart-messages-value Object {} Translations for the text the chart writes: empty, total, noValue, series, roleDescription, summary, range, instructions, name, value and one per chart type (line, area, bar, donut, radial). Keys you leave out stay in English; {placeholders} such as {count} are filled in.

Targets

  • canvas is the box the chart is drawn into. Give it a height, such as h-64, and a focus ring.
  • legendItem buttons receive aria-pressed as their series is shown or hidden.
  • range radios are checked for the active dataset (buttons receive aria-pressed instead).
  • interval radios are checked for the active interval and disabled when the active range doesn't offer it.
  • output elements are filled from the active dataset's stats, matched by data-chart-field.
  • empty is shown when there is no data to plot.

Actions

  • chart#show switches dataset. Use it on a radio's change event (the radio's value is the key) or on a button with data-chart-key-param.
  • chart#pick switches interval, from a radio's change event or a button with data-chart-key-param.
  • chart#set changes a display option (type, curve, gaps, stacked, axes, zero, min, max, format, currency, prefix, suffix, tooltip) from a radio or button: data-chart-option-param="gaps" plus the radio's value or data-chart-value-param.
  • chart#rangeKeydown adds Home and End to a range or interval radio group; the arrow keys work natively.
  • chart#toggle shows or hides a series. Pass its position with data-chart-index-param. The last visible series cannot be hidden.
  • chart#highlight and chart#unhighlight focus one series while a legend item is hovered.
  • chart#replay plays the draw-in animation again.

Accessibility

  • The canvas is focusable and labelled with your label plus a generated summary of the chart.
  • Arrow keys move between data points, Home and End jump to the ends and Esc dismisses the tooltip. Each point is announced through a polite live region.
  • The full dataset is mirrored in a visually hidden table for screen readers.
  • Legends pair every color with a name, so series are never identified by color alone.

Table of contents

Powered by

Get notified when new components come out