In my previous 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:
<fret-board frets="4" strings="6" chord="C Major">
<string-note string="6" mute></string-note>
<string-note string="5" fret="3" finger="3"></string-note>
<string-note string="4" fret="2" finger="2"></string-note>
<string-note string="3" open></string-note>
<string-note string="2" fret="1" finger="1"></string-note>
<string-note string="1" open></string-note>
</fret-board>
... 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 <number>
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-note string="6" mute></string-note> <!-- Muted string -->
<string-note string="3" open></string-note> <!-- Open string -->
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-note string="6" fret="1" barre="6" finger="1"></string-note>
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, <ol>
, where we set the value
attribute on the first item:
<ol><li value="7"></li></ol>
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.
Top comments (30)
As a backend dev learning about the front end, this is very cool. As a totally blind person trying to learn to play the guitar, this is frustrating. Is there any way to make this work with a screen reader? There is so little accessible content out there already. I'd love to explore how this might be made accessible.
standardguitar.com/accessible-guit...
is a great resource, but not something I could embed in a HTML doc to be used by sighted and blind people alike.
Interesting, thank you for pointing that out! The markup consists of two custom elements, fret-board and string-note. Fret-board have “frets” and “strings” attributes, while string-note have “string”, “fret” and “finger” attributes — but while I find that intuitive and easy-to-grasp, I must admit I didn’t check how it’s presented to a screen-reader. I’ll look into that.
thanks Mads. I was thinking of an aria-label in the fret-board element.
Alternatively, I was thinking of doing something analogous to your CSS using SVG which would be included with an img element which honors the alt attribute.
Ultimately, I'd like something that default announces the chord (e.g., C major) but only enumerates the strings and frets when a key is pressed. A details/summary comboe could work but a screen reader by default would say "button collapsed"... too verbose if you already know the chord and just want to learn a song.
probably unlikely to find a solution that works across all browsers and all screen readers :)
Hi Joel,
I've added a details/summary section with chord-name and chord-info.
In Chrome DevTools, I wasn't able to see the expanded content in the A11y Tree unless I pre-loaded it with "open", but that might simply be an issue with refresh. Otherwise, let me know — then I can add the chord-info to an aria-label on the main fret-board element.
You can test it here:
browser.style/ui/fret-board/
Or install it from here:
npm i @browser.style/fret-board
Best wishes,
Mads
Hi Mads,
that's awesome!
Using single letter navigation in NVDA (screen reader I use), I could type 'f' and go to the next form element. that takes me to the next "button" (how details element is considered( which would tell me the chord name. I press space (or enter) and that expands the details and I hear the fingering for the chord. Perfect.
I don't know if I'd add aria-label. First, I don't know if it would work as, I think, by default the custom elements do not get focus. I don't know how aria-label would work in that case.
I really am in the learning process re front end and accessiblity. I developed infrastructure for telecom and API platforms before losing my sight. I knew nothing about accessibility and very little about front end dev. I was thrust into that world.
Anyway, the point I'm taking a long time to not make clearly is I'm high level familiar with accessibility trees and dynamic changes to html docs in the browser plus how CSS fits into all that.
I really appreciate you taking the time to try these changes. I'm very motivated now to go back through your post and really understand how this all works.
I'm developing a python package to server side render html docs of arbitrary complexity using classes to represent elements. The idea is accessibility will be enforced within the classes, to the degree possible. As part of that project, I have builders which are higher level with interfaces to encapsulate classes and attributes needed to create, for example, an accessible HTML table.
I'd love to add a chord library based on your work to the builders.
Happy to hear it worked - and thank you for sharing all this. Reach out if you encounter any issues.
Pretty cool way to lay out guitar chords in CSS like that, no JS needed. Makes me wonder if stuff like this will push more music tools to be just pure CSS/html eventually or if there'll always be a spot for heavier frameworks?
Thanks! Any specific tools you have in mind?
I did a CSS-grid piano:
codepen.io/stoumann/pen/ExpNrLR
Used in a Chord Visualizer:
codepen.io/stoumann/pen/dyjjWqJ
Awesome!
This just really spoke to me as a guitar player :)
I've done some similar music/dev - I used SVG for piano bennysutton.com/chords/the-1-5-4-6... - probably would have used your css approach had I seen it first. With relation to guitar chords the real problem is how to calculate all the different inversions and finger positions bennysutton.com/chords/chord-scales I created a json file that I would be happy to share with you
Would love to see this as a published web component. Would also love for it to play the chord. Some other musical ideas: Full tablature song display; Full sheet music display (with optional guitar chords of course!).
Great work.
I need more time 😁
Absolutely love this! Feels like the perfect blend of dev brain and musician brain. I’ve been working on a few music-related tools myself and this hit that sweet spot of simplicity and usefulness. Really appreciate how readable the markup is too. You made it easy to extend or drop into other projects without a bunch of dependencies. Total win. Thanks for sharing this!
Thank you – really hope someone will use it in their own projects!
Awesome :-)
Thank you!
This is great! are you expanding it? playing the sound, making it interactive?
Thanks! Yeah, might do that — first, I want to create a wrapper component that'll show all versions of a given chord.
Incredible, man! I never thought of representing chords like this
I think it’s a common way to display guitar chords; I have an old book of guitar chords, represented like this. If you add
direction: rtl;
it works for left-handed guitarists as well!This is lovely!
Thanks!
Thought the demo would play (audible) but it doesn't or at least I don't know how to play.
(BigSur 11.7.10, Google Chrome
No, it only teaches finger-positions for chords. You have to pick up a guitar and play yourself ;-)
Ah, I see, thanks. So the remark that it is running under Chrome has been misleading me to think it would produce sound, but that's hardly to work when it's CSS only :)
A for rendering it seems to work under Firefox either.
Exactly! Chrome is the only browser that currently supports the updated attr()-method in CSS, hence the "only in Chrome" comment. I wrote a small polyfill for other browsers.
Who's Fred? Haha. should be 'fret' but makes little difference. Great job!
Haha, well spotted! Thx