“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’re the one who sucks at CSS. Right here’s the CSS Parser API. Go make your individual styling language and see how shut any different is to excellent.”
Nicely, CSS, you’ve been teasing me since 2017 with the opportunity 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 like 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 can do ::nth-letter in the event you wished.
You’re only a tease, CSS, which signifies that in 2026, I nonetheless can’t write kinds like Chris Coyier’s hypothetical instance from again in 2011.
h1.fancy::nth-letter(n) {
show: inline-block;
padding: 20px 10px;
coloration: white;
}
h1.fancy::nth-letter(even) {
remodel: skewY(15deg);
background: #C97A7A;
}
h1.fancy::nth-letter(odd) {
remodel: skewY(-15deg);
background: #8B3F3F;
}
Not possible demos of ::nth-letter
When you desire 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 display that utilizing this syntax is baby’s play.
If ::nth-letter existed, we might 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, as a result of using the brand new sibling-index() operate.
If we had ::nth-letter, we might migrate Temani Afif’s wonderful direction-aware elastic hover, then gleefully delete all of the spans within the unique 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 shouldn’t be attainable 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 inconceivable, it has been stated — by Philip Walton at Google, who tried actually arduous prior to now to make production-ready CSS polyfills — that it’s not attainable to jot down a dependable polyfill for CSS. He gave up the concept, however I prefer 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 outdated that the examples present methods to polyfill flexbox. Within the decade since he stopped supporting this library, it doesn’t seem to be the feasibility of excellent CSS polyfills has improved.
Nonetheless, Philip’s findings haven’t stopped cool CSS polyfills from present. They are often helpful, even when they will’t be excellent. Good is the enemy of excellent.
Why we’re not going to surrender on ::nth-letter
To take care of our motivation for simulating ::nth-letter, I word that the shortage of a spec would possibly make implementing it simpler than writing a real polyfill. Something we create on this area will technically be a shim slightly than a polyfill. All polyfills are shims, however not all shims are polyfills — like all cows are animals, however not the opposite means round.
We’re patching CSS so as to add performance that by no means existed, whereas a polyfill simulates a characteristic that exists in sure environments, and/or a minimum of has a proper spec. The closest we obtained to a draft spec was experimental work Adobe tried in WebKit again in 2012, which by no means obtained 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 free 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 will even work.
As Humpty Dumpty stated, the phrases will imply what I need 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 could possibly be:
- “C” as a result of that’s the third letter as it will seem whenever 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 baby of the paragraph ingredient. - “D” or “B” if we styled the paragraph to make use of a right-to-left writing path. In a extra possible state of affairs, 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 will depend on the supply order of the direct baby of the ingredient.
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 online makes use of English. If we’re simulating a browser characteristic, we are able to’t ignore different languages, can we?
Not solely are writing instructions completely different in languages aside from English, however some languages use a number of characters to characterize a single letter. Now, in principle, ::first-letter selects all components of such a letter. However the browser help for that’s poor. ::first-letter has another fascinating edge circumstances I wouldn’t have anticipated, resembling deciding on punctuation along with the primary letter, possibly as a result of that’s how drop caps are usually offered.
At this level, I determine 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 deal with whether or not every character is a letter. And I believe my 8-year-old would have been dissatisfied if the exclamation level he added to his rainbow textual content wasn’t coloured.
Look, in the event you don’t prefer it, return to your individual universe the place there’s no ::nth-letter in any respect. Or you’ll be able to tinker with the supply code I’ll present you subsequent.
Tips on how to write an inconceivable polyfill
I revealed this experimental library on npm. That’s what the above CodePen makes use of by way of unpckg. The ::nth-letter package deal obtained 1.3k downloads in its first week with out me promoting it, in order that was good.
As an alternative of making an attempt to construct an ideal polyfill, there’s a sure freedom in understanding we are able to’t. We’ll subsequently do the only factor that would probably work. We rewrite the CSS and remodel the DOM so the browser can do the remainder. Right here’s a simplified model that’s 29 strains of JavaScript and works in as we speak’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', 'hooked up');
new SplitText(el, { sort: 'chars', charsClass: 'char' });
});
});
}
});
So much 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 section, we get a way that introducing customized CSS syntax received’t be as straightforward as we’d hope. It’s much less conveniently apparent 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 enable it, however that is without doubt 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 sweet at.
The results of the alternative will translate the invalid CSS…
.rainbow::nth-letter(n) {
coloration: #f432a0;
}
…into this legitimate CSS:
.rainbow .char:nth-child(n) {
coloration: #f432a0;
}
This nice video concludes that the least unhealthy possibility 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 most effective of the unhealthy 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 kinds from the web page and replaces them with the rewritten kinds, 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, now we 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.
Getting 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 cut 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', 'hooked up');
new SplitText(el, { sort: 'chars', charsClass: 'char' });
});
}
It really works! The auto-magically generated CSS receives an auto-magically generated DOM to type. All of us reside fortunately ever after. Article over for actual this time.
Or is it?
Do now we have to change the DOM for this?
As talked about in a 2021 CSS-Methods 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 now we have to mess up the markup to make a comparatively easy aesthetic change.”
The identical submit spoke of a possible accessibility situation in the event you cut up characters into their very own parts: “display readers (some, anyway?) learn every of these characters with pauses in between.” Analysis exhibits that VoiceOver may cause this situation, though it’s reported that the position attribute can now alleviate it. The SplitText plugin I exploit additionally mechanically accounts for accessibility, but it surely could not work on all screenreaders, and sadly, accessibility for cut up textual content is tougher to get proper than you’d suppose.
Additionally, if ::nth-letter have been a local characteristic, it will be a pseudo-element. It will be nice if we might simulate that, understanding there’s a danger we’ll journey over these further parts that my library provides to the DOM.
A pseudo-element might give us the most effective 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 much like keep away from polluting our DOM?
Sure and no.
The tough fact is we could by no means have the ability 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’ll be able to see from this 2021 unofficial draft, if we ever get this API, it will probably expose the browser’s CSS parser for programmatic use — but it surely in all probability 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’s additionally the HTML-in-canvas API proposal which might allow us to customise the best way 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 might 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 gentle DOM with further markup, then now we have to cover that markup within the shadow DOM. If we do 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) {
coloration: pink;
}
The reason being that the shadow DOM of my ingredient would appear like:
1
2
A client of my part shouldn’t have the ability 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, resembling :empty and :last-child, can’t be appended“ to ::half. As soon as upon a time, there was a ::shadow pseudo-element that might 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 approach to nonetheless use :nth-child along with ::half in the event you suppose laterally.
What if we populate every character’s ::half attribute based mostly on the :nth-child selectors we all know we might want to help? We all know what these are, since we created them after we have been regex changing the kinds!
Then we’d have:
.rainbow::half(nth-child(n)) {
coloration: #f432a0;
}
And the HTML in our shadow DOM would look one thing like:
Rainbow
#ShadowRoot
We will 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', 'hooked up');
const wrapper = doc.createElement("span");
wrapper.setAttribute('aria-hidden', 'true');
wrapper.innerHTML = el.innerHTML;
shadow.appendChild(wrapper);
const cut up = new SplitText(wrapper, { sort: "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 most effective reply to make use of shadow DOM?
Probably not, it causes a minimum of two large points:
- This model received’t work on parts that don’t help attaching a shadow DOM, resembling
or. - We will’t use the emergent
sibling-index()operate within the kinds for a shadow half, as a result ofsibling-index()depends on understanding the construction of the DOM, similar to:nth-childdoes. This prevents supporting the textual content styling demos I confirmed in the beginning. These demos wouldn’t work with the shadow DOM model of::nth-letter.
I discover that ::first-letter can be significantly restricted within the styling it helps. That’s not sufficient cause to knowingly cripple our implementation of ::nth-letter when there’s an possibility to not. I conclude the sunshine DOM model is healthier. It could be “gross” markup, however a minimum of we’re now not those who want to jot down or preserve it. And if browsers ever help ::nth-letter natively, the design of the shim is meant so we‘d hold the CSS as-is, delete the reference to my library, and by no means communicate of it once more.
The (precise) ending
Now that now we have a easy foundation for implementing issues like ::nth-letter, it will be possible so as to add ::nth-word, ::nth-last-letter, and so forth. Chris Coyier confirmed cool use circumstances 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.
