As part of my curious UI inspection, I considered why not to take a look at well-liked web sites and see how they applied the remark part format. At first, I assumed this might be a straightforward process, but it surely wasn’t.
I spent loads of time making an attempt to know how this works and the way we are able to do it higher with fashionable CSS like :has
, dimension container queries, and magnificence queries. On this article, I’ll stroll you thru my thought course of and share my findings alongside the way in which.
Let’s dive in!
Article sequence
Desk of contents
Introduction
Right here is the format that we are going to construct. At first look, it could be easy, however there are loads of tiny particulars.

We have now a remark that may be nested into two extra ranges. I’ll name these “depths” within the article.
Contemplate the next determine.

It reveals how the depth
is modified based mostly on the extent of nesting for every remark.
Fascinated with the format
Earlier than diving into the wonderful particulars, I favor to work on the format first and ensure it really works nice. The objective of this observe is to discover the potential of contemporary CSS to resolve that downside for me.

The primary to bear in mind is the HTML markup. The construction of the feedback is an efficient match for an unordered listing <ul>
.
<ul>
<li>
<Remark />
</li>
<li>
<Remark />
</li>
</ul>
The <Remark />
acts as a placeholder for the remark part. That’s the HTML for an inventory of two feedback with none replies.
If we’ve a reply or extra to one of many feedback, then a brand new <ul>
might be appended.
<ul>
<li>
<!-- Major remark -->
<Remark />
<ul>
<!-- Remark reply -->
<li>
<Remark />
</li>
</ul>
</li>
<li>
<Remark />
</li>
</ul>
From a semantic perspective, the above sounds logical.
I known as the title “remark wrapper” to not confuse the that means of the remark part itself. Within the following part, I’ll clarify my considering on constructing the format to deal with the indentation or spacing for the remark replies.
Contemplate the next markup:
<ul>
<li>
<!-- Major remark -->
<Remark />
<ul>
<!-- Remark reply -->
<li>
<Remark />
</li>
</ul>
</li>
<li>
<Remark />
</li>
</ul>
We have now two essential feedback and one reply. From a visible perspective, it is going to seem like the next:

I favor to maintain all of the spacing and indentation stuff to the <li>
gadgets solely, as they act as a dwelling for the remark part.
Utilizing information attributes to deal with the spacing
The very first thing I considered is utilizing information attributes on the <ul>
and <li>
parts.
<ul>
<li data-nested="true">
<!-- Major remark -->
<Remark />
<ul>
<!-- Remark reply -->
<li>
<Remark />
</li>
</ul>
</li>
<li>
<Remark />
</li>
</ul>
We are able to name the <ul>
and do particular styling as wanted. Within the determine beneath, the primary reply has a spacing on the left to point that it’s visually associated to the primary remark.

In CSS, we are able to do one thing like this:
li[data-nested="true"],
li[data-nested="true] li {
padding-left: 3rem;
}
Whereas which may work, we are able to do significantly better with CSS variables and magnificence queries.
Utilizing CSS type queries
We are able to test if the CSS variable --nested: true
is added to the container and magnificence the kid gadgets based mostly on that.
Contemplate the next markup the place I added an inline CSS variable --nested: true
to the <ul>
parts.
<ul>
<li type="--nested: true;">
<!-- Major remark -->
<Remark />
<ul type="--nested: true;">
<!-- Remark reply -->
<li>
<Remark />
</li>
</ul>
</li>
<li>
<Remark />
</li>
</ul>
With type queries, I can test if the CSS variable is there and magnificence the <li>
aspect based mostly on that. That is supported in Chrome Canary solely, for now.
@container type(--nested: true) {
/* Add spacing to the 2nd degree <li> gadgets. */
li {
padding-left: 3rem;
}
}
Right here is why I favor type queries over information attributes:
- It’s simpler to know. It reads
@container
, and the plain phrases say it. If the container has the--nested: true
CSS variable, then apply the next stuff. - It’s doable to mix a number of type queries for extra management over CSS.
- If wanted, we are able to use dimension container queries together with type queries, too.
One other resolution is to make use of CSS subgrid to construct the nested feedback format. Let me be sincere with you, it is going to be extra CSS code, but it surely’s cool to discover the potential of recent CSS options.
I’ll clarify about subgrid in a short to present you an thought. Contemplate the next CSS grid:
<ul>
<li class="essential">
<!-- Major remark -->
<Remark />
<ul>
<!-- Remark reply -->
<li>
<Remark />
<ul>
<li>
<Remark />
</li>
</ul>
</li>
</ul>
</li>
</ul>
li.essential {
show: grid;
grid-template-columns: 3rem 3rem 1fr;
}
This might be added to the primary direct <li>
aspect of the <ul>
listing. That grid will seem like this:

