Fretboard NeckATM
The neck of a fretted instrument, written as one element with data attributes: the tuning names the strings, a capo clamps a fret, and the fingers go on it - a chord's shape drawn faint (its barre a dashed line, × on a muted string), the notes sounding filled with their fret, the notes coming next outlined. Nothing is drawn on an open string. The runtime draws the strings, the nut (or the capo's bold bar with the nut thin behind it), the fret wires, the inlays and the fret numbers. Across with the 1st string on top, as a tab draws it, or upright as a chord chart draws it; the right hand mirrors either, and string colours give every string a chart colour. Its State API sets the fingers - a Fretboard bound to it sets them as its playhead moves; one chord's diagram is the Chord Chart.
On this page (13)
§Fretboard Neck
A ukulele's neck in G C E A with the C shape on it: its one finger, on the 1st string at the 3rd fret, is sounding - filled, with its fret. The open strings show nothing. Open the State tab to put another shape, notes sounding, notes next, a barre or a capo on the neck.
§Notes next
The G shape (0232) faint, the note sounding filled - the 3rd string at the 2nd fret - and the one coming next outlined, the 2nd string at the 3rd fret: what a picking pattern plays now and then.
§Upright
data-orientation="vertical": the neck stood up as a chord chart draws it - the nut on top, the strings from the 4th (G) on the left. F (2010) with its 2nd string sounding.
§Right hand
data-hand="right" mirrors the neck for a left-handed player: across, the nut on the right.
§String colours
data-colors="strings": each string and its dots in a chart colour, the 1st in --chart-1.
§Guitar with a capo
Six strings (E A D G B e) and a capo on the 3rd fret: its bold bar on the 3rd fret wire, the nut a thin line behind it. The F shape counts from the capo, as tabs for a capo are written - its barre (data-barre="1") lies at the capo's 1st fret, the 6th string sounding there.
§Muted strings
A guitar's C (x32010): × at the nut on the muted 6th string, the strummed chord sounding - every fretted string filled with its fret, the open ones nothing.
§A scale
data-frets="12" shows twelve frets, every inlay up to the double dot at the 12th. The C major scale in the first position as notes sounding, no shape - its open C, E and A strings show nothing.
§Play it through the API
df$.shadcn.fretboardNeck.play() puts the notes sounding on the neck, with a shape and the notes next: Strum plays each chord's fretted strings; Pick walks through them one string at a time, the next string outlined.
§States
Named states via the shared State API, bound on each .fretboard-neck:
default- the neck with the fingers its config puts on it
The state takes { shape, barre, notes, next, capo }, merged into the current config - notes and next as "string/fret" text or a list of { string, fret }. getState() reports what the neck shows, also what a bound Fretboard or the markup set.
The first example carries data-state-demo; bun run screenshots captures screenshots/{mode}/fretboard-neck.png.
| State | Type | Values | Default | Description |
|---|---|---|---|---|
shape | string | — | — | A chord's shape, drawn faint; observed from getState().config.shape. |
notes | string | — | — | The notes sounding, "string/fret" pairs; observed from getState().config.notes. |
next | string | — | — | The notes coming next, outlined; observed from getState().config.next. |
barre | number | — | — | The fret the shape's barre lies on (empty: none); observed from getState().config.barre. |
capo | number | — | 0 | The capo's fret (0: none); observed from getState().config.capo. |
§API
Generated from fretboard-neck.ts and the shared State API - the descriptions are their JSDoc, the types are checked by the compiler.
States
type FretboardNeckState = 'default' - setState(name, config) takes the config of the state it names.
| State | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
default | The neck with the fingers the config puts on it.
|
Every element
| Member | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
el.api.setState<S extends FretboardNeckState>(name: S, config?: FretboardNeckStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | |||||||||
el.api.getState(): { name: FretboardNeckState; config: FretboardNeckStateConfigs[FretboardNeckState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed. Returns | |||||||||
el.api.render(state?: { name: FretboardNeckState; config: FretboardNeckStateConfigs[FretboardNeckState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | |||||||||
el.api.settled(): Promise<void> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). Returns | |||||||||
el.store: Store<{ name: FretboardNeckState; config: FretboardNeckStateConfigs[FretboardNeckState] }> | A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component. |
Registry
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
df$.shadcn.fretboardNeckApi.setState<S extends FretboardNeckState>(el: HTMLElement, name: S, config?: FretboardNeckStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | ||||||||||||
df$.shadcn.fretboardNeckApi.getState(el: HTMLElement): { name: FretboardNeckState; config: FretboardNeckStateConfigs[FretboardNeckState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed.
Returns | ||||||||||||
df$.shadcn.fretboardNeckApi.render(state: { name: FretboardNeckState; config: FretboardNeckStateConfigs[FretboardNeckState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | ||||||||||||
df$.shadcn.fretboardNeckApi.store(el: HTMLElement): Store<{ name: FretboardNeckState; config: FretboardNeckStateConfigs[FretboardNeckState] }> | The element's store (bindComponent made it).
Returns | ||||||||||||
df$.shadcn.fretboardNeckApi.commit<S extends FretboardNeckState>(el: HTMLElement, name: S, config?: FretboardNeckStateConfigs[S]): void | Record a state the element reached on its own (no DOM work) - for a component's own handlers.
| ||||||||||||
df$.shadcn.fretboardNeckStates: FretboardNeckState[] | The declared states, 'default' first: default. |
df$.shadcn.fretboardNeck
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
play(target: string | HTMLElement, notes: string | FretboardNeckNote[], config: FretboardNeckConfig = {}): FretboardNeckNote[] | Put fingers on the neck - the notes sounding, and optionally a shape and the notes next (setState; what it does not name stays).
Returns |
Types
| Type | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FretboardNeckConfig | The config the state takes.
| ||||||||||||||||||
FretboardNeckNote | A note on the neck.
|
§CSS view file
The scroll box, the board (strings and their names, the nut, the fret wires as a gradient, a capo's bar with the nut thin behind it), the inlays and fret numbers, the barre, the dots by role, the upright and mirrored neck, the string colours and the accessibility media queries.
/* -- Fretboard Neck --------------------------------------------- The neck of a fretted instrument: the runtime draws .fretboard-neck-board from the markup - the strings and their names, the nut on the fretting hand's side (a capo's bold bar on its fret wire, the nut a thin line behind it), the fret wires as a gradient, the inlays and fret numbers, a barre's dashed line and the dots of the fingers. Across (the 1st string on top, as a tab draws it) or upright (data-orientation="vertical", as a chord chart draws it); data-hand="right" mirrors either. The root scrolls when the neck is wider than its column. */@layer components { .fretboard-neck { display: block; max-inline-size: 100%; overflow-x: auto; color: var(--foreground); scrollbar-width: thin; } .fretboard-neck-board { --fretboard-neck-nut: 1.75rem; --fretboard-neck-gap: 1.375rem; --fretboard-neck-fret-size: 2rem; --fretboard-neck-strings: 4; --fretboard-neck-frets: 12; /* centring an element on its point: half its width toward the start of the line */ --fretboard-neck-tx: -50%; position: relative; min-inline-size: calc(var(--fretboard-neck-nut) + var(--fretboard-neck-frets) * 2.25rem); block-size: calc(var(--fretboard-neck-strings) * var(--fretboard-neck-gap)); margin-block: 0.25rem 1.25rem; margin-inline: 1.25rem 0.25rem; direction: ltr; } .fretboard-neck[data-hand="right"] .fretboard-neck-board { --fretboard-neck-tx: 50%; direction: rtl; } .fretboard-neck-string { position: absolute; inset-inline: 0; inset-block-start: calc((var(--fretboard-neck-s, 1) - 0.5) * var(--fretboard-neck-gap)); block-size: 1.5px; background-color: var(--fretboard-neck-color, var(--muted-foreground)); translate: 0 -50%; } .fretboard-neck-string-name { position: absolute; inset-inline: -1.25rem auto; inset-block-start: calc((var(--fretboard-neck-s, 1) - 0.5) * var(--fretboard-neck-gap)); color: var(--fretboard-neck-color, var(--muted-foreground)); font-family: var(--font-mono); font-size: 0.6875rem; line-height: 1; translate: 0 -50%; } /* the fret wires, after the nut */ .fretboard-neck-wires { position: absolute; inset-block: calc(var(--fretboard-neck-gap) / 2 - 1px); inset-inline: var(--fretboard-neck-nut) 0; border-inline-start: 4px solid var(--foreground); border-radius: 1px; background-image: linear-gradient(to left, var(--border) 2px, transparent 2px); background-size: calc(100% / var(--fretboard-neck-frets)) 100%; } /* a capo: the bold bar moves to its fret wire, the nut stays a thin line behind it */ .fretboard-neck[data-capo] .fretboard-neck-wires { border-inline-start-width: 2px; } .fretboard-neck-capo { position: absolute; inset-block: calc(var(--fretboard-neck-gap) / 2 - 4px); inset-inline-start: calc( var(--fretboard-neck-nut) + (100% - var(--fretboard-neck-nut)) * var(--fretboard-neck-f, 1) / var(--fretboard-neck-frets) ); inline-size: 5px; border-radius: 2px; background-color: var(--foreground); translate: var(--fretboard-neck-tx) 0; } /* a barre of the shape: one finger across its strings, a dashed line through them */ .fretboard-neck-barre { position: absolute; inset-block-start: calc((var(--fretboard-neck-s1, 1) - 0.5) * var(--fretboard-neck-gap) - 0.5rem); inset-inline-start: calc( var(--fretboard-neck-nut) + (100% - var(--fretboard-neck-nut)) * (var(--fretboard-neck-f, 1) - 0.5) / var(--fretboard-neck-frets) ); inline-size: 0; block-size: calc((var(--fretboard-neck-s2, 1) - var(--fretboard-neck-s1, 1)) * var(--fretboard-neck-gap) + 1rem); border-inline-start: 3px dashed var(--muted-foreground); translate: var(--fretboard-neck-tx) 0; } .fretboard-neck-inlay, .fretboard-neck-fret-number, .fretboard-neck-dot { position: absolute; inset-inline-start: calc( var(--fretboard-neck-nut) + (100% - var(--fretboard-neck-nut)) * (var(--fretboard-neck-f, 1) - 0.5) / var(--fretboard-neck-frets) ); translate: var(--fretboard-neck-tx) -50%; } .fretboard-neck-inlay { inset-block-start: 50%; inline-size: 0.5rem; block-size: 0.5rem; border-radius: 50%; background-color: var(--muted); box-shadow: inset 0 0 0 1px var(--border); &[data-double] { box-shadow: inset 0 0 0 1px var(--border), 0 calc(var(--fretboard-neck-gap) * -1) 0 var(--muted), 0 var(--fretboard-neck-gap) 0 var(--muted); } } .fretboard-neck-fret-number { inset-block-start: calc(100% + 0.625rem); color: var(--muted-foreground); font-family: var(--font-mono); font-size: 0.625rem; } .fretboard-neck-dot { inset-block-start: calc((var(--fretboard-neck-s, 1) - 0.5) * var(--fretboard-neck-gap)); display: grid; place-items: center; inline-size: 1.125rem; block-size: 1.125rem; border-radius: 50%; font-family: var(--font-mono); font-size: 0.625rem; font-weight: 600; line-height: 1; &[data-nut] { inset-inline-start: calc(var(--fretboard-neck-nut) / 2); } /* the shape */ &[data-role="chord"] { background-color: color-mix(in oklab, var(--fretboard-neck-color, var(--muted-foreground)) 30%, var(--card)); } &[data-role="muted"] { color: var(--muted-foreground); } /* what comes next */ &[data-role="next"] { border: 2px dashed var(--fretboard-neck-color, var(--primary)); background-color: var(--card); } /* what sounds now */ &[data-role="active"] { background-color: var(--fretboard-neck-color, var(--primary)); color: var(--primary-foreground); box-shadow: 0 0 0 3px color-mix(in oklab, var(--fretboard-neck-color, var(--primary)) 25%, transparent); } } /* upright: the nut on top, the strings from the highest-numbered on the left (from the right for the right hand) */ .fretboard-neck[data-orientation="vertical"] { & .fretboard-neck-board { min-inline-size: 0; inline-size: calc(var(--fretboard-neck-strings) * var(--fretboard-neck-gap)); block-size: calc(var(--fretboard-neck-nut) + var(--fretboard-neck-frets) * var(--fretboard-neck-fret-size)); margin-block: 1.25rem 0.5rem; margin-inline-start: 1.75rem; } & .fretboard-neck-string, & .fretboard-neck-string-name, & .fretboard-neck-dot { inset-inline-start: calc( (var(--fretboard-neck-strings) - var(--fretboard-neck-s, 1) + 0.5) * var(--fretboard-neck-gap) ); } & .fretboard-neck-string { inset-block: calc(var(--fretboard-neck-nut) - 2px) 0; inline-size: 1.5px; block-size: auto; translate: var(--fretboard-neck-tx) 0; } & .fretboard-neck-string-name { inset-block-start: -0.75rem; inset-inline-end: auto; translate: var(--fretboard-neck-tx) -50%; } & .fretboard-neck-wires { inset-block: var(--fretboard-neck-nut) 0; inset-inline: calc(var(--fretboard-neck-gap) / 2 - 1px); border-inline-start: 0; border-block-start: 4px solid var(--foreground); background-image: linear-gradient(to top, var(--border) 2px, transparent 2px); background-size: 100% calc(100% / var(--fretboard-neck-frets)); } & .fretboard-neck-inlay, & .fretboard-neck-fret-number, & .fretboard-neck-dot { inset-block-start: calc( var(--fretboard-neck-nut) + (100% - var(--fretboard-neck-nut)) * (var(--fretboard-neck-f, 1) - 0.5) / var(--fretboard-neck-frets) ); } & .fretboard-neck-inlay { inset-inline-start: 50%; &[data-double] { box-shadow: inset 0 0 0 1px var(--border), calc(var(--fretboard-neck-gap) * -1) 0 0 var(--muted), var(--fretboard-neck-gap) 0 0 var(--muted); } } & .fretboard-neck-fret-number { inset-inline-start: -1rem; } & .fretboard-neck-dot[data-nut] { inset-block-start: calc(var(--fretboard-neck-nut) / 2); } &[data-capo] .fretboard-neck-wires { border-block-start-width: 2px; } & .fretboard-neck-capo { inset-block: calc( var(--fretboard-neck-nut) + (100% - var(--fretboard-neck-nut)) * var(--fretboard-neck-f, 1) / var(--fretboard-neck-frets) ) auto; inset-inline: calc(var(--fretboard-neck-gap) / 2 - 4px); inline-size: auto; block-size: 5px; translate: 0 -50%; } & .fretboard-neck-barre { inset-block-start: calc( var(--fretboard-neck-nut) + (100% - var(--fretboard-neck-nut)) * (var(--fretboard-neck-f, 1) - 0.5) / var(--fretboard-neck-frets) ); inset-inline-start: calc( (var(--fretboard-neck-strings) - var(--fretboard-neck-s2, 1) + 0.5) * var(--fretboard-neck-gap) - 0.5rem ); inline-size: calc((var(--fretboard-neck-s2, 1) - var(--fretboard-neck-s1, 1)) * var(--fretboard-neck-gap) + 1rem); block-size: 0; border-inline-start: 0; border-block-start: 3px dashed var(--muted-foreground); translate: 0 -50%; } } /* string colours (data-colors="strings"): string n takes chart colour n - its line, its name, its dots - and from the 6th on they start over; text in the string's colour mixed toward the text colour, legible in either mode */ .fretboard-neck[data-colors="strings"] { & :is([data-string="1"], [data-string="6"], [data-string="11"]) { --fretboard-neck-color: var(--chart-1); } & :is([data-string="2"], [data-string="7"], [data-string="12"]) { --fretboard-neck-color: var(--chart-2); } & :is([data-string="3"], [data-string="8"]) { --fretboard-neck-color: var(--chart-3); } & :is([data-string="4"], [data-string="9"]) { --fretboard-neck-color: var(--chart-4); } & :is([data-string="5"], [data-string="10"]) { --fretboard-neck-color: var(--chart-5); } & .fretboard-neck-string { block-size: 2.5px; } &[data-orientation="vertical"] .fretboard-neck-string { inline-size: 2.5px; block-size: auto; } & :is(.fretboard-neck-string-name, .fretboard-neck-dot[data-role="muted"]) { color: color-mix(in oklab, var(--fretboard-neck-color, var(--foreground)) 55%, var(--foreground)); font-weight: 700; } & .fretboard-neck-dot[data-role="active"] { background-color: color-mix(in oklab, var(--fretboard-neck-color, var(--primary)) 40%, var(--card)); box-shadow: 0 0 0 3px var(--fretboard-neck-color, var(--primary)), 0 0 0 6px color-mix(in oklab, var(--fretboard-neck-color, var(--primary)) 25%, transparent); color: color-mix(in oklab, var(--fretboard-neck-color, var(--foreground)) 55%, var(--foreground)); } } @media (prefers-contrast: more) { .fretboard-neck-dot[data-role="chord"] { background-color: var(--muted-foreground); color: var(--card); } } @media (forced-colors: active) { .fretboard-neck-dot[data-role="active"] { background-color: Highlight; color: HighlightText; forced-color-adjust: none; } .fretboard-neck-string, .fretboard-neck-capo { background-color: CanvasText; forced-color-adjust: none; } }}§JavaScript view file
The shape's and the notes' parsers, the barre a shape can hold, the board drawn from the markup with each position's strongest role, the description, the State API and df$.shadcn.fretboardNeck.
// -- Fretboard Neck ----------------------------------------------// The neck of a fretted instrument from its markup: the strings of its tuning, the nut on the fretting hand's side (or// a capo's bold bar with the frets behind it), the inlays and fret numbers - and the fingers on it: a chord's shape// (faint; its barre a dashed line; × on a muted string), the notes sounding (filled, with their fret) and the notes// coming next (outlined); nothing on an open string. Across, the 1st string on top as a tab draws it, or upright as a// chord chart draws it; the right hand mirrors either. Bound to a Fretboard (data-fretboard-for) it follows the// playhead: the Fretboard sets its tuning, capo and frets and, as it plays, its shape and notes.// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();const fretboardNeckStates = ['default'];/** The config the state takes. */export interface FretboardNeckConfig { /** a chord's shape, drawn faint: a fret per string from the string the tuning names first to the 1st ("0003", "x32010", "x 10 12 12 12 10"); "" for none */ shape?: string; /** the fret the shape's barre lies on, null for none */ barre?: number | null; /** the notes sounding, drawn filled with their fret: "string/fret" pairs ("3/2 2/3"; string 1 is the highest), or a list of { string, fret } */ notes?: string | FretboardNeckNote[]; /** the notes coming next, drawn outlined - as notes */ next?: string | FretboardNeckNote[]; /** the capo's fret, 0 for none; the frets of the shape and the notes count from it */ capo?: number;}/** A note on the neck. */export interface FretboardNeckNote { /** the string, 1 = the highest */ string: number; /** the fret, 0 = open (nothing is drawn on an open string) */ fret: number;}// VERIFIED: (verify's API docs gate) the states below are exactly the declared ones, each// described, and every config field typed, described and named in the code./** setState() configs per state. */export interface FretboardNeckStateConfigs { /** The neck with the fingers the config puts on it. */ default: FretboardNeckConfig;}type Fret = number | 'x';type Barre = { fret: number; from: number; to: number };type Note = { string: number; fret: number | 'x' };const INLAYS = [3, 5, 7, 10, 12, 15];const ROLES = ['muted', 'chord', 'next', 'active'];let described = 0;const clamp = (v: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, v));const esc = (s: unknown): string => String(s).replace( /[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c, );const numAttr = (el: Element, name: string): number | null => { const v = el.getAttribute(name); return v === null || v === '' || !Number.isFinite(Number(v)) ? null : Number(v);};/** a shape from its text: one character per string, or a space- / comma-separated list once a fret has two digits */function parseShape(text: string | null | undefined): Fret[] | null { const s = String(text ?? '').trim(); if (!s) return null; const tokens = /[\s,]/.test(s) ? s.split(/[\s,]+/).filter(Boolean) : [...s]; const out: Fret[] = []; for (const t of tokens) { if (t === 'x' || t === 'X') out.push('x'); else if (/^\d{1,2}$/.test(t) && Number(t) <= 24) out.push(Number(t)); else return null; } return out.length >= 1 && out.length <= 12 ? out : null;}const shapeText = (shape: Fret[]): string => shape.some((f) => typeof f === 'number' && f > 9) ? shape.join(' ') : shape.join('');/** a barre on a fret: the strings from the first to the last pressed there, every string between pressed there or * higher - a barre the shape cannot hold is none */function barreOf(shape: Fret[], fret: number): Barre | null { if (!(fret > 0)) return null; const at = shape.map((f, i) => (f === fret ? i : -1)).filter((i) => i >= 0); if (at.length < 2) return null; const from = at[0]; const to = at[at.length - 1]; for (let i = from; i <= to; i++) if (typeof shape[i] !== 'number' || (shape[i] as number) < fret) return null; return { fret, from, to };}/** notes from their text ("3/2 2/3") or a list - each on a string and a fret that exist */function parseNotes(v: unknown): FretboardNeckNote[] { const list: unknown[] = Array.isArray(v) ? v : String(v ?? '') .trim() .split(/\s+/) .filter(Boolean); const out: FretboardNeckNote[] = []; for (const x of list) { const [s, f] = typeof x === 'string' ? x.split('/').map(Number) : [Number((x as FretboardNeckNote)?.string), Number((x as FretboardNeckNote)?.fret)]; if (Number.isInteger(s) && s >= 1 && s <= 12 && Number.isInteger(f) && f >= 0 && f <= 24) out.push({ string: s, fret: f }); } return out;}const notesText = (notes: FretboardNeckNote[]): string => notes.map((n) => `${n.string}/${n.fret}`).join(' ');/** the tuning's names, from the string it names first (the highest-numbered) to the 1st */const tuningOf = (el: Element): string[] => (el.getAttribute('data-tuning') ?? '').trim().split(/\s+/).filter(Boolean);const capoOf = (el: Element): number => clamp(Math.round(numAttr(el, 'data-capo') ?? 0), 0, 12);/** the neck: its board (strings, nut or capo, wires, inlays, fret numbers) and the fingers on it */function neckMarkup(el: Element): { board: string; text: string } { const shape = parseShape(el.getAttribute('data-shape')); const tuning = tuningOf(el); const n = tuning.length || shape?.length || Math.max(1, numAttr(el, 'data-strings') ?? 4); const capo = capoOf(el); const notes = parseNotes(el.getAttribute('data-notes')); const next = parseNotes(el.getAttribute('data-next')); const highest = Math.max( 3, ...(shape ?? []).map((f) => (typeof f === 'number' ? f : 0)), ...notes.map((x) => x.fret), ...next.map((x) => x.fret), ); const frets = clamp(Math.round(numAttr(el, 'data-frets') ?? capo + highest + 2), 3, 24); const strings = Array.from({ length: n }, (_, k) => { const s = n - k; const name = tuning[k] ?? ''; return `${name ? `<span class="fretboard-neck-string-name" data-string="${s}" style="--fretboard-neck-s: ${s}">${esc(name)}</span>` : ''}<span class="fretboard-neck-string" data-string="${s}" style="--fretboard-neck-s: ${s}"></span>`; }).join(''); const inlays = INLAYS.filter((f) => f <= frets) .map( (f) => `<span class="fretboard-neck-inlay" style="--fretboard-neck-f: ${f}"${f === 12 ? ' data-double' : ''}></span>`, ) .join(''); const numbers = INLAYS.filter((f) => f <= frets) .map((f) => `<span class="fretboard-neck-fret-number" style="--fretboard-neck-f: ${f}">${f}</span>`) .join(''); const capoBar = capo ? `<span class="fretboard-neck-capo" style="--fretboard-neck-f: ${capo}"></span>` : ''; // the fingers: each position takes its strongest role const roles = new Map<string, string>(); const put = (x: Note, role: string) => { if (x.fret === 0 || x.string < 1 || x.string > n) return; const k = `${x.string}/${x.fret}`; if (ROLES.indexOf(role) > ROLES.indexOf(roles.get(k) ?? '')) roles.set(k, role); }; const barre = shape ? barreOf(shape, numAttr(el, 'data-barre') ?? 0) : null; (shape ?? []).forEach((fret, k) => { if (barre && fret === barre.fret && k >= barre.from && k <= barre.to) return; put({ string: n - k, fret }, fret === 'x' ? 'muted' : 'chord'); }); next.forEach((x) => put(x, 'next')); notes.forEach((x) => put(x, 'active')); const bar = barre ? `<span class="fretboard-neck-barre" style="--fretboard-neck-f: ${capo + barre.fret}; --fretboard-neck-s1: ${n - barre.to}; --fretboard-neck-s2: ${n - barre.from}"></span>` : ''; const dots = [...roles] .map(([k, role]) => { const [string, fret] = k.split('/'); // a muted string: × at the nut, or just behind the capo const muted = fret === 'x'; return `<span class="fretboard-neck-dot" data-role="${role}" data-string="${string}"${muted && !capo ? ' data-nut' : ''} style="--fretboard-neck-s: ${string}; --fretboard-neck-f: ${muted ? capo : capo + Number(fret)}">${role === 'active' || role === 'muted' ? (muted ? '×' : fret) : ''}</span>`; }) .join(''); const say = (list: FretboardNeckNote[]) => list.map((x) => `string ${x.string} ${x.fret ? `fret ${x.fret}` : 'open'}`).join(', '); const text = [ tuning.length ? `${tuning.join(' ')} neck` : `${n} strings`, capo ? `capo ${capo}` : '', shape ? `shape ${shape.join(' ')}${barre ? `, barre at fret ${barre.fret}` : ''}` : '', notes.length ? `sounding ${say(notes)}` : '', next.length ? `next ${say(next)}` : '', ] .filter(Boolean) .join('; '); const board = `<div class="fretboard-neck-board" aria-hidden="true" style="--fretboard-neck-strings: ${n}; --fretboard-neck-frets: ${frets}"><div class="fretboard-neck-wires"></div>${strings}${inlays}${numbers}${capoBar}${bar}${dots}</div>`; return { board, text };}/** Draw the neck again from its markup. */function draw(el: HTMLElement): void { const m = neckMarkup(el); const rt = el._fretboardNeck as { board: string; desc: HTMLElement }; if (rt.board !== m.board) { rt.board = m.board; const board = dfDollar(m.board).get(0) as HTMLElement; const old = dfDollar(el).children('.fretboard-neck-board'); if (old.length) old.replaceWith(board); else dfDollar(el).prepend(board); } if (dfDollar(rt.desc).text() !== m.text) dfDollar(rt.desc).text(m.text);}/** the markup of the state: the shape, barre, notes, next notes and capo as data attributes */function applyMarkup(el: Element, _stateName: string, config: FretboardNeckConfig): void { const write = (name: string, v: string | null) => { if (el.getAttribute(name) !== v) dfDollar(el).attr(name, v); }; // the authored text stays while it means the same shape or notes ("x 5 7 7 7 5" is x57775) const same = (name: string, text: string | null, norm: (v: string | null) => string | null) => norm(el.getAttribute(name)) === text; const shapeNorm = (v: string | null) => { const s = parseShape(v); return s ? shapeText(s) : null; }; const notesNorm = (v: string | null) => notesText(parseNotes(v)) || null; if (config.shape !== undefined) { const text = shapeNorm(typeof config.shape === 'string' ? config.shape : null); if (!same('data-shape', text, shapeNorm)) write('data-shape', text); } if (config.barre !== undefined) write( 'data-barre', config.barre === null || !(Number(config.barre) > 0) ? null : String(Math.round(Number(config.barre))), ); if (config.notes !== undefined) { const text = notesText(parseNotes(config.notes)) || null; if (!same('data-notes', text, notesNorm)) write('data-notes', text); } if (config.next !== undefined) { const text = notesText(parseNotes(config.next)) || null; if (!same('data-next', text, notesNorm)) write('data-next', text); } if (config.capo !== undefined) { const c = clamp(Math.round(Number(config.capo) || 0), 0, 12); write('data-capo', c ? String(c) : null); }}/** what the neck shows, read from its markup */const configOf = (el: Element) => ({ shape: el.getAttribute('data-shape') ?? '', barre: numAttr(el, 'data-barre'), notes: el.getAttribute('data-notes') ?? '', next: el.getAttribute('data-next') ?? '', capo: capoOf(el),});/** UI side of setState: the state's markup, the neck drawn again. */function triggerStateChange(el: HTMLElement, stateName: string, incoming: FretboardNeckConfig): undefined { applyMarkup(el, stateName, incoming); if (el._fretboardNeck) draw(el); return undefined;}/** Registry-level API; pass the .fretboard-neck element explicitly. Unknown names throw. */export const fretboardNeckApi = componentState({ component: 'fretboard-neck', states: fretboardNeckStates, // the config is the fingers on the neck: a setState names what changes, the rest stays mergeConfig: true, apply: (el, state, _previous, incoming) => triggerStateChange(el, state.name, incoming as FretboardNeckConfig), // a bound Fretboard and the markup change it without setState read: (el, state) => { const now = configOf(el); const c = state.config as FretboardNeckConfig; if ( c.shape === now.shape && c.barre === now.barre && c.notes === now.notes && c.next === now.next && c.capo === now.capo ) return state; return { name: 'default', config: now as Record<string, unknown> }; }, markup: (el, state) => applyMarkup(el, state.name, state.config as FretboardNeckConfig),});df$.fretboardNeckApi = fretboardNeckApi;df$.fretboardNeckStates = fretboardNeckStates;function init() { dfDollar('.fretboard-neck:not([data-init])') .toArray() .forEach((el: HTMLElement) => { el.dataset.init = ''; const desc = dfDollar( `<span class="fretboard-neck-desc" id="fretboard-neck-desc-${++described}" hidden></span>`, ).get(0) as HTMLElement; dfDollar(el).append(desc); if (!el.hasAttribute('role')) dfDollar(el).attr('role', 'img'); const by = (el.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean); dfDollar(el).attr('aria-describedby', [...by, desc.id].join(' ')); el._fretboardNeck = { board: '', desc }; draw(el); // el.store + el.api (AGENTS.md "State through stores"): $('#neck').api.setState('default', { notes: '3/2' }) bindComponent(el, fretboardNeckApi, { name: 'default', config: configOf(el) }); // the markup changes the neck: drawn again new MutationObserver(() => draw(el)).observe(el, { attributes: true, attributeFilter: [ 'data-tuning', 'data-strings', 'data-frets', 'data-shape', 'data-barre', 'data-notes', 'data-next', 'data-capo', ], }); });}/** the neck of a target: the .fretboard-neck element or a selector of it */const resolve = (target: string | HTMLElement): HTMLElement | undefined => { const el = typeof target === 'string' ? dfDollar(target).get(0) : target; const neck = el?.closest?.<HTMLElement>('.fretboard-neck'); return neck?._fretboardNeck ? neck : undefined;};df$.fretboardNeck = { /** * Put fingers on the neck - the notes sounding, and optionally a shape and the notes next (setState; what it does * not name stays). * @param target - the .fretboard-neck element or a selector of it * @param notes - the notes sounding: "3/2 2/3" or a list of { string, fret }; [] or "" for none * @param config - the shape, barre, next notes or capo to show with them * @returns the notes now, as { string, fret }; [] for an unknown element */ play: ( target: string | HTMLElement, notes: string | FretboardNeckNote[], config: FretboardNeckConfig = {}, ): FretboardNeckNote[] => { const el = resolve(target); if (!el) return []; el.api?.setState('default', { ...config, notes }); return parseNotes(el.getAttribute('data-notes')); },};init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub