1 GitHub star = 1 new component.

Star the repo
Charts & Statschartsvgzero-dependencybulletkpitargetperformance

Bullet Chart

Compact KPI-vs-target bars with qualitative performance bands and an explicit on-target status.

Live Preview

Quarterly targets

Measure vs target with performance bands

Revenue ($k)270On target
Profit23%Below
New customers1650Below
NPS54On target
View data
MetricValueTargetStatus
Revenue ($k)270250On target
Profit23%26%Below target
New customers16502100Below target
NPS5450On target

Usage

Copy and paste this code into your React component:

tsx
import { BulletChart } from 'react-component-kit-ui/charts';

<BulletChart metrics={[
    { label: 'Revenue', value: 270, target: 250, ranges: [150, 225, 300], unit: '$k' },
    { label: 'NPS', value: 54, target: 50, ranges: [30, 50, 70] },
  ]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/BulletChart.tsx + BulletChart.css:

BulletChart.tsx
'use client';

import type { CSSProperties } from 'react';
import ChartFrame from './ChartFrame';
import './BulletChart.css';

/** "a: b; --c: d" โ†’ style object */
function st(css: string | undefined): CSSProperties {
  const o: Record<string, string> = {};
  for (const decl of String(css ?? '').split(';')) {
    const i = decl.indexOf(':');
    if (i < 0) continue;
    const k = decl.slice(0, i).trim();
    if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
  }
  return o as CSSProperties;
}

interface Metric {
  label: string;
  value: number;
  target: number;
  ranges: [number, number, number];
  unit?: string;
}

/**
 * BulletChart โ€” Compact KPI-vs-target bars with qualitative bands
 * (poor / ok / good), a measure bar and a target tick. Stack several for a grid.
 *
 * @param {Array<{label: string, value: number, target: number, ranges: [number, number, number], unit?: string}>} metrics
 *   - Each metric with its qualitative band thresholds (ascending).
 * @param {string} color - Measure bar color.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
export interface BulletChartProps {
  title?: string;
  subtitle?: string;
  metrics?: Metric[];
  color?: string;
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function BulletChart({
  title = 'Quarterly targets',
  subtitle = 'Measure vs target with performance bands',
  metrics = [
    { label: 'Revenue ($k)', value: 270, target: 250, ranges: [150, 225, 300] },
    { label: 'Profit', value: 23, target: 26, ranges: [20, 25, 30], unit: '%' },
    { label: 'New customers', value: 1650, target: 2100, ranges: [1400, 2000, 2500] },
    { label: 'NPS', value: 54, target: 50, ranges: [30, 50, 70] },
  ],
  color = 'var(--c-text-1, #fff)',
  variant = 'glass',
  size = 'md',
  className = '',
}: BulletChartProps) {
  const pct = (v: number, max: number) => `${Math.min(100, (v / max) * 100)}%`;
  return (
    <>
      <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} table={{ headers: ['Metric', 'Value', 'Target', 'Status'], rows: metrics.map((mt) => [mt.label, `${mt.value}${mt.unit ?? ''}`, `${mt.target}${mt.unit ?? ''}`, mt.value >= mt.target ? 'On target' : 'Below target']) }}>
        <div className="bc" data-rk-d2b72d="">
          {metrics.map((mt) => {
            const max = mt.ranges[2];
            const met = mt.value >= mt.target;
            return (
              <div className="bc__row cf-mark" tabIndex={0} role="meter" aria-valuemin={0} aria-valuemax={max} aria-valuenow={mt.value} aria-label={`${mt.label}: ${mt.value}${mt.unit ?? ''} of target ${mt.target}${mt.unit ?? ''}`} data-tip-title={mt.label} data-tip={`Value: ${mt.value}${mt.unit ?? ''}|Target: ${mt.target}${mt.unit ?? ''}|${met ? 'var(--chart-pos)::On target' : 'var(--chart-neg)::Below target'}`} data-rk-d2b72d="">
                <div className="bc__meta" data-rk-d2b72d="">
                  <span className="bc__label" data-rk-d2b72d="">{mt.label}</span>
                  <span className="bc__value" data-rk-d2b72d="">
                    {mt.value}{mt.unit ?? ''}
                    <span className={`bc__status ${met ? 'bc__status--ok' : 'bc__status--bad'}`} data-rk-d2b72d="">
                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden="true" data-rk-d2b72d="">
                        {met ? <path d="M20 6 9 17l-5-5" data-rk-d2b72d="" /> : <><circle cx="12" cy="12" r="9" data-rk-d2b72d="" /><path d="M12 8v4M12 16h.01" data-rk-d2b72d="" /></>}
                      </svg>
                      {met ? 'On target' : 'Below'}
                    </span>
                  </span>
                </div>
                <div className="bc__track" aria-hidden="true" data-rk-d2b72d="">
                  <span className="bc__band" style={st(`width:${pct(mt.ranges[2], max)}; opacity:0.07`)} data-rk-d2b72d=""></span>
                  <span className="bc__band" style={st(`width:${pct(mt.ranges[1], max)}; opacity:0.11`)} data-rk-d2b72d=""></span>
                  <span className="bc__band" style={st(`width:${pct(mt.ranges[0], max)}; opacity:0.16`)} data-rk-d2b72d=""></span>
                  <span className="bc__measure" style={st(`width:${pct(mt.value, max)}; background:${color}`)} data-rk-d2b72d=""></span>
                  <span className="bc__target" style={st(`left:${pct(mt.target, max)}`)} data-rk-d2b72d=""></span>
                </div>
              </div>
            );
          })}
        </div>
      </ChartFrame>
    </>
  );
}

export default BulletChart;
BulletChart.css
/* BulletChart โ€” ported from astro-component-kit (charts/BulletChart.astro) */

.bc:where([data-rk-d2b72d]) { display: grid; gap: 1rem; }
.bc__row:where([data-rk-d2b72d]) {
  display: grid;
  gap: 0.45rem;
  padding: 0.35rem;
  border-radius: 8px;
}
.bc__row:focus-visible:where([data-rk-d2b72d]) { outline: 2px solid var(--c-primary-light, #d4a017); }
.bc__meta:where([data-rk-d2b72d]) { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
.bc__label:where([data-rk-d2b72d]) { font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
.bc__value:where([data-rk-d2b72d]) {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 650;
  color: var(--c-text-1, #fff);
  font-variant-numeric: tabular-nums;
}
.bc__status:where([data-rk-d2b72d]) {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--c-text-2, #a3a3a3);
  white-space: nowrap;
}
.bc__status--ok:where([data-rk-d2b72d]) svg:where([data-rk-d2b72d]) { color: var(--chart-pos, #34c38f); }
.bc__status--bad:where([data-rk-d2b72d]) svg:where([data-rk-d2b72d]) { color: var(--chart-neg, #f06a6a); }
.bc__track:where([data-rk-d2b72d]) {
  position: relative;
  height: 22px;
  border-radius: 4px;
  overflow: hidden;
}
.bc__band:where([data-rk-d2b72d]) {
  position: absolute;
  inset: 0 auto 0 0;
  background: #fff;
}
.bc__measure:where([data-rk-d2b72d]) {
  position: absolute;
  left: 0;
  top: 7px;
  height: 8px;
  border-radius: 0 3px 3px 0;
  transform-origin: left;
  animation: bullet-chart-bc-grow 0.8s cubic-bezier(0.4, 0, 0.2, 1) both;
}
.bc__target:where([data-rk-d2b72d]) {
  position: absolute;
  top: 3px;
  bottom: 3px;
  width: 3px;
  margin-left: -1.5px;
  border-radius: 2px;
  background: var(--chart-4, #c98500);
}
@keyframes bullet-chart-bc-grow { from { transform: scaleX(0); } }
@media (prefers-reduced-motion: reduce) { .bc__measure:where([data-rk-d2b72d]) { animation: none; } }

Quick Info

Category
Charts & Stats
Filename
BulletChart.tsx
Dependencies
None โ€” pure React + CSS
Tags
chartsvgzero-dependencybulletkpitargetperformance