npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tungstenstudio/dartboard-input

v1.1.0

Published

Interactive SVG dartboard input component built with D3

Readme

@tungstenstudio/dartboard-input

Interactive SVG dartboard input component built with D3. Click, tap, or keyboard-navigate beds to fire throw events.

Live Demo

Install

npm install @tungstenstudio/dartboard-input d3-selection d3-shape
# or
pnpm add @tungstenstudio/dartboard-input d3-selection d3-shape
# or
yarn add @tungstenstudio/dartboard-input d3-selection d3-shape
# or
bun add @tungstenstudio/dartboard-input d3-selection d3-shape

Quick Start

import { Dartboard } from '@tungstenstudio/dartboard-input';
import '@tungstenstudio/dartboard-input/style.css';

const board = new Dartboard('#dartboard');
board.render();

document.querySelector('#dartboard').addEventListener('throw', (e) => {
  console.log(e.detail); // { bed: 'T20', segment: 20, ring: 'triple', score: 60 }
});

React Usage

import { useRef, useEffect } from 'react';
import { Dartboard } from '@tungstenstudio/dartboard-input';
import '@tungstenstudio/dartboard-input/style.css';

function DartboardInput({ onThrow }) {
  const ref = useRef(null);

  useEffect(() => {
    const board = new Dartboard(ref.current, { size: 400 });
    board.render();

    const handler = (e) => onThrow?.(e.detail);
    ref.current.addEventListener('throw', handler);

    return () => board.destroy();
  }, []);

  return <div ref={ref} />;
}

API

new Dartboard(container, options?, segments?, rings?)

Creates a dartboard instance.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | container | string \| HTMLElement | — | CSS selector or DOM element | | options | Partial<DartboardOptions> | DEFAULT_OPTIONS | Size, padding, and ring proportions | | segments | Segment[] | DEFAULT_SEGMENTS | Board segment layout (standard 1–20) | | rings | Rings | DEFAULT_RINGS | Ring definitions (names, abbreviations, multipliers) |

When size is null (the default), the board auto-sizes to fit its container using Math.min(offsetHeight, offsetWidth). The padding option (default 0) adds inner padding so the board edges aren't clipped by the viewBox boundary — the SVG viewBox stays at the full size while the board radius shrinks by the padding amount.

board.render(): this

Renders the dartboard SVG. Chainable.

board.destroy(): void

Removes the board from the DOM and cleans up references.

board.throwAt(target): void

Programmatically triggers a throw event.

board.throwAt('T20');                              // bed string
board.throwAt({ segment: 20, ring: 'TRIPLE' });   // explicit BedTarget

board.highlight(targets, options?): void

Adds a CSS class to targeted beds. All targeting methods accept any mix of bed strings, numbers, and BedTarget objects.

// Bed strings — target specific beds
board.highlight(['T20', 'D16', 'DB25']);

// Segment number — target all scoring beds on that segment
board.highlight([20]);       // double, triple, inner single, outer single
board.highlight(['20']);     // same thing as a string

// Miss ring
board.highlight(['miss']);   // highlight entire miss ring
board.highlight(['M20']);    // highlight a specific miss bed

// BedTarget object — for precision targeting
board.highlight([{ segment: 20, ring: 'INNER_SINGLE' }]);

// BedTarget without ring — same as segment number
board.highlight([{ segment: 20 }]);

// Custom class name
board.highlight(['D20'], { className: 'my-highlight' });

board.unhighlight(targets, options?): void

Removes a CSS class from specific beds (counterpart to highlight).

board.unhighlight(['T20']);
board.unhighlight([20]);     // unhighlight all beds on segment 20
board.unhighlight(['miss']); // unhighlight entire miss ring

board.reset(className?): void

Removes highlight class from all beds.

board.disable(): this

Disables the board — suppresses all throw and hover events, dims the board visually. Chainable.

board.enable(): this

Re-enables the board after disable(). Chainable.

board.disabled: boolean

Read-only property indicating whether the board is currently disabled.

board.segments: Segment[]

Read-only array of segments the board was constructed with.

board.rings: Rings

Read-only ring definitions the board was constructed with.

Bed Strings

Beds are identified by an abbreviation prefix and a segment number:

| Input | Targets | Example | |-------|---------|---------| | T + number | Triple | 'T20' | | D + number | Double | 'D16' | | S + number | Both singles (inner + outer) | 'S20' | | DB + number | Double Bull (Bullseye) | 'DB25' | | B + number | Single Bull (Bull) | 'B25' | | M + number | Miss | 'M20' | | number only | All scoring beds on that segment | '20' or 20 | | 'miss' | Entire miss ring (all 20 segments) | 'miss' |

When a number is used alone, all scoring beds on that segment are targeted (double, triple, inner single, outer single, single bull, double bull) — the miss ring is excluded. To target a specific single, use the explicit BedTarget form: { segment: 20, ring: 'INNER_SINGLE' }.

Events

throw

Dispatched on the container element when a bed is clicked, tapped, or activated via keyboard.