At present, in CSS grid, it’s not doable to cross the primary grid to the kid gadgets. In our case, I need to cross the grid columns to the primary <ul>
, after which to the <li>
of that <ul>
.
Because of CSS subgrid, that is doable now. It’s accessible solely in Firefox and Safari. Chrome is on the way in which!
Contemplate the next determine:

First, we have to arrange the primary grid as follows. We have now 3 columns.
@container type(--nested: true) {
li.essential {
show: grid;
grid-template-columns: 3rem 3rem 1fr;
.remark {
grid-column: 1 / -1;
}
}
}
The .remark
part will all the time span the complete width. That’s why I added grid-column: 1 / -1
. It means: “span the remark from the primary to the final column”. That is useful to keep away from coming into the column quantity manually for every depth within the nesting. One thing like this:
/* Not good */
@container type(--nested: true) {
li.essential .remark {
grid-column: 1 / 4;
}
ul[depth="1"] .remark,
ul[depth="2"] .remark {
grid-column: 1 / 3;
}
}
Cool. The subsequent step is to place the depth=1
feedback inside the primary grid, after which add subgrid
and place the interior <li>
.
@container type(--nested: true) {
ul[depth="1"] {
grid-column: 2 / 4;
show: grid;
grid-template-columns: subgrid;
> li {
grid-column: 1 / 3;
show: grid;
grid-template-columns: subgrid;
}
}
}
Lastly, we have to place the depth=2
listing.
@container type(--nested: true) {
ul[depth="2"] {
grid-column: 2 / 3;
}
}

That resolution works, however I’m not a fan of all of that particulars. A easy padding
can repair it.
Nonetheless unsure about the way it works? See the next video which reveals me inspecting the grid within the DevTools.
Take a look at the Codepen demo.
That’s it! Now we’ve feedback with spacing that displays their relationship.
Fascinated with the connecting strains
To make it extra clear that there’s a reply to a remark, there are connecting strains between the primary remark and replies. The Fb group used a <div>
to deal with the strains. Can I do one thing completely different?
Contemplate the next determine:

At first, your thoughts would possibly attempt to trick you into: “Hey, this seems like a rectangle with a left and backside border and a border radius on the underside left nook”.
li:earlier than {
content material: "";
width: 30px;
top: 70px;
border-left: 2px stable #ef5da8;
border-bottom: 2px stable #ef5da8;
border-bottom-left-radius: 15px;
}

The second I typed top:..
within the above CSS, I noticed that this gained’t work. How would I do know the peak? It’s not doable with CSS, why? As a result of I have to make the underside fringe of the road aligned to the primary reply avatar.
Then, I considered utilizing pseudo-elements for that objective. What if that curved line will be divided into two components?

The line will be added to the primary remark, and the curved aspect is for the reply.
Nicely, what if we’ve one other reply to the primary reply? Here’s a visible that reveals how the strains will work:

In CSS, we have to use pseudo-elements to realize the road impact. Earlier than diving into the CSS to implement that, I need to spotlight that the road or the curve might be positioned in line with the complete row.

Dealing with the road added to the primary remark
That is the primary problem to deal with. We have to add a line to the first remark if it has replies. How to do this? Because of CSS :has
, we are able to test if an <li>
accommodates a <ul>
, and if sure, we apply the CSS wanted.
Contemplate the next HTML:
<ul type="--depth: 0;">
<li type="--nested: true;">
<Remark />
<ul type="--depth: 1;">
<li>
<Remark />
</li>
</ul>
</li>
<li>
<Remark />
</li>
</ul>

We have to type each <Remark />
with the next situations:
- it’s a direct little one of an
<li>
- the
<li>
has a<ul>
as a baby - the guardian is at
depth: 0
ordepth: 1
Right here is how the above translate to CSS. CSS variables + type queries + has = a strong conditional styling.
@container type(--depth: 0) or type(--depth: 1) {
li:has(ul) > .remark {
place: relative;
&:earlier than {
content material: "";
place: absolute;
left: calc(var(--size) / 2);
prime: 2rem;
backside: 0;
width: 2px;
background: #222;
}
}
}
The above is simply an instance of why I favor utilizing type queries over HTML information attributes for dealing with the depth of the remark and replies.
Subsequent, we’d like the road and curved aspect for the depth: 1
replies. This time, the <li>
will use each the :earlier than
and :after
pseudo-elements.

