You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Whether using HTML, or using JavaScript libraries, the only way to target specific letters is to wrap them in uneccessary elements. This can cause accessibility issues—a logo being read out letter-by-letter, for example, rather than as a whole word.
I understand that this would be hard to implement (given that ::first-letter was hard enough) but I think the use-cases and accessibility benefits could outweigh the potential difficulty.
I've published a demo earlier this week for :nth-letter() and :nth-word() pseudos, including a JavaScript implementation that can be tested/used from CSS: https://codepen.io/tomhodgins/pen/YJZyPr
All of these examples (and I expect, nearly all reasonable examples you could dredge up beyond these) are fancy headings; any usage of ::nth-letter in body text would almost certainly be... dodgy at best. ::first-letter has its classic use-case of drop-caps (which we're blessing with specialized properties!), but even with that, there's a very good chance we wouldn't consider it worthwhile if someone tried to introduce it today!
One-off effects on top-level headings in a document still seem to be well-handled by just putting some spans around the letters you want to individually style. Letting people just do that, rather than trying to deal with all the complexities and weirdness of ::nth-letter, is probably still the way to go.
(For example, I don't think any of those examples work if a "real" element splits the character, like <span>fo</span><span>[COMBINING ACUTE ACCENT]o</span>. Looks like three letters, fóo, and any reasonable definition of ::nth-letter(2) would have it targeting the entire ó letter, but the pseudo-element has to be split into two separate boxes due to the existing tree structure.)
@upsuperinitial-letters is a layout property that happens to apply to :first-letter, it doesn't magically select the first letter the way text-combine-upright: digits magically selects numbers ...
@tabatkins I don't think any reasonably marked up document would put an element boundary there, so it's OK for that to be handled as a simplified error of some kind. I think the basic point is that pseudo-elements of this type make implementers very unhappy* so a moderately convincing use case with an easy workaround is likely not enough motivation.
A fun example is even-odd styling of :nth-letter() over a paragraph. Now insert a caret at character 5 and start typing. Somehow that has to work as expected, and reasonably preformantly as well because that's what the user expects. Other fun considerations include “exactly how do Indic scripts work here?”, “what about Dutch ij?” (which is probably not the same way as :first-letter), and other i18n inquiries.
One-off effects on top-level headings in a document still seem to be well-handled by just putting some spans around the letters you want to individually style.
Like I said, I knew it would be difficult to implement. Still, I'm surprised that difficulty of implementation would win out over the accessibility problems of wrapping individual letters in a bunch of spans.
All of these examples (and I expect, nearly all reasonable examples you could dredge up beyond these)
Dredge up.
Interesting.
Reacted by Ilya Streltsyn, Gregory Wild-Smith, Jen Simmons and Md Arif Hossain
Still, I'm surprised that difficulty of implementation would win out over the accessibility problems of wrapping individual letters in a bunch of spans.
The hierarchy of concerns tells us that certain players in the space matter more than others; user-a11y is generally more important than implementation difficulty. It is not, however, absolute - ::nth-letter() has significant implementation difficulties, over and above even that of ::first-letter which is already troublesome, and has less-common use-cases. All of these combine to at least make ::nth-letter() a rather low priority project, versus many other things we could add to CSS.
Solving the a11y issues can be done more simply than adding ::nth-letter(), such as having something in HTML or CSS that says "this element is not a semantic separation of its contained text from the surrounding text", which could be applied to the spans. This would also solve slightly wider problems than ::nth-letter(), as anything that splits up a word for any reason could also use it, rather than the solution being limited solely to the use-case of applying different CSS to letter-based groupings.
Dredge up.
Interesting.
? I'm confused, it looks like you might be reading some deep meaning into my wording?
Author of Splitting.js here. The difficulty of implementation is an issue I'm sure, but ::nth-letter would allow for so many great possibilities especially if combined with a letter-index() counter and letter-count() total, akin to this proposal to get the index of children.
Bonus points if ::nth-word could be implemented as well.
In the interim, I would love some proper accessibility support for splitting up text with <span>s, as mentioned. I've tried several approaches in Splitting but can't find a right setup for keeping the text properly available to screenreaders. Perhaps aria-role="inline" that would treat an element as if it were not distinct from its siblings.
Reacted by Adam Argyle, zack, schindld and kazerniel
Worth mentioning that we've had lots of discussion in SVG about how to handle features that assign properties to individual characters (w3c/svgwg#631). However, the conclusion we made there was to stict to predictable parsing (codepoints) over typographical elegance — an approach that isn't at all consistent with CSS ::first-letter.
Perhaps there should be a more extensive snapshot profile, i. e. as a separate specification, that also included properties or values, but as a selector, ::nth-letter() – and ::last-letter and ::nth-last-letter() – would fit in very well with :has(). That would only address performance concerns, not solve any internationalization issues at all, of course.
Snapshot profile is dead (luckily), it's just the draft hasn't been updated accordingly yet (I'm going to make a relevant PR this weekend). IMO, there is already no problem with accessing any character of the text (as well as with selecting DOM elements basing on their contents) from JS APIs. It's styling these things that CSS developers have demanded for years but can't do yet.
Here's a use case for ::nth-letter() (or at least second letter). When trying to reproduce historical texts like Shakespeare, initial letter doesn't work for several reasons:
There are hard line breaks in poetry and the dropcap should span two lines (or more in many cases) and it's not recognized (<br /> doesn't work for semantic reasons)
Sometimes the dropcap is two letters (see below).
The next letter after the dropcap is often capitalized, but not always.
If there's reason enough to support dropcaps, there should be reason enough to support them correctly, not superficially.
You could do this with initial-letter to enclose the two "V" characters in a span - (or making it a "W" and just using an appropriate font...) So I'm not sure this is a case for nth-letter
However, here's an idea. How about we introduce control over which letters are considered part of the first-letter by using Unicode joiners? We could we specify that the first-letter pseudo-element must not break at a ZWJ and must break at ZWNJ.
That would allow us to support this example with V‍VHen to the Sessions..., and also cover quite a few of the edge cases that seem to be coming up with punctuation and first-letter, without any additional CSS properties. I suspect this would be pretty easy to implement too.
It's possible to ask font designers to include these kinds of examples as single characters, but not likely to happen.
I tried initial-letter with a <span> and it doesn't work with two letters, nor does it work on inline elements (only block elements, which breaks the dropcap formatting). Here's a Codepen: https://codepen.io/severdia/pen/BapMMzx
I think it's an interesting idea to control it with a Unicode word joiner. That should work for all the use cases I can think of. Thanks!
As for ::nth-letter, I am not aware of any substitute, but neither am I aware of strong use cases.
A common use case for nth-letter/ nth-word / etc. are code or more general text editors and annotations for grammar, syntax, spelling, entities (NLP), etc. Which often require overlapping markup (see standoff properties / annotations for more).
In above use cases styling the characters is insufficient: You will also want to register event listener, arbitrary data, … on it. So I’m not convinced an extension of the CSS spec would be a good idea.
Except for using canvas/ CSS Houdini, I currently don’t see a good alternative to using a million span tags that wrap each character. To keep the markup readable I'm storing the plain text string in a data attribute of e.g. a paragraph tag. (however markup readability shouldn’t be a concern if you can inspect a readable JSON of the content)
While exploring staggered text fade-in animations, I built a rough prototype of ::nth-word() in Blink and found the performance promising compared to wrapping each word in a <span>. I'd like to explore the details a bit more.
Internationalization
There are existing solutions for determining what constitutes a "letter" (i.e. grapheme cluster) or "word," and I wouldn't want this feature to invent a worse wheel. There exists precedent in CSS to defer to the UA with a suggestion to use UAX-29 which is I believe what Intl.Segmenter uses. I think ::nth-letter() and ::nth-word() should follow suit.
Counting words
Words can cross element boundaries, which would suggest that ::nth-word() would be non-tree-abiding. Unfortunately, that forgoes a powerful capability: using counters, which when coupled with the parse() function (#12488) would give authors a lot of flexibility in determining how per-word effects progress. One might imagine a staggered text fade-in with specialized handling for various types of descendant blocks:
@mixin --staggered {
@starting-style { opacity:0; }
opacity:1;
transition: opacity 600ms;
transition-delay:calc(6ms*parse(<number> counter(stagger)));
counter-increment: stagger;
}
.fade-in {
counter-reset: stagger;
}
.fade-in>p::nth-word(n) {
@apply --staggered;
}
/* treat an inline code sample as an atomic unit */
.fade-in>pre.code {
@apply --staggered;
}
/* stagger in gallery images one at a time */
.fade-in> .gallery {
display: flex;
flex-wrap: wrap;
&>img {
@apply --staggered;
}
}
I would love if we could find a coherent way to make that work.
Barring that, we could imagine word-index() and letter-index() functions, akin to sibling-index(). Those would involve more baked-in indexing mechanics, including when indices reset. It would make it harder for authors to continue an effect like the above from one sibling paragraph to the next - they would need to keep a running word count to offset the delays in each paragraph. Or maybe there's a property that could be set to link indices together from one block to the next, and let blocks participate in indexing as atomic units, something like:
word-indexing: [ restart | continue | block ]
Only has an effect on block-level elements. Word indexing always continues through inline-level elements.
restart (initial value): the first word gets index 1. Each subsequent word's index increases by 1 until a descendant block is entered.
continue: the first word gets index n+1, where n is the index of the last word prior to the block in in-order traversal.
block: the block itself, and all words within it, are treated as if they are at index n+1, where n is defined as for continue.
There exists precedent in CSS to defer to the UA with a suggestion to use UAX-29 which is I believe what Intl.Segmenter uses. I think ::nth-letter() and ::nth-word() should follow suit.
Yes.
Words can cross element boundaries, which would suggest that ::nth-word() would be non-tree-abiding. Unfortunately, that forgoes a powerful capability: using counters, which when coupled with the parse() function (#12488) would give authors a lot of flexibility in determining how per-word effects progress.
I agree that in general this needs to be solved, because a typical use-case here is having <span>/<b>/<i>/… elements nested inside the element whose words you want to count: more than often, you just want to count the words of the flattened text across all children.
If regular counters cannot be used (why? Is it the inheritance?), what if we had a separate word counter, modeled after counter?
This would allow counting using an <An+B> pattern. For example to gradually fade out “the last 3 words” you could use ::nth-last-word(-n+3)(assuming we also do a ::nth-last-X in addition to ::nth-X). It would also allow nesting of word counters.
The value of the counter would be retrieved using a new counter() function that immediately returns a number, e.g. counter-index() — which would also immediately tackle requests like #1026
(Or parse(), of course … but that is more to type)
Or maybe there's a property that could be set to link indices together from one block to the next, and let blocks participate in indexing as atomic units
My fear here is that authors would end up writing the following CSS a lot just to make it work:
If regular counters cannot be used (why? Is it the inheritance?)
That's one issue, yes. Consider:
<p>I made a tur<b>duck</b>en for the Big Game.</p>
p {
counter-reset: wordcount;
}
p::nth-word(n) {
counter-increment: wordcount;
}
The word "turducken" generates three boxes. What values do these boxes get for wordcount? What if there's a rule that uses, or increments, wordcount on <b>?
what if we had a separate word counter, modeled after counter?
Yeah, this might be the way to go. I think having it count words rather than boxes is enough of a difference that it's reasonable to have a new notion of "counter." Or maybe there's a keyword we can add to counter-reset to designate a given counter as a word counter?
The CSS Working Group just discussed [css-pseudo-4] `::nth-letter` pseudo-element.
The full IRC log of that discussion
<ydaniv> kbabbitt: would like to talk about ::nth-letter and -word
<ydaniv> ... in April F2F I presented text-based animations for staggering effects on text
<ydaniv> ... got some feedback, during the discussion, couple of ppl raised idea on using these pseudos
<ydaniv> ... was worried about perf issue of this
<ydaniv> ... I did a prototype of this, seems not worse than what we have today
<TabAtkins> q+
<JakeA> q+
<ydaniv> ... also there's Intl. issues on what is letter/word
<ydaniv> ... there's also precendence for first letter etc.
<ydaniv> ... would like to reuse this solution here
<ydaniv> ... another issue is around words that cross element boundries
<ydaniv> ... has been problematic in the past, but could use arbitrary order
<ChrisL> q+
<emilio> q+
<ydaniv> ... wondering if something could be defined
<florian> q+
<astearns> ack TabAtkins
<ydaniv> TabAtkins: 2 things, first is seems like all use-cases is fancy headings, which doesn't hurt to just add elements to span letters, but then already adding a cost
<ydaniv> ... I would be warry on body text
<ydaniv> ... 2nd is about the counter part, would defer that
<ydaniv> kbabbitt: grabbing things in spans like authors do today, from perf perspective you need to already wrap and do matching
<ydaniv> TabAtkins: have different idea on whether this is words and letters
<kizu> There are plenty of a11y concerns with wrapping letters in spans
<ydaniv> ... let's say nth-letter has a smaller set of use-cases
<ydaniv> ... words is different
<ydaniv> ... different set of use-cases
<ydaniv> kbabbitt: a11y tools around it is also broken and need to fix it
<astearns> ack JakeA
<ydaniv> q+
<kizu> Translation would always not work as well with spanned letters
<TabAtkins> TabAtkins: a11y tools treat any markup as defining word boundaries, essentially
<ydaniv> JakeA: when you say the word thing isn't tree abiding, it works on highlighting
<ydaniv> kbabbitt: works on highlights...
<ydaniv> JakeA: what happens if you did display property on something like highlight
<ydaniv> fantasai: with highlight you can't change font-size
<ydaniv> JakeA: so those limitations would be same here?
<fantasai> ::first-letter and ::first-line can affect layout
<kizu> q+
<ydaniv> kbabbitt: you may be right
<ydaniv> JakeA: so you can't do a border on it?
<ydaniv> fantasai: you can't do a border on first-letter/line, but can affect layout
<ydaniv> florian: these are weird also because they affect layout but don't depend on it
<astearns> ack fantasai
<ydaniv> fantasai: use-case you presented in issue, you want to reveal using opacity over a timeline, how would you control timing?
<ydaniv> kbabbitt: so some sort counter mechanism, can't use counters, but some sort of notion we need to flesh out
<ydaniv> fantasai: if a major use-case is to reveal words, you need JS to drive the styling changes, you might as well just use JS for the whole things
<ydaniv> kbabbitt: you also have to be able to transition it
<ydaniv> fantasai: let's say you want to reveal words over 6 seconds, and after 6 seconds it's opacity 1
<ydaniv> ... everything starts 0, with first word starting to transition, and then going word by word
<ydaniv> ... you only need 6 highlights
<ydaniv> ... and cycle through them, but you need JS to drive it
<ydaniv> ... but the highlight API solves that
<ydaniv> bramus: what if you want to affect font size?
<emilio> q+
<ydaniv> fantasai: then you need JS
<ydaniv> ... one my concerns with nth-letter, it's easy to define, but for -word, what is it for Chinese for example?
<ydaniv> florian: we have an answer, but is it the good one?
<ydaniv> TabAtkins: you're going to use the Intl.Segmentor API
<ydaniv> ... my point is regardless ppl will use JS
<ydaniv> florian: if we have an answer, we don't care, as long as we can chunk text
<ydaniv> ... depends on context and use-case
<ydaniv> ... e.g. in Japanese you probably want phrase by phrase
<ydaniv> ... is it what you wanted? depends
<lea> `::nth-segment(<An+B> of [letter | syllable | word | line])`
<ydaniv> ... so Intl.Segmantor is reasonable API, but here are bunch of use-cases
<TabAtkins> Intl.Segmentor exposes "grapheme", "word", or "sentence"
<keithamus> qq+
<ydaniv> ... so at letter level there's also ambiguity, probably doable, not too scared of definig, but nth-word...
<jfkthame> florian++
<keithamus> q-
<ydaniv> ... you probably want a different level of chunking things
<TabAtkins> I would say that we should probably expose all the Intl.Segmentor types, and rely on that API to expose more chunking as necessary
<ydaniv> fantasai: if you're using it to style specific words to be different in your document it gets a little bit fragile
<ydaniv> ... with highlight you can tweak it
<ydaniv> ... a JS can use the unicode stuff and build on that, add heuristics, etc.
<TabAtkins> q+
<ydaniv> ... but can't do that in our impl. here
<ydaniv> ... a solution that relies on JS to later work on that...
<TabAtkins> We *could* make this work in a way that *allows* JS to custom-segmentize the text, while still exposing the default Intl.Segmenter types
<schenney> q?
<TabAtkins> can / arguably should
<ydaniv> ... if it's baked into the UA it becomes a blackbox
<astearns> ack florian
<florian> q-
<JohnJansen> q+
<astearns> ack ChrisL
<ydaniv> ChrisL: looking at the first-letter pseudo, has punctuation association
<ydaniv> ... but also says if it was second letter, trying to expand...
<ydaniv> kbabbitt: I can see it going both ways
<astearns> ack emilio
<ydaniv> emilio: when we talk about this a few months ago, that doesn't work with transtiions well, seems fixable
<ydaniv> ... transition a highlight doesn't seem to different
<fantasai> i/emilio: when we talk/ChrisL: For ::first-letter there's existing typographic traditions that we are recreating in CSS. But for :nth-letter() it is not so clear.
<ChrisL> we have existing typographic conventions to draw on fir first letter; we don't for 2nd or subsequent letters
<ydaniv> emilio: I was thinking about highlight per word, but not per word that transitions
<TabAtkins> I think, leaning on the `grapheme` segmenting of the Intl API, we'd just be totally disconnected from the ::first-letter pseudo.
<fantasai> scribe+
<TabAtkins> In the string `"Hi"`, the grapheme segmenter returns four segments, one character each
<bramus> q+
<ydaniv> ... you just start transition on one word, when transition is over you continue to the next
<fantasai> emilio: with highlight, you transition color: transparent to color: black. When the transition is done, you move the highlight to the next word.
<ydaniv> ... applies to letters as well
<TabAtkins> ::nth-segment(an+b of [graphemes | words | sentences | custom] [lang <lang-tag>?])
<ydaniv> kbabbitt: could be that way, if you're driving this through JS, may not be [missed]
<TabAtkins> (lang defaults to the element language if not specified in the selector)
<ydaniv> emilio: you're only doing extra work in the engine if there's an nth-letter to resolve
<ydaniv> ... that's how I understood fantasai's issue with it
<ydaniv> ... maybe we need something for that?
<fantasai> emilio: And transitions on highlights seems like something we should add anyway
<astearns> q?
<astearns> zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<ydaniv> ... other thing that scares me about -letter/-word is what happens is first line when you have nested elements
<ydaniv> ... brings a lot of other questions
<ydaniv> kbabbitt: first-line I agree
<ydaniv> emilio: nth-word also, you need to define how to combine the style of the [missed]
<TabAtkins> I suggest we reuse the ::highlight conflict rules
<ydaniv> ... don't know if first-letter with nested elements etc
<ydaniv> ... you still need to define how those element stack
<TabAtkins> only one ::highlight applies to a given character, whatever's deepest
<ydaniv> ... the spec isn't super interoperable
<ydaniv> ... how do you reconcile those?
<ydaniv> astearns: I'd like to continue with q instead of diving into details
<astearns> ack ydaniv
<fantasai> ydaniv: If talking specifically about animations, then I also gave a few concerns in the issue last F2F
<fantasai> ... If you want to stagger animations, then you need not the order of siblings or letters or words, but order of the effects
<TabAtkins> I don't know that highlights being limited to painting effects actually matters much here, does it? You still can't, like, draw it multiple times; you have to know the full set of highlights that might apply so you can choose the innermost one and only paint once.
<fantasai> ... which is something we have half-baked and not ready, experimental, in web animations 2
<fantasai> ... which is group effect and sequences
<TabAtkins> same here - if you segment in multiple ways based on multiple wrapper, the innermost one would win for a given character
<fantasai> ... We want to figure that out if we want proper staggering
<fantasai> ydaniv: Another major use case of text is transforms, and we haven't touched that at all.
<fantasai> ... If the use case we want to tackle is animating text, we need to tackle transforms on letters and words
<fantasai> kbabbitt: Agree. I think we wanted to define transform box on sub-elements
<fantasai> kbabbitt: here I'm focused on narrow problem of addressing character or word in CSS, then can compose with staggered animatiosn and transform bo.
<astearns> ack kizu
<ydaniv> kizu: a few things, if we wrap things in spans, every translations won't catch it if wrap text
<ydaniv> ... if it's a letter, should it be gliph? other? what about ligatures?
<ydaniv> ... in this case I don't think we want same separation
<ydaniv> ... a lot to think about
<ydaniv> ... I imagine authors would want to hack around styling specific letters
<astearns> can highlights affect text rendering the way required by the opacity transitions?
<ydaniv> ... ppl will probably want a specific letter-styling
<ydaniv> ... if we're not doing layout, or if background images are included?
<ydaniv> ... there could be privacy issues
<astearns> ack TabAtkins
<fantasai> ... people already us ::first-line for exfiltration by measuring text
<ydaniv> TabAtkins: responding to florian, wanting some general Segmentor thing can be done
<ydaniv> ... take a list of ranges/segments and wrap it, that is solvable
<ydaniv> ... if the segmentor allows more things in the future we could allow that as well
<bramus> q-
<astearns> ack fantasai
<ydaniv> fantasai: other set of concerns, the goal is to solve animations, but if you have it ppl will use it for other things
<ydaniv> ... it will get messed up on translations etc.
<JohnJansen> q-
<ydaniv> ... as soon as the segmentor gets augmented you get a different word, and it becomes too much dependency between content and style
<ydaniv> ... dependency on order in a way ppl aren't currently concerned
<JohnJansen> I was on the queue but Tab beat me to it. I don't think those additional use-cases are unsolvable in this approach, and are defined by unicode already, so they can be discussed to enable those specific scenarios without blocking the more prevalent use case of "word" across all languages: https://www.unicode.org/versions/Unicode15.0.0/ch06.pdf
<ydaniv> ... doesn't sound like fun if we do it on every word and stuff change, and you have to count everything
<ydaniv> ... also need to think of dynamic content
<ydaniv> ... in general, would be better on the other missing APIs we need, fix those, and make it easier to build up the JS libraries we need for these
<ydaniv> ... but don't think that nth-word is worth pursuing ATM
<bramus> For reference, things authors are building (using JS) today: https://github.com//issues/13689#issuecomment-4101334716
<ydaniv> ... it's quite different than allowing author to specify a specific word/letter
<ydaniv> astearns: moving back to the issue
Activity
tomhodgins commentedon Oct 12, 2018
I've published a demo earlier this week for
:nth-letter()and:nth-word()pseudos, including a JavaScript implementation that can be tested/used from CSS: https://codepen.io/tomhodgins/pen/YJZyPrtabatkins commentedon Oct 12, 2018
All of these examples (and I expect, nearly all reasonable examples you could dredge up beyond these) are fancy headings; any usage of ::nth-letter in body text would almost certainly be... dodgy at best.
::first-letterhas its classic use-case of drop-caps (which we're blessing with specialized properties!), but even with that, there's a very good chance we wouldn't consider it worthwhile if someone tried to introduce it today!One-off effects on top-level headings in a document still seem to be well-handled by just putting some
spans around the letters you want to individually style. Letting people just do that, rather than trying to deal with all the complexities and weirdness of ::nth-letter, is probably still the way to go.(For example, I don't think any of those examples work if a "real" element splits the character, like
<span>fo</span><span>[COMBINING ACUTE ACCENT]o</span>. Looks like three letters, fóo, and any reasonable definition of ::nth-letter(2) would have it targeting the entire ó letter, but the pseudo-element has to be split into two separate boxes due to the existing tree structure.)upsuper commentedon Oct 13, 2018
Yeah...
::first-letterreally taught us that it isn't something worth doing :) It should have been done in theinitial-letterway...fantasai commentedon Oct 13, 2018
@upsuper
initial-lettersis a layout property that happens to apply to:first-letter, it doesn't magically select the first letter the waytext-combine-upright: digitsmagically selects numbers ...@tabatkins I don't think any reasonably marked up document would put an element boundary there, so it's OK for that to be handled as a simplified error of some kind. I think the basic point is that pseudo-elements of this type make implementers very unhappy* so a moderately convincing use case with an easy workaround is likely not enough motivation.
:first-letter), and other i18n inquiries.adactio commentedon Oct 13, 2018
@tabatkins said:
Like I said, I knew it would be difficult to implement. Still, I'm surprised that difficulty of implementation would win out over the accessibility problems of wrapping individual letters in a bunch of
spans.Dredge up.
Interesting.
tabatkins commentedon Oct 15, 2018
The hierarchy of concerns tells us that certain players in the space matter more than others; user-a11y is generally more important than implementation difficulty. It is not, however, absolute -
::nth-letter()has significant implementation difficulties, over and above even that of::first-letterwhich is already troublesome, and has less-common use-cases. All of these combine to at least make::nth-letter()a rather low priority project, versus many other things we could add to CSS.Solving the a11y issues can be done more simply than adding
::nth-letter(), such as having something in HTML or CSS that says "this element is not a semantic separation of its contained text from the surrounding text", which could be applied to thespans. This would also solve slightly wider problems than::nth-letter(), as anything that splits up a word for any reason could also use it, rather than the solution being limited solely to the use-case of applying different CSS to letter-based groupings.? I'm confused, it looks like you might be reading some deep meaning into my wording?
shshaw commentedon Dec 4, 2019
Author of Splitting.js here. The difficulty of implementation is an issue I'm sure, but
::nth-letterwould allow for so many great possibilities especially if combined with aletter-index()counter andletter-count()total, akin to this proposal to get the index of children.Bonus points if
::nth-wordcould be implemented as well.You can see some of the really creative ways folks have utilized Splitting for text, and it's not isolated to decorative headings. More use cases can be found in GSAP with its SplitText plugin.
In the interim, I would love some proper accessibility support for splitting up text with
<span>s, as mentioned. I've tried several approaches in Splitting but can't find a right setup for keeping the text properly available to screenreaders. Perhapsaria-role="inline"that would treat an element as if it were not distinct from its siblings.AmeliaBR commentedon Dec 4, 2019
Worth mentioning that we've had lots of discussion in SVG about how to handle features that assign properties to individual characters (w3c/svgwg#631). However, the conclusion we made there was to stict to predictable parsing (codepoints) over typographical elegance — an approach that isn't at all consistent with CSS
::first-letter.faceless2 commentedon Dec 4, 2019
See also w3c/svgwg#537, and in particular the linked-to examples showing how complicated this is for some scripts, eg https://w3c.github.io/i18n-tests/quick-tests/svg-counting/svg-counting-001.
Crissov commentedon Dec 5, 2019
Perhaps there should be a more extensive snapshot profile, i. e. as a separate specification, that also included properties or values, but as a selector,
::nth-letter()– and::last-letterand::nth-last-letter()– would fit in very well with:has(). That would only address performance concerns, not solve any internationalization issues at all, of course.SelenIT commentedon Dec 5, 2019
Snapshot profile is dead (luckily), it's just the draft hasn't been updated accordingly yet (I'm going to make a relevant PR this weekend). IMO, there is already no problem with accessing any character of the text (as well as with selecting DOM elements basing on their contents) from JS APIs. It's styling these things that CSS developers have demanded for years but can't do yet.
3 remaining items
severdia commentedon Apr 24, 2021
Here's a use case for
::nth-letter()(or at least second letter). When trying to reproduce historical texts like Shakespeare, initial letter doesn't work for several reasons:<br />doesn't work for semantic reasons)If there's reason enough to support dropcaps, there should be reason enough to support them correctly, not superficially.
faceless2 commentedon Apr 25, 2021
You could do this with
initial-letterto enclose the two "V" characters in a span - (or making it a "W" and just using an appropriate font...) So I'm not sure this is a case fornth-letterHowever, here's an idea. How about we introduce control over which letters are considered part of the first-letter by using Unicode joiners? We could we specify that the
first-letterpseudo-element must not break at a ZWJ and must break at ZWNJ.That would allow us to support this example with
V‍VHen to the Sessions..., and also cover quite a few of the edge cases that seem to be coming up with punctuation and first-letter, without any additional CSS properties. I suspect this would be pretty easy to implement too.severdia commentedon Apr 25, 2021
It's possible to ask font designers to include these kinds of examples as single characters, but not likely to happen.
I tried
initial-letterwith a<span>and it doesn't work with two letters, nor does it work on inline elements (only block elements, which breaks the dropcap formatting). Here's a Codepen: https://codepen.io/severdia/pen/BapMMzxI think it's an interesting idea to control it with a Unicode word joiner. That should work for all the use cases I can think of. Thanks!
johannesmutter commentedon Jun 15, 2022
A common use case for nth-letter/ nth-word / etc. are code or more general text editors and annotations for grammar, syntax, spelling, entities (NLP), etc. Which often require overlapping markup (see standoff properties / annotations for more).
In above use cases styling the characters is insufficient: You will also want to register event listener, arbitrary data, … on it. So I’m not convinced an extension of the CSS spec would be a good idea.
Except for using canvas/ CSS Houdini, I currently don’t see a good alternative to using a million span tags that wrap each character. To keep the markup readable I'm storing the plain text string in a data attribute of e.g. a paragraph tag. (however markup readability shouldn’t be a concern if you can inspect a readable JSON of the content)
kbabbitt commentedon Jul 27, 2026
While exploring staggered text fade-in animations, I built a rough prototype of
::nth-word()in Blink and found the performance promising compared to wrapping each word in a<span>. I'd like to explore the details a bit more.Internationalization
There are existing solutions for determining what constitutes a "letter" (i.e. grapheme cluster) or "word," and I wouldn't want this feature to invent a worse wheel. There exists precedent in CSS to defer to the UA with a suggestion to use UAX-29 which is I believe what
Intl.Segmenteruses. I think::nth-letter()and::nth-word()should follow suit.Counting words
Words can cross element boundaries, which would suggest that
::nth-word()would be non-tree-abiding. Unfortunately, that forgoes a powerful capability: using counters, which when coupled with theparse()function (#12488) would give authors a lot of flexibility in determining how per-word effects progress. One might imagine a staggered text fade-in with specialized handling for various types of descendant blocks:I would love if we could find a coherent way to make that work.
Barring that, we could imagine
word-index()andletter-index()functions, akin tosibling-index(). Those would involve more baked-in indexing mechanics, including when indices reset. It would make it harder for authors to continue an effect like the above from one sibling paragraph to the next - they would need to keep a running word count to offset the delays in each paragraph. Or maybe there's a property that could be set to link indices together from one block to the next, and let blocks participate in indexing as atomic units, something like:Only has an effect on block-level elements. Word indexing always continues through inline-level elements.
restart(initial value): the first word gets index 1. Each subsequent word's index increases by 1 until a descendant block is entered.continue: the first word gets index n+1, where n is the index of the last word prior to the block in in-order traversal.block: the block itself, and all words within it, are treated as if they are at index n+1, where n is defined as forcontinue.bramus commentedon Jul 27, 2026
Yes.
I agree that in general this needs to be solved, because a typical use-case here is having
<span>/<b>/<i>/… elements nested inside the element whose words you want to count: more than often, you just want to count the words of the flattened text across all children.If regular counters cannot be used (why? Is it the inheritance?), what if we had a separate word counter, modeled after
counter?This would allow counting using an
<An+B>pattern. For example to gradually fade out “the last 3 words” you could use::nth-last-word(-n+3)(assuming we also do a::nth-last-Xin addition to::nth-X). It would also allow nesting of word counters.The value of the counter would be retrieved using a new
counter()function that immediately returns a number, e.g.counter-index()— which would also immediately tackle requests like #1026(Or
parse(), of course … but that is more to type)My fear here is that authors would end up writing the following CSS a lot just to make it work:
I’m also not sure how this would play nice with nesting or to counting
<An+B>patterns.kbabbitt commentedon Jul 28, 2026
That's one issue, yes. Consider:
The word "turducken" generates three boxes. What values do these boxes get for
wordcount? What if there's a rule that uses, or increments,wordcounton<b>?kbabbitt commentedon Jul 30, 2026
Yeah, this might be the way to go. I think having it count words rather than boxes is enough of a difference that it's reasonable to have a new notion of "counter." Or maybe there's a keyword we can add to
counter-resetto designate a given counter as a word counter?bramus commentedon Aug 4, 2026
For reference, GSAP demos that would benefit from this: #13689 (comment) (the split text into characters section)
css-meeting-bot commentedon Aug 4, 2026
The CSS Working Group just discussed
[css-pseudo-4] `::nth-letter` pseudo-element.The full IRC log of that discussion
<ydaniv> kbabbitt: would like to talk about ::nth-letter and -word<ydaniv> ... in April F2F I presented text-based animations for staggering effects on text
<ydaniv> ... got some feedback, during the discussion, couple of ppl raised idea on using these pseudos
<ydaniv> ... was worried about perf issue of this
<ydaniv> ... I did a prototype of this, seems not worse than what we have today
<TabAtkins> q+
<JakeA> q+
<ydaniv> ... also there's Intl. issues on what is letter/word
<ydaniv> ... there's also precendence for first letter etc.
<ydaniv> ... would like to reuse this solution here
<ydaniv> ... another issue is around words that cross element boundries
<ydaniv> ... has been problematic in the past, but could use arbitrary order
<ChrisL> q+
<emilio> q+
<ydaniv> ... wondering if something could be defined
<florian> q+
<astearns> ack TabAtkins
<ydaniv> TabAtkins: 2 things, first is seems like all use-cases is fancy headings, which doesn't hurt to just add elements to span letters, but then already adding a cost
<ydaniv> ... I would be warry on body text
<ydaniv> ... 2nd is about the counter part, would defer that
<ydaniv> kbabbitt: grabbing things in spans like authors do today, from perf perspective you need to already wrap and do matching
<ydaniv> TabAtkins: have different idea on whether this is words and letters
<kizu> There are plenty of a11y concerns with wrapping letters in spans
<ydaniv> ... let's say nth-letter has a smaller set of use-cases
<ydaniv> ... words is different
<ydaniv> ... different set of use-cases
<ydaniv> kbabbitt: a11y tools around it is also broken and need to fix it
<astearns> ack JakeA
<ydaniv> q+
<kizu> Translation would always not work as well with spanned letters
<TabAtkins> TabAtkins: a11y tools treat any markup as defining word boundaries, essentially
<ydaniv> JakeA: when you say the word thing isn't tree abiding, it works on highlighting
<ydaniv> kbabbitt: works on highlights...
<ydaniv> JakeA: what happens if you did display property on something like highlight
<ydaniv> fantasai: with highlight you can't change font-size
<ydaniv> JakeA: so those limitations would be same here?
<fantasai> ::first-letter and ::first-line can affect layout
<kizu> q+
<ydaniv> kbabbitt: you may be right
<ydaniv> JakeA: so you can't do a border on it?
<ydaniv> fantasai: you can't do a border on first-letter/line, but can affect layout
<ydaniv> florian: these are weird also because they affect layout but don't depend on it
<astearns> ack fantasai
<ydaniv> fantasai: use-case you presented in issue, you want to reveal using opacity over a timeline, how would you control timing?
<ydaniv> kbabbitt: so some sort counter mechanism, can't use counters, but some sort of notion we need to flesh out
<ydaniv> fantasai: if a major use-case is to reveal words, you need JS to drive the styling changes, you might as well just use JS for the whole things
<ydaniv> kbabbitt: you also have to be able to transition it
<ydaniv> fantasai: let's say you want to reveal words over 6 seconds, and after 6 seconds it's opacity 1
<ydaniv> ... everything starts 0, with first word starting to transition, and then going word by word
<ydaniv> ... you only need 6 highlights
<ydaniv> ... and cycle through them, but you need JS to drive it
<ydaniv> ... but the highlight API solves that
<ydaniv> bramus: what if you want to affect font size?
<emilio> q+
<ydaniv> fantasai: then you need JS
<ydaniv> ... one my concerns with nth-letter, it's easy to define, but for -word, what is it for Chinese for example?
<ydaniv> florian: we have an answer, but is it the good one?
<ydaniv> TabAtkins: you're going to use the Intl.Segmentor API
<ydaniv> ... my point is regardless ppl will use JS
<ydaniv> florian: if we have an answer, we don't care, as long as we can chunk text
<ydaniv> ... depends on context and use-case
<ydaniv> ... e.g. in Japanese you probably want phrase by phrase
<ydaniv> ... is it what you wanted? depends
<lea> `::nth-segment(<An+B> of [letter | syllable | word | line])`
<ydaniv> ... so Intl.Segmantor is reasonable API, but here are bunch of use-cases
<TabAtkins> Intl.Segmentor exposes "grapheme", "word", or "sentence"
<keithamus> qq+
<ydaniv> ... so at letter level there's also ambiguity, probably doable, not too scared of definig, but nth-word...
<jfkthame> florian++
<keithamus> q-
<ydaniv> ... you probably want a different level of chunking things
<TabAtkins> I would say that we should probably expose all the Intl.Segmentor types, and rely on that API to expose more chunking as necessary
<ydaniv> fantasai: if you're using it to style specific words to be different in your document it gets a little bit fragile
<ydaniv> ... with highlight you can tweak it
<ydaniv> ... a JS can use the unicode stuff and build on that, add heuristics, etc.
<TabAtkins> q+
<ydaniv> ... but can't do that in our impl. here
<ydaniv> ... a solution that relies on JS to later work on that...
<TabAtkins> We *could* make this work in a way that *allows* JS to custom-segmentize the text, while still exposing the default Intl.Segmenter types
<schenney> q?
<TabAtkins> can / arguably should
<ydaniv> ... if it's baked into the UA it becomes a blackbox
<astearns> ack florian
<florian> q-
<JohnJansen> q+
<astearns> ack ChrisL
<ydaniv> ChrisL: looking at the first-letter pseudo, has punctuation association
<ydaniv> ... but also says if it was second letter, trying to expand...
<ydaniv> kbabbitt: I can see it going both ways
<astearns> ack emilio
<ydaniv> emilio: when we talk about this a few months ago, that doesn't work with transtiions well, seems fixable
<ydaniv> ... transition a highlight doesn't seem to different
<fantasai> i/emilio: when we talk/ChrisL: For ::first-letter there's existing typographic traditions that we are recreating in CSS. But for :nth-letter() it is not so clear.
<ChrisL> we have existing typographic conventions to draw on fir first letter; we don't for 2nd or subsequent letters
<ydaniv> emilio: I was thinking about highlight per word, but not per word that transitions
<TabAtkins> I think, leaning on the `grapheme` segmenting of the Intl API, we'd just be totally disconnected from the ::first-letter pseudo.
<fantasai> scribe+
<TabAtkins> In the string `"Hi"`, the grapheme segmenter returns four segments, one character each
<bramus> q+
<ydaniv> ... you just start transition on one word, when transition is over you continue to the next
<fantasai> emilio: with highlight, you transition color: transparent to color: black. When the transition is done, you move the highlight to the next word.
<ydaniv> ... applies to letters as well
<TabAtkins> ::nth-segment(an+b of [graphemes | words | sentences | custom] [lang <lang-tag>?])
<ydaniv> kbabbitt: could be that way, if you're driving this through JS, may not be [missed]
<TabAtkins> (lang defaults to the element language if not specified in the selector)
<ydaniv> emilio: you're only doing extra work in the engine if there's an nth-letter to resolve
<ydaniv> ... that's how I understood fantasai's issue with it
<ydaniv> ... maybe we need something for that?
<fantasai> emilio: And transitions on highlights seems like something we should add anyway
<astearns> q?
<astearns> zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<ydaniv> ... other thing that scares me about -letter/-word is what happens is first line when you have nested elements
<ydaniv> ... brings a lot of other questions
<ydaniv> kbabbitt: first-line I agree
<ydaniv> emilio: nth-word also, you need to define how to combine the style of the [missed]
<TabAtkins> I suggest we reuse the ::highlight conflict rules
<ydaniv> ... don't know if first-letter with nested elements etc
<ydaniv> ... you still need to define how those element stack
<TabAtkins> only one ::highlight applies to a given character, whatever's deepest
<ydaniv> ... the spec isn't super interoperable
<ydaniv> ... how do you reconcile those?
<ydaniv> astearns: I'd like to continue with q instead of diving into details
<astearns> ack ydaniv
<fantasai> ydaniv: If talking specifically about animations, then I also gave a few concerns in the issue last F2F
<fantasai> ... If you want to stagger animations, then you need not the order of siblings or letters or words, but order of the effects
<TabAtkins> I don't know that highlights being limited to painting effects actually matters much here, does it? You still can't, like, draw it multiple times; you have to know the full set of highlights that might apply so you can choose the innermost one and only paint once.
<fantasai> ... which is something we have half-baked and not ready, experimental, in web animations 2
<fantasai> ... which is group effect and sequences
<TabAtkins> same here - if you segment in multiple ways based on multiple wrapper, the innermost one would win for a given character
<fantasai> ... We want to figure that out if we want proper staggering
<fantasai> ydaniv: Another major use case of text is transforms, and we haven't touched that at all.
<fantasai> ... If the use case we want to tackle is animating text, we need to tackle transforms on letters and words
<fantasai> kbabbitt: Agree. I think we wanted to define transform box on sub-elements
<fantasai> kbabbitt: here I'm focused on narrow problem of addressing character or word in CSS, then can compose with staggered animatiosn and transform bo.
<astearns> ack kizu
<ydaniv> kizu: a few things, if we wrap things in spans, every translations won't catch it if wrap text
<ydaniv> ... if it's a letter, should it be gliph? other? what about ligatures?
<ydaniv> ... in this case I don't think we want same separation
<ydaniv> ... a lot to think about
<ydaniv> ... I imagine authors would want to hack around styling specific letters
<astearns> can highlights affect text rendering the way required by the opacity transitions?
<ydaniv> ... ppl will probably want a specific letter-styling
<ydaniv> ... if we're not doing layout, or if background images are included?
<ydaniv> ... there could be privacy issues
<astearns> ack TabAtkins
<fantasai> ... people already us ::first-line for exfiltration by measuring text
<ydaniv> TabAtkins: responding to florian, wanting some general Segmentor thing can be done
<ydaniv> ... take a list of ranges/segments and wrap it, that is solvable
<ydaniv> ... if the segmentor allows more things in the future we could allow that as well
<bramus> q-
<astearns> ack fantasai
<ydaniv> fantasai: other set of concerns, the goal is to solve animations, but if you have it ppl will use it for other things
<ydaniv> ... it will get messed up on translations etc.
<JohnJansen> q-
<ydaniv> ... as soon as the segmentor gets augmented you get a different word, and it becomes too much dependency between content and style
<ydaniv> ... dependency on order in a way ppl aren't currently concerned
<JohnJansen> I was on the queue but Tab beat me to it. I don't think those additional use-cases are unsolvable in this approach, and are defined by unicode already, so they can be discussed to enable those specific scenarios without blocking the more prevalent use case of "word" across all languages: https://www.unicode.org/versions/Unicode15.0.0/ch06.pdf
<ydaniv> ... doesn't sound like fun if we do it on every word and stuff change, and you have to count everything
<ydaniv> ... also need to think of dynamic content
<ydaniv> ... in general, would be better on the other missing APIs we need, fix those, and make it easier to build up the JS libraries we need for these
<ydaniv> ... but don't think that nth-word is worth pursuing ATM
<bramus> For reference, things authors are building (using JS) today: https://github.com//issues/13689#issuecomment-4101334716
<ydaniv> ... it's quite different than allowing author to specify a specific word/letter
<ydaniv> astearns: moving back to the issue