interface ThrowDetail {
  bed: string;      // e.g. 'T20', 'D16', 'B25', 'DB25', 'M20'
  segment: number;  // e.g. 20, 16, 25
  ring: string;     // e.g. 'triple', 'double', 'singleBull', 'doubleBull', 'miss'
  score: number;    // e.g. 60, 32, 25, 50, 0
}

hover

Dispatched when a bed is entered or left.

interface HoverDetail {
  bed: string;
  segment: number;
  ring: string;
  score: number;
  hovering: boolean;
}

Segment States

Three built-in state classes are included for marking beds as good, bad, or neutral — useful for coaching sessions, practice games, or heatmaps:

// Mark good targets (green)
board.highlight(['T20'], { className: 'is-good' });

// Mark bad targets (red)
board.highlight(['T1'], { className: 'is-bad' });

// Mark neutral targets (blue)
board.highlight(['D5'], { className: 'is-neutral' });

// Remove a specific state
board.unhighlight(['T1'], { className: 'is-bad' });

// Clear all of one state
board.reset('is-good');

The colors are themeable via CSS custom properties:

.c-Dartboard {
  --dartboard-good: #228b22;
  --dartboard-bad: #e32636;
  --dartboard-neutral: #4a6fa5;
}

You can also define your own state classes. Use this specificity pattern to override the default bed fills:

.c-Dartboard .c-Dartboard-bed.is-warning.isDark,
.c-Dartboard .c-Dartboard-bed.is-warning.isLight {
  fill: orange;
}

Bed Labels

Overlay a short, centered text label on every scoring bed, and update or clear it at runtime — without re-rendering the board. The label text is opaque: whatever string your labeler returns is rendered verbatim and given no meaning by the library, so it works equally for scores, coaching cues, glyphs, hit counts, or anything else.

board.labelBeds(labeler, options?): this

Draws (or replaces) a label on every scoring bed. The labeler is called once per bed with a BedLabelContext — the full bed model (segment, color, ring) plus a convenience score (segment × ring.multiplier; single bull = 25, double bull = 50). Return a string to render, or null / undefined / '' to skip that bed. Each call fully replaces the previous labels (no accumulation), so just call it again whenever your state changes. Chainable.

// "What's left" — overlay the score remaining after each bed, for a player on 70
board.labelBeds((bed) => String(70 - bed.score));
// S20 → "50", T20 → "10", D20 → "30", single bull → "45", double bull → "20"

Because the returned string is opaque, the same API labels beds with anything:

// Mark good/bad zones — arbitrary text, no scoring involved
board.labelBeds((bed) => (bed.ring.multiplier === 3 ? 'GOOD' : 'BAD'));

// A glyph on one bed, nothing elsewhere
board.labelBeds((bed) => (bed.ring.abbr === 'T' && bed.segment === 20 ? '✓' : ''));

board.clearBedLabels(): this

Removes all bed labels. (destroy() also removes them; reset() does not — it only clears highlight classes.) Chainable.

Notes

  • Coverage: all 82 scoring beds — inner/outer single, double, triple, and both bull rings. The MISS ring is never labeled (it already renders the 1–20 numbers and always scores 0).
  • Placement: labels are centered on each bed via the board's own geometry and scale with the board size. The double bull label sits at the center; the single bull label sits at the bottom of its ring, so the two never overlap.
  • Layering: labels render above the beds but below dart markers (addDart), and never intercept pointer/keyboard events, highlights, or throw/hover events.

Styling

Each label is a <text class="c-Dartboard-label"> (plus your optional options.className). Color is automatic: labels are light by default with a thin dark halo so they stay legible on the mid-tone red/green scoring beds, while the light (beige) single beds use dark text. All three are themeable:

.c-Dartboard {
  --dartboard-label-on-dark: #fff;            /* text on black / red / green beds */
  --dartboard-label-on-light: #111;           /* text on the beige single beds */
  --dartboard-label-halo: rgba(0, 0, 0, 0.6); /* outline drawn behind every label */
}

The scoring beds (double, triple, both bulls) are red/green even on their "light" segments, so their labels carry an extra c-Dartboard-label--scoring class that keeps the text light there.

The default font size scales with the board. Override it by targeting the text:

.c-Dartboard-label {
  font-size: 18px;
  font-weight: 700;
}

Pass options.className to style one overlay independently:

board.labelBeds((bed) => String(70 - bed.score), { className: 'remaining' });

Mobile / Touch-Friendly Sizing

The default ring proportions can be too narrow for comfortable tapping on small screens. The exported MOBILE_OPTIONS preset widens the scoring rings (double, triple, bull) for larger touch targets:

import { Dartboard, MOBILE_OPTIONS } from '@tungstenstudio/dartboard-input';

const isMobile = window.matchMedia('(max-width: 600px)').matches;
const board = new Dartboard('#dartboard', isMobile ? MOBILE_OPTIONS : {});
board.render();

You can also build your own proportions — the *Percent options control each ring's share of the radius (they should sum to 100):

new Dartboard('#dartboard', {
  missPercent: 7,
  doublePercent: 14,
  outerSinglePercent: 21,
  triplePercent: 14,
  innerSinglePercent: 26,
  singleBullPercent: 9,
  doubleBullPercent: 9,
});

Theming

Override CSS custom properties on the .c-Dartboard element:

.c-Dartboard {
  --dartboard-dark: #000;
  --dartboard-light: #f5f5dc;
  --dartboard-scoring-dark: #e32636;
  --dartboard-scoring-light: #228b22;
  --dartboard-miss: #000;
  --dartboard-miss-label: #fff;
  --dartboard-stroke: silver;
  --dartboard-stroke-width: 2px;
  --dartboard-highlight-color: #ffd700;
  --dartboard-dark-hover: #444;
  --dartboard-good: #228b22;
  --dartboard-bad: #e32636;
  --dartboard-neutral: #4a6fa5;
  --dartboard-label-on-dark: #fff;
  --dartboard-label-on-light: #111;
  --dartboard-label-halo: rgba(0, 0, 0, 0.6);
}

CSS Class Structure

Each bed element in the SVG has several classes you can use for fine-grained styling:

| Class | Description | |-------|-------------| | .c-Dartboard | Wrapper div around the SVG | | .c-Dartboard-bed | Every clickable bed element | | .c-Dartboard-bed--{n} | Bed for segment number n (e.g. .c-Dartboard-bed--20) | | .c-Dartboard-{ring} | Ring group: double, triple, innerSingle, outerSingle, singleBull, doubleBull, miss | | .isDark / .isLight | Alternating segment color class | | .is-hovered | Applied while a bed is pointer-hovered | | .is-highlighted | Default highlight class (includes a pulsing animation) | | .is-disabled | Applied to the wrapper when the board is disabled | | .c-Dartboard-missLabel | Segment number labels in the miss ring | | .c-Dartboard-labels | Group wrapping all bed labels (pointer-events: none) | | .c-Dartboard-label | A single bed label from labelBeds; also carries .isDark/.isLight | | .c-Dartboard-label--scoring | Label on a red/green scoring bed (double, triple, bull); keeps light text |

To override fills for a specific ring, combine the ring group with bed classes:

/* Make all triple beds gold */
.c-Dartboard .c-Dartboard-triple .c-Dartboard-bed.isDark,
.c-Dartboard .c-Dartboard-triple .c-Dartboard-bed.isLight {
  fill: gold;
}

/* Style a single bed */
.c-Dartboard .c-Dartboard-triple .c-Dartboard-bed--20 {
  fill: hotpink;
}

Keyboard & Accessibility

All beds are focusable (tabindex="0") with role="button" and descriptive aria-label attributes (e.g. "T20, scores 60"). The SVG has role="group" and aria-label="Dartboard".

  • Enter or Space on a focused bed dispatches a throw event
  • Tab / Shift+Tab navigates between beds
  • Focus is indicated with an outline using --dartboard-highlight-color

Terminology

This library uses standard darts terminology:

  • Segment — one of the 20 numbered wedges on the board (1–20)
  • Bed — a specific scoring zone: the intersection of a segment and a ring (e.g., "triple 20")
  • Ring — a concentric band: double, triple, inner single, outer single, single bull, double bull, miss

Types

All types are exported for TypeScript consumers:

  • Dartboard — main class
  • DartboardOptions — constructor options
  • ThrowDetail — throw event payload
  • HoverDetail — hover event payload
  • BedTarget — explicit target ({ segment, ring? }) — omit ring to target all scoring beds
  • BedInput — union of BedTarget | string | number accepted by all targeting methods
  • HighlightOptions — options for highlight
  • BedLabelContext — the labeler argument for labelBeds (Bed & { score: number })
  • BedLabelOptions — options for labelBeds
  • Ring, Rings, Segment, Bed — board model types

Constants

  • DEFAULT_OPTIONS — default ring proportions (see below)
  • MOBILE_OPTIONS — wider scoring rings for touch targets
  • DEFAULT_RINGS — standard ring definitions (name, abbreviation, multiplier)
  • DEFAULT_SEGMENTS — standard 1–20 segment layout with positions and colors

Default values (DEFAULT_OPTIONS):

| Option | Default | Mobile | |--------|---------|--------| | padding | 2 | — | | missPercent | 10 | 7 | | doublePercent | 10 | 14 | | outerSinglePercent | 25 | 21 | | triplePercent | 10 | 14 | | innerSinglePercent | 30 | 26 | | singleBullPercent | 8 | 9 | | doubleBullPercent | 7 | 9 |

Utilities

Exported helper functions:

  • parseBed(bed, rings, segments?) — parse a bed string into BedTarget[]
  • buildThrowDetail(bed) — convert a Bed into a ThrowDetail object
  • addRingToSegments(ring, segments) — combine a Ring with Segment[] to produce Bed[]
  • asPercent(n) — convert an integer percentage to a decimal (e.g. 100.1)

License

ISC