@container type(--depth: 1) {
li:not(:last-child) {
place: relative;
&:earlier than {
/* Line */
}
}
li {
place: relative;
&:after {
/* Curved aspect */
}
}
}
Lastly, We have to add the curved aspect to every <li>
of depth: 2
, and the road to all <li>
s besides the final one. We have to do the next logic:
- Add the curved aspect to every
<li>
indepth: 2
. - Add the road to every
<li>
besides the final one indepth: 2
.
The curved line is a rectangle with a border and radius on the underside left. Let me clarify that:

@container type(--depth: 2) {
li {
place: relative;
&:after {
content material: "";
place: absolute;
inset-inline-start: 15px;
prime: -2px;
top: 20px;
width: 28px;
border-inline-start: 2px stable #000;
border-bottom: 2px stable #000;
border-end-start-radius: 10px;
}
}
li:not(:last-child) {
&:earlier than {
/* Line */
}
}
}

Discover that I’m utilizing logical properties for the border
and border-radius
. That is helpful to make the UI dynamically flip when the doc language is RTL. I’ll come to this later within the article.
Take a look at the Codepen demo.
Disabling the strains
What if for some purpose we have to conceal connecting strains? Nicely, due to type queries, that is easy as toggling on and off a CSS variable.

By nesting all of the depth
associated type queries throughout the --lines: true
one, we are able to make sure that the limes might be proven solely when the CSS variable is about.
@container type(--lines: true) {
@container type(--depth: 0) {}
@container type(--depth: 1) {}
@container type(--depth: 1) {}
@container type(--depth: 2) {}
}
Checkout the next video:
You could be considering that the entire above is only for the primary containing format and features. Sure, that’s right! I haven’t even thought in regards to the remark part but.
Let’s speak a better look:

At first look, it is a excellent case for flexbox. We are able to have flexbox to show the avatar and remark field in the identical line.
<div class="remark">
<div class="person"></div>
<!-- As a result of an extra wrapper does not damage. -->
<div>
<div class="comment__body"></div>
<div class="comment__actions">
<a href="#">Like</a>
<a href="#">Reply</a>
</div>
</div>
</div>
Please notice that the HTML above could be very fundamental. It doesn’t mirror a production-level code, and it’s simply there to assist clarify the CSS stuff.
.remark {
--size: 2rem;
show: flex;
hole: 0.5rem;
}
.avatar {
flex: 0 0 var(--size);
width: var(--size);
top: var(--size);
border-radius: 50%;
}
That’s the essential format, however we’ve much more than that. Nevertheless, on this article, I’ll focus solely on the issues which can be distinctive and necessary to elucidate.
Subsequent are just a few issues for the remark physique part.

This half alone of the remark part might want to deal with:
- Minimal width
- Lengthy content material
- Multilingual content material (LTR vs RTL)
- Context menu
- Remark interactions
- Enhancing state
- Error state
I gained’t be capable to spotlight the entire above on this article, as I’d find yourself writing a guide.

What I’ll spotlight are some bets that I discovered fascinating to be an excellent candidate for contemporary CSS.
Altering the person avatar dimension
When a reply is nested inside a remark, the person avatar dimension will change into smaller. That is good to make it visually straightforward to tell apart between a essential remark and a reply.

Utilizing type queries is ideal for that.
.person {
flex: 0 0 var(--size);
width: var(--size);
top: var(--size);
}
.remark {
--size: 2rem;
@container type(--depth: 1) or type(--depth: 2) {
--size: 1.5rem;
}
}
Dynamic textual content alignment with dir=auto
A remark would possibly include a left-to-right (LTR) or right-to-left (RTL) language. The textual content alignment must be completely different based mostly on the content material language. Because of dir=auto
HTML attribute, we are able to let the browser try this for us.
<div class="remark">
<div class="person"></div>
<div>
<div class="comment__body">
<p dir="auto"></p>
</div>
<div class="comment__actions"></div>
</div>
</div>

CSS Logical Properties
By utilizing CSS logical properties, we are able to construct the remark part in a manner that modifications based mostly on the doc course. The identical applies to the connecting strains, too.

Take a look at the next video. Discover the way it makes switching from LTR to RTL a breeze!
The emoji-only state
When the person provides a remark that consists of emojis solely, the containing wrapper will change a bit:
This can be a excellent use-case for CSS :has
.
.remark:has(.emjois-wrapper) {
background: var(--default);
padding: var(--reset);
}

Conclusion
It’s all the time thrilling of what will be executed with fashionable CSS. Making an attempt to consider a part or a format that’s already inbuilt a brand new methods is a good way to study new stuff. I discovered loads of new issues and loved the entire course of.
Thanks for studying.