“I believe I’m executed with actuality.”
— The Seventh Circle by Architects
We’ve all, sooner or later, had the thought that CSS sucks. Certainly, the overhyped buzz round the brand new pretext.js library as a “CSS killer” displays how a lot all of us need to strangle CSS at instances
Sometime sooner or later, CSS would possibly reply again: “No, you might be the one who sucks at CSS. Right here’s the CSS Parser API. Go make your individual styling language and see how shut any various is to good.”
Properly, CSS, you’ve been teasing me since 2017 with the potential of that API, which I hoped would let me create my very own CSS syntax, however no such factor materialized.
And whereas I’m venting, since 2003 we’ve requested over and over and over for ::nth-letter, which looks as if a pure suggestion. I imply, we’ve all the time had ::first-letter to imitate print results like drop caps, so we all know you might do ::nth-letter if you happen to wished.
You’re only a tease, CSS, which implies that in 2026, I nonetheless can’t write types like Chris Coyier’s hypothetical instance from again in 2011.
h1.fancy::nth-letter(n) {
show: inline-block;
padding: 20px 10px;
colour: white;
}
h1.fancy::nth-letter(even) {
remodel: skewY(15deg);
background: #C97A7A;
}
h1.fancy::nth-letter(odd) {
remodel: skewY(-15deg);
background: #8B3F3F;
}
Unattainable demos of ::nth-letter
In case you want to play with an interactive instance, right here is the invalid syntax ::nth-letter working in CodePen.
And right here’s a video demo by my eight-year-old, to show that utilizing this syntax is youngster’s play.
If ::nth-letter existed, we may migrate my textual content vortex scrolling impact to make use of it, after which delete the JavaScript, as seen beneath. That is Chrome/Safari-only, attributable to the usage of the brand new sibling-index() operate.
If we had ::nth-letter, we may migrate Temani Afif’s superb direction-aware elastic hover, then gleefully delete all of the spans within the authentic markup round every letter. The ::nth-letter code can be as proven within the CodePen beneath.
If solely ::nth-letter existed, I would make it my mission to go round upgrading each typography styling demo to make use of it.
Alas, the syntax to make this work will not be potential with CSS and HTML. Such capabilities exist solely within the wildest realms of our creativeness. Article ends right here.
Wait, what? How do all these demos work?
Whereas we’re on the subject of doing the unattainable, it has been mentioned — by Philip Walton at Google, who tried actually exhausting prior to now to make production-ready CSS polyfills — that it’s not potential to jot down a dependable polyfill for CSS. He gave up the concept, however I wish to think about his nickname at Google turned “Polyphil,” so it wasn’t a complete loss.
Philip additionally created this deserted framework for creating CSS polyfills, which nonetheless works, though it’s so previous that the examples present easy methods to polyfill flexbox. Within the decade since he stopped supporting this library, it doesn’t look like the feasibility of good CSS polyfills has improved.
Nonetheless, Philip’s findings haven’t stopped cool CSS polyfills from current. They are often helpful, even when they will’t be good. Good is the enemy of fine.
Why we’re not going to surrender on ::nth-letter
To take care of our motivation for simulating ::nth-letter, I be aware that the dearth of a spec would possibly make implementing it simpler than writing a real polyfill. Something we create on this house will technically be a shim relatively than a polyfill. All polyfills are shims, however not all shims are polyfills — like all cows are animals, however not the opposite manner round.
We’re patching CSS so as to add performance that by no means existed, whereas a polyfill simulates a function that exists in sure environments, and/or a minimum of has a proper spec. The closest we received to a draft spec was experimental work Adobe tried in WebKit again in 2012, which by no means received anyplace.
Having defined that, I’ll use the phrases polyfill and shim interchangeably right here, as a result of polyfill is the extra well-known time period, and since I’m anyhow about to play quick and unfastened with what phrases imply.
Defining our phrases
Since no person is aware of how ::nth-letter would behave, I could make up my very own solutions to questions like these Jeremy Keith raised about how it could even work.
As Humpty Dumpty mentioned, the phrases will imply what I would like them to imply.
1. What does “nth” imply?
Jeremy questioned what the third letter in a paragraph can be. Take this instance markup:
ABCDEF
The third letter might be:
- “C” as a result of that’s the third letter as it could seem if you learn from left to proper, whatever the DOM construction. In any case,
p::first-letterwould choose “A,” even when that character was deeply nested in markup inside the paragraph. - “E” as a result of that’s what
:nth-childwould do. E is the third direct youngster of the paragraph component. - “D” or “B” if we styled the paragraph to make use of a right-to-left writing course. In a extra possible situation, if the paragraph above have been modified to
Hebrew characters are inherently right-to-left in Unicode — after which the reply can be completely different once more.אבקדפע
The reply, within the universe I created for this text, is that ::nth-letter will behave the identical as :nth-child, which is determined by the supply order of the direct youngster of the component.
Isn’t life less complicated when the rigorous drafting strategy of the W3C is changed with the whims of a lone crackpot?
2. What does “letter” imply?
We touched on how different languages would have an effect on ::nth-letter. Solely half of the net makes use of English. If we’re simulating a browser function, we are able to’t ignore different languages, can we?
Not solely are writing instructions completely different in languages apart from English, however some languages use a number of characters to signify a single letter. Now, in principle, ::first-letter selects all components of such a letter. However the browser assist for that’s poor. ::first-letter has another fascinating edge instances I wouldn’t have anticipated, corresponding to choosing punctuation along with the primary letter, possibly as a result of that’s how drop caps are usually offered.
At this level, I resolve that any reply I give would disappoint some folks if their thought of a letter isn’t what’s chosen by ::nth-letter. To avoid this debate, let’s say ::nth-letter is an alias for the nth character.
A bit excessive, however the examples I confirmed above of how folks think about ::nth-letter don’t appear to give attention to whether or not every character is a letter. And I believe my 8-year-old would have been disenchanted if the exclamation level he added to his rainbow textual content wasn’t coloured.
Look, if you happen to don’t prefer it, return to your individual universe the place there’s no ::nth-letter in any respect. Or you possibly can tinker with the supply code I’ll present you subsequent.
Tips on how to write an unattainable polyfill
I revealed this experimental library on npm. That’s what the above CodePen makes use of by way of unpckg. The ::nth-letter bundle obtained 1.3k downloads in its first week with out me promoting it, in order that was good.
As an alternative of attempting to construct an ideal polyfill, there’s a sure freedom in figuring out we are able to’t. We’ll subsequently do the only factor that would presumably work. We rewrite the CSS and remodel the DOM so the browser can do the remaining. Right here’s a simplified model that’s 29 traces of JavaScript and works in right this moment’s browsers. As we discover the way it works, you’ll see that the brevity is achieved by leveraging what CSS can already do with minimal tampering.
import getCssData from 'get-css-data';
import { SplitText } from 'gsap/SplitText';
getCssData({
onComplete(cssText, cssArray, nodeArray) {
nodeArray.forEach(e => e.take away());
const selectors = new Set();
const nthArgs = new Set();
cssText = cssText.change(//*[sS]*?*//g, '');
// Substitute ::nth-letter with :nth-child in CSS
let rewrittenCss = cssText.change(
/([^,{{rn]+?)::?nth-letter[ t]*(([^n)]*))/gi,
(full, selector, args) => {
selector = selector.trim();
selectors.add(selector);
nthArgs.add(args);
// Use :nth-child as a substitute of ::nth-letter
return `${selector} .char:nth-child(${args})`;
}
);
doc.head.insertAdjacentHTML("beforeend", ``);
selectors.forEach(selector => {
doc.querySelectorAll(selector).forEach(el => {
if (el.hasAttribute('data-nth-letter')) return;
el.setAttribute('data-nth-letter', 'connected');
new SplitText(el, { kind: 'chars', charsClass: 'char' });
});
});
}
});
Lots is happening on this small block of code, so let’s break down the phases.
Translating ::nth-letter into legitimate CSS
Even at this primary part, we get a way that introducing customized CSS syntax received’t be as simple as we’d hope. It’s much less conveniently apparent easy methods to do it than monkey patching JavaScript, though the dangers are corresponding to patching globals in JavaScript.
The way in which CSS is utilized to an online web page doesn’t present an excellent alternative to intercept normal CSS behaviors and customise them.
Certainly, even making the nonstandard ::nth-letter syntax accessible to our JavaScript code is tough, as a result of the CSS parser will discard invalid CSS, so if the person consists of the selector .rainbow::nth-letter(2n), that received’t be accessible to JavaScript when it accesses the stylesheets property of the DOM.
We have to collect all uncooked CSS free from judgment of validity, so let’s use get-css-data, which concatenates the uncooked contents of any type tags within the DOM and makes use of fetch to incorporate the contents of every stylesheet imported by way of hyperlink tags.
Sidenote: get-css-data received’t work if the CORS coverage doesn’t permit it, however that is likely one of the inherent limitations of CSS polyfills.
Subsequent, we rewrite the nonstandard CSS utilizing common expressions, which is a bit ghetto. A extra rigorous method would use one thing like PostCSS at construct time. However, we are able to get away with regex on this case, as a result of we’re not doing our personal parsing of CSS; we’re doing a comparatively easy find-replace, which regex is nice at.
The results of the alternative will translate the invalid CSS…
.rainbow::nth-letter(n) {
colour: #f432a0;
}
…into this legitimate CSS:
.rainbow .char:nth-child(n) {
colour: #f432a0;
}
This nice video concludes that the least dangerous choice for implementing a CSS polyfill is to “rewrite the CSS to focus on particular person parts whereas sustaining cascade order.” Philip provides that he has “by no means seen a polyfill do that. I don’t suggest it, however I believe it’s the very best of the dangerous choices.” Higher late than by no means to create a polyfill utilizing this technique.
Implementing the translator for ::nth-letter
The shim removes the unique types from the web page and replaces them with the rewritten types, like so:
getCssData({
onComplete(cssText, cssArray, nodeArray) {
nodeArray.forEach(e => e.take away());
const selectors = new Set();
const nthArgs = new Set();
cssText = cssText.change(//*[sS]*?*//g, '');
// Substitute ::nth-letter with :nth-child in CSS
let rewrittenCss = cssText.change(
/([^,{{rn]+?)::?nth-letter[ t]*(([^n)]*))/gi,
(full, selector, args) => {
selector = selector.trim();
selectors.add(selector);
nthArgs.add(args);
// Use :nth-child as a substitute of ::nth-letter
return `${selector} .char:nth-child(${args})`;
}
);
doc.head.insertAdjacentHTML("beforeend", ``);
}
});
At this level, we now have translated the unsupported ::nth-letter syntax into legitimate CSS. Nevertheless it nonetheless wants some DOM parts to type, or it received’t do something.
Making ready the DOM
Since ::nth-letter doesn’t exist, my implementation is finally a handy abstraction for what I did manually in my spiral scrollytelling article. So, after gathering all the weather that require styling of particular person characters, we break up the focused content material into div tags, utilizing the freely accessible SplitText plugin from GSAP.
selectors.forEach(selector => {
doc.querySelectorAll(selector).forEach(el => {
if (el.hasAttribute('data-nth-letter')) return;
el.setAttribute('data-nth-letter', 'connected');
new SplitText(el, { kind: 'chars', charsClass: 'char' });
});
}
It really works! The auto-magically generated CSS receives an auto-magically generated DOM to type. All of us stay fortunately ever after. Article over for actual this time.
Or is it?
Do we now have to change the DOM for this?
As talked about in a 2021 CSS-Tips e-newsletter that lamented ::nth-letter being “sadly nonetheless not a factor,” the answer of spitting the textual content into separate parts per character is “fairly gross, proper? It’s a disgrace that we now have to mess up the markup to make a comparatively easy aesthetic change.”
The identical put up spoke of a possible accessibility subject if you happen to break up characters into their very own parts: “display readers (some, anyway?) learn every of these characters with pauses in between.” Analysis reveals that VoiceOver could cause this subject, though it’s reported that the function attribute can now alleviate it. The SplitText plugin I exploit additionally routinely accounts for accessibility, but it surely might not work on all screenreaders, and sadly, accessibility for break up textual content is more durable to get proper than you’d assume.
Additionally, if ::nth-letter have been a local function, it could be a pseudo-element. It will be nice if we may simulate that, figuring out there’s a threat we are going to journey over these further parts that my library provides to the DOM.
A pseudo-element may give us the very best of each worlds for fixing the duty at hand: one thing that’s purely presentational and doesn’t pollute the DOM, however can nonetheless behave like a part of the DOM for styling functions solely. Can we implement one thing just like keep away from polluting our DOM?
Sure and no.
The tough fact is we might by no means be capable to implement our personal customized pseudo-elements.
Earlier, I expressed the hope that the CSS Parser API would sometime assist, however even within the unlikely occasion that this API materializes, the intent wouldn’t be to permit builders to implement their very own CSS syntax or pseudo-elements. As you possibly can see from this 2021 unofficial draft, if we ever get this API, it could probably expose the browser’s CSS parser for programmatic use — but it surely most likely wouldn’t assist us customise how CSS is interpreted. Customized pseudo-elements can be the area of a hypothetical CSS Renderer API, which is one thing my mind simply got here up with that no person has even proposed.
Bramus from the Chrome group has a draft doc outlining how a CSS parser extensions API would work, and that is nearer to what I imagined the hypothetical CSS parser API would possibly present, however Bramus’s doc doesn’t at the moment focus on customized psuedo-elements. There may be additionally the HTML-in-canvas API proposal which might allow us to customise the way in which parts are rendered with out modifying their DOM. That’s already experimentally accessible in Chrome, however nonetheless wouldn’t give us customized psuedo-elements we may arbitrarily type utilizing CSS.
Shadow DOM model of ::nth-letter
If we’re caught with manipulating the DOM, the closest we are able to get to customized pseudo-elements is to cover the character parts within the shadow DOM of the focused parts, whereas exposing an API that lets us type chosen characters from outdoors the goal.
If we’re decided that focused parts of this new selector received’t pollute the mild DOM with further markup, then we now have to cover that markup within the shadow DOM. If we try this, then the closest I do know of to a customized pseudo-element is the ::half pseudo-element. If we use that, then by design, we are able to’t use:
.container::half(character):nth-child(2) {
colour: pink;
}
The reason being that the shadow DOM of my component would appear like:
1
2
A client of my part shouldn’t be capable to know the construction of the shadow DOM from outdoors the part utilizing CSS. That’s why “structural pseudo-classes that match based mostly on tree info, corresponding to :empty and :last-child, can’t be appended“ to ::half. As soon as upon a time, there was a ::shadow pseudo-element that may have allow us to type :nth-child from outdoors the shadow DOM, but it surely was deprecated a lifetime in the past.
Truly, there’s a method to nonetheless use :nth-child along with ::half if you happen to assume laterally.
What if we populate every character’s ::half attribute based mostly on the :nth-child selectors we all know we might want to assist? We all know what these are, since we created them once we have been regex changing the types!
Then we’d have:
.rainbow::half(nth-child(n)) {
colour: #f432a0;
}
And the HTML in our shadow DOM would look one thing like:
Rainbow
#ShadowRoot
We are able to generate such a shadow DOM utilizing the next barely extra complicated model of the JavaScript:
import getCssData from 'get-css-data';
import { SplitText } from 'gsap/SplitText';
getCssData({
onComplete(cssText, cssArray, nodeArray) {
nodeArray.forEach(e => e.take away());
const selectors = new Set();
const nthArgs = new Set();
// Take away CSS feedback
cssText = cssText.change(//*[sS]*?*//g, '');
let rewrittenCss = cssText.change(
/([^,{rn]+?)::?nth-letter[ t]*(([^n)]*))/gi,
(full, selector, args) => {
selector = selector.trim();
selectors.add(selector);
nthArgs.add(args);
return `${selector}::half(nth-child(${CSS.escape(args)}))`;
}
);
doc.head.insertAdjacentHTML("beforeend", ``);
selectors.forEach(selector => {
doc.querySelectorAll(selector).forEach(el => {
if (el.shadowRoot || el.hasAttribute('data-nth-letter')) return;
const shadow = el.attachShadow({ mode: "closed" });
el.setAttribute('data-nth-letter', 'connected');
const wrapper = doc.createElement("span");
wrapper.setAttribute('aria-hidden', 'true');
wrapper.innerHTML = el.innerHTML;
shadow.appendChild(wrapper);
const break up = new SplitText(wrapper, { kind: "chars", charsClass: "char" });
nthArgs.forEach((arg, i) => {
let chars = wrapper.querySelectorAll(`.char:nth-child(${arg})`);
chars.forEach(c => {
const prev = c.half || "";
c.half = (prev ? prev + " " : "") + `nth-child(${arg})`;
});
});
});
});
}
});
By pre-calculating the :nth-child selectors as names of the shadow components which match the ::nth-letter usages our CSS has requested, we are able to choose them from outdoors, with out touching the sunshine DOM, and with out hitting a brick wall of the intentional limitations of shadow DOM.
It really works! Are we there but? Is the very best reply to make use of shadow DOM?
Probably not, it causes a minimum of two huge points:
- This model received’t work on parts that don’t assist attaching a shadow DOM, corresponding to
or. - We are able to’t use the emergent
sibling-index()operate within the types for a shadow half, as a result ofsibling-index()depends on figuring out the construction of the DOM, similar to:nth-childdoes. This prevents supporting the textual content styling demos I confirmed at the beginning. These demos wouldn’t work with the shadow DOM model of::nth-letter.
I discover that ::first-letter can also be significantly restricted within the styling it helps. That’s not sufficient purpose to knowingly cripple our implementation of ::nth-letter when there’s an choice to not. I conclude the sunshine DOM model is best. It is likely to be “gross” markup, however a minimum of we’re now not those who want to jot down or keep it. And if browsers ever assist ::nth-letter natively, the design of the shim is meant so we‘d maintain the CSS as-is, delete the reference to my library, and by no means converse of it once more.
The (precise) ending
Now that we now have a easy foundation for implementing issues like ::nth-letter, it could be possible so as to add ::nth-word, ::nth-last-letter, and so forth. Chris Coyier confirmed cool use instances for these in [his call for ::nth everything.
There are still many limitations to the ::nth-letter shim, such as:
- It doesn’t work if you change the DOM or the styles on the fly, although we probably could support that.
- It doesn’t work if you use
::nth-letterin a CSS selector passed toquerySelectorAll, although we could monkey-patch JavaScript to make that work. - I am unsure how scalable it is.
- It could lead to hard-to-diagnose bugs because it rewrites all the CSS and adds unexpected “char” divs to the DOM. I noticed that Philip Schatz’s polyfill for a crazy working draft called the “CSS Generated Content Module” requires the consumer to opt-in by using special attributes on the
linkorstyletags. That’s an interesting compromise that might limit the blast radius by only triggering the CSS rewrites where we need them, but it seems less convenient than just referencing the library and then using the new syntax. - External stylesheets not allowed by CORS won’t work.
In summary, I’d probably use ::nth-letter and its hypothetical friends all the time if these features were built into browsers. But I must admit that, having explored the complexity of building generic support for a design we can often adequately solve with a few lines of JavaScript, I see why the browsers are reluctant to implement and maintain such a feature.
My shim might give the powers that be another reason to say native support isn’t necessary, or if lots of people use my ::nth-letter hack in the wild, the browser gods might recognize the need to implement it for real.
Either way, let’s never argue again, CSS. I understand now why you did what you did. I could never stay mad at you.









