Guitar Chords in CSS
In my last article, I looked into the new, improved attr() method in CSS. I was over the moon (pun intended). This time, I’ll continue looking into the attr() method, showing how we can make easily readable components — at least for guitarists — like this: ... become this — with no JavaScript at all: Let’s get started! Basic grid First, we need a basic grid. How many cells and rows depend on the frets and strings attributes we defined in the HTML — so let’s grab those as and set two custom properties: --_frets: attr(frets type(), 4); --_strings: attr(strings type(), 6); In CSS, we’ll double the number of strings, so we can place notes on the strings — one grid-unit to the left of the string itself. We also want top and bottom rows for chord-name and open/mute indicators, so our grid looks like this: fret-board { grid-template-columns: repeat(calc(var(--_strings) * 2), 1fr); grid-template-rows: var(--fret-board-top-row-h, 12%) repeat(calc(var(--_frets)), 1fr) var(--fret-board-bottom-row-h, 15%); } We now have: Frets and strings The frets and strings are added to a pseudo-element as two linear gradients: fret-board { --fret-board-fret-c: light-dark(#000, #FFF); --fret-board-fret-w: clamp(0.0625rem, 0.03125rem + 0.5cqi, 0.5rem); --fret-board-string-c: light-dark(#0008, #FFF8); --fret-board-string-w: clamp(0.0625rem, 0.03125rem + 0.5cqi, 0.125rem); background-image: /* Vertical strings */ linear-gradient( 90deg, var(--fret-board-string-c) var(--fret-board-string-w), transparent 0 var(--fret-board-string-w) ), /* Horizontal frets */ linear-gradient( 180deg, var(--fret-board-fret-c) var(--fret-board-fret-w), transparent 0 var(--fret-board-fret-w) ); background-position: 0 var(--fret-board-fret-w), 0 0; background-repeat: repeat-x, repeat-y; background-size: /* Width and height for strings */ calc(100% / (var(--_strings) - 1) - (var(--fret-board-string-w) / var(--_strings))) calc(100% - (2 * var(--fret-board-fret-w))), /* Width and height for frets */ 100% calc(100% / var(--_frets) - (var(--fret-board-fret-w) / var(--_frets))); } OK, that was a handful — but we basically created horizontal lines for frets from top to bottom, and vertical lines for the strings running across the board. Adding notes and fingers Let’s add some notes — but first, let’s grab those attributes: --barre: attr(barre type(), 1); --fret: attr(fret type(), 0); --string: attr(string type(), 0); We’ll get to --barre soon, but for now, we place the note using this formula: string-note { grid-column: calc((var(--_strings) * 2) - (var(--string) * 2 - 1)) / span calc(var(--barre) * 2); grid-row: calc(var(--fret) + 1); } Let’s break this down: Grid Column Positioning: (var(--_strings) * 2) - This starts at the rightmost side of our grid (var(--string) * 2 - 1) - This calculates how far to move from the right For example, with 6 strings: String 1 (highest) goes at position 12 - (1*2-1) = 11 String 6 (lowest) goes at position 12 - (6*2-1) = 1 Span Calculation: span calc(var(--barre) * 2) - For regular notes, spans 2 columns For barre chords, spans more columns depending on how many strings are covered Grid Row Formula: calc(var(--fret) + 1) - The +1 accounts for the header row Open strings (fret = 0) go in row 1, first fret in row 2, etc. This gets us: Displaying finger numbers For good tablature, we need to show which finger to use for each note. We get this from the finger attribute in our HTML and display it using a pseudo-element: string-note::after { color: var(--string-note-c, light-dark(#FFF, #222)); content: attr(finger); font-size: var(--string-note-fs, 7cqi); font-weight: var(--string-note-fw, 500); text-box: cap alphabetic; } Note: text-box: cap alphabetic is a modern CSS feature that trims spacing created by line-height. The light-dark() function automatically adjusts text color for light or dark mode. Muted and open strings Guitar chords often involve strings that aren’t pressed down but are either played open or muted (not played): For the visual representation of string states: For muted strings, we create an X symbol using Temani Atif’s cross shape with border-image and a 45° rotation For open strings, we use a CSS mask with a radial gradient to create a hollow circle effect string-note[mute] { border-image: conic-gradient(var(--fret-board-bg) 0 0) 50%/calc(50% - 0.25cqi); rotate: 45deg; } string-note[open] { border-radius: 50%; mask: radial-gradient(circle farthest-side at center, transparent calc(100% - 1cqi), #000 calc(100% - 1cqi + 1px)); } Barre Chords A "barre chord" is one where you hold down multiple strings with one finge

In my last article, I looked into the new, improved attr()
method in CSS. I was over the moon (pun intended). This time, I’ll continue looking into the attr()
method, showing how we can make easily readable components — at least for guitarists — like this:
frets="4" strings="6" chord="C Major">
string="6" mute>
string="5" fret="3" finger="3">
string="4" fret="2" finger="2">
string="3" open>
string="2" fret="1" finger="1">
string="1" open>
... become this — with no JavaScript at all:
Let’s get started!
Basic grid
First, we need a basic grid. How many cells and rows depend on the frets
and strings
attributes we defined in the HTML — so let’s grab those as
and set two custom properties:
--_frets: attr(frets type(<number>), 4);
--_strings: attr(strings type(<number>), 6);
In CSS, we’ll double the number of strings, so we can place notes on the strings — one grid-unit to the left of the string itself. We also want top and bottom rows for chord-name and open/mute indicators, so our grid looks like this:
fret-board {
grid-template-columns: repeat(calc(var(--_strings) * 2), 1fr);
grid-template-rows:
var(--fret-board-top-row-h, 12%)
repeat(calc(var(--_frets)), 1fr)
var(--fret-board-bottom-row-h, 15%);
}
We now have:
Frets and strings
The frets and strings are added to a pseudo-element as two linear gradients:
fret-board {
--fret-board-fret-c: light-dark(#000, #FFF);
--fret-board-fret-w: clamp(0.0625rem, 0.03125rem + 0.5cqi, 0.5rem);
--fret-board-string-c: light-dark(#0008, #FFF8);
--fret-board-string-w: clamp(0.0625rem, 0.03125rem + 0.5cqi, 0.125rem);
background-image:
/* Vertical strings */
linear-gradient(
90deg,
var(--fret-board-string-c) var(--fret-board-string-w),
transparent 0 var(--fret-board-string-w)
),
/* Horizontal frets */
linear-gradient(
180deg,
var(--fret-board-fret-c) var(--fret-board-fret-w),
transparent 0 var(--fret-board-fret-w)
);
background-position:
0 var(--fret-board-fret-w),
0 0;
background-repeat:
repeat-x,
repeat-y;
background-size:
/* Width and height for strings */
calc(100% / (var(--_strings) - 1) - (var(--fret-board-string-w) / var(--_strings)))
calc(100% - (2 * var(--fret-board-fret-w))),
/* Width and height for frets */
100%
calc(100% / var(--_frets) - (var(--fret-board-fret-w) / var(--_frets)));
}
OK, that was a handful — but we basically created horizontal lines for frets from top to bottom, and vertical lines for the strings running across the board.
Adding notes and fingers
Let’s add some notes — but first, let’s grab those attributes:
--barre: attr(barre type(<number>), 1);
--fret: attr(fret type(<number>), 0);
--string: attr(string type(<number>), 0);
We’ll get to --barre
soon, but for now, we place the note using this formula:
string-note {
grid-column: calc((var(--_strings) * 2) - (var(--string) * 2 - 1)) / span calc(var(--barre) * 2);
grid-row: calc(var(--fret) + 1);
}
Let’s break this down:
-
Grid Column Positioning:
-
(var(--_strings) * 2)
- This starts at the rightmost side of our grid -
(var(--string) * 2 - 1)
- This calculates how far to move from the right - For example, with 6 strings:
- String 1 (highest) goes at position
12 - (1*2-1) = 11
- String 6 (lowest) goes at position
12 - (6*2-1) = 1
- String 1 (highest) goes at position
-
-
Span Calculation:
-
span calc(var(--barre) * 2)
- For regular notes, spans 2 columns - For barre chords, spans more columns depending on how many strings are covered
-
-
Grid Row Formula:
-
calc(var(--fret) + 1)
- The+1
accounts for the header row - Open strings (fret = 0) go in row 1, first fret in row 2, etc.
-
This gets us:
Displaying finger numbers
For good tablature, we need to show which finger to use for each note. We get this from the finger
attribute in our HTML and display it using a pseudo-element:
string-note::after {
color: var(--string-note-c, light-dark(#FFF, #222));
content: attr(finger);
font-size: var(--string-note-fs, 7cqi);
font-weight: var(--string-note-fw, 500);
text-box: cap alphabetic;
}
Note:
text-box: cap alphabetic
is a modern CSS feature that trims spacing created byline-height
. Thelight-dark()
function automatically adjusts text color for light or dark mode.
Muted and open strings
Guitar chords often involve strings that aren’t pressed down but are either played open or muted (not played):
string="6" mute>
string="3" open>
For the visual representation of string states:
- For muted strings, we create an X symbol using Temani Atif’s cross shape with
border-image
and a 45° rotation - For open strings, we use a CSS mask with a radial gradient to create a hollow circle effect
string-note[mute] {
border-image: conic-gradient(var(--fret-board-bg) 0 0) 50%/calc(50% - 0.25cqi);
rotate: 45deg;
}
string-note[open] {
border-radius: 50%;
mask: radial-gradient(circle farthest-side at center,
transparent calc(100% - 1cqi),
#000 calc(100% - 1cqi + 1px));
}
Barre Chords
A "barre chord" is one where you hold down multiple strings with one finger. Let’s add the barre
attribute in HTML:
string="6" fret="1" barre="6" finger="1">
We already covered the calculations above, but here’s how it looks visually:
Fret Numbers
Sometimes, a chord does not start on the first fret, and we need to indicate a fret number. For this, we use an ordered list,
, where we set the value
attribute on the first item:
- value="7">
You can inspect the styles in the final demo, but it looks like this:
Other instruments with frets and strings
Now, the great thing about controlling the CSS from a few attributes is how easy we can make it work for other instruments. Here are some examples:
Ukulele — 4 strings:
Banjo — 5 strings:
Demo
Here’s a demo. Please note that it only works in Chrome (for now):
I hope you enjoyed this article as much as I did writing it — now, I’ll grab my guitar and play some of these chords.