A little while back, Ganesh Dahal penned a post here on CSS-Tricks responding to a tweet that asked about adding CSS box shadows on WordPress blocks and elements. There’s a lot of great stuff in there that leverages new features that shipped in WordPress 6.1 that provide controls for applying shadows to things directly in the Block Editor and Site Editor UI.
Ganesh touched briefly on button elements in that post. I want to pick that up and go deeper into approaches for styling buttons in WordPress block themes. Specifically, we’re going to crack open a fresh theme.json
file and break down various approaches to styling buttons in the schema.
Why buttons, you ask? That’s a good question, so let’s start with that.
The different types of buttons
When we’re talking about buttons in the context of the WordPress Block Editor, we have to distinguish between two different types:
- Child blocks inside of the Buttons block
- Buttons that are nested inside other block (e.g. the Post Comments Form block)
If we add both of these blocks to a template, they have the same look by default.

But the markup is very different:
<div class="wp-block-button"> <a class="wp-block-button__link wp-element-button">Button 1</a> </div> <p class="form-submit wp-block-button"> <input name="submit" type="submit" id="submit" class="wp-block-button__link wp-element-button" value="Post Comment"> </p>
As we can see, the HTML tag names are different. It’s the common classes — .wp-block-button
and .wp-element-button
— that ensure consistent styling between the two buttons.
If we were writing CSS, we would target these two classes. But as we know, WordPress block themes have a different way of managing styles, and that’s through the theme.json
file. Ganesh also covered this in great detail, and you’d do well giving his article a read.
So, how do we define button styles in theme.json
without writing actual CSS? Let’s do it together.
Creating the base styles
theme.json
is a structured set of schema written in property:value pairs. The top level properties are called “sections”, and we’re going to work with the styles
section. This is where all the styling instructions go.
We’ll focus specifically on the elements
in the styles
. This selector targets HTML elements that are shared between blocks. This is the basic shell we’re working with:
// theme.json { "version": 2, "styles": { "elements": { // etc. } } }
So what we need to do is define a button
element.
={ "version": 2, "styles": { "elements": { "button": { // etc. } } } }
That button
corresponds to HTML elements that are used to mark up button elements on the front end. These buttons contain HTML tags that could be either of our two button types: a standalone component (i.e. the Button block) or a component nested within another block (e.g. the Post Comment block).
Rather than having to style each individual block, we create shared styles. Let’s go ahead and change the default background and text color for both types of buttons in our theme. There’s a color
object in there that, in turn, supports background
and text
properties where we set the values we want:
{ "version": 2, "styles": { "elements": { "button": { "color": { "background": "#17a2b8", "text": "#ffffff" } } } } }
This changes the color of both button types:

If crack open DevTools and have a look at the CSS that WordPress generates for the buttons, we see that the .wp-element-button
class adds the styles we defined in theme.json
:
.wp-element-button { background-color: #17a2b8; color: #ffffff; }
Those are our default colors! Next, we want to give users visual feedback when they interact with the button.
Implementing interactive button styles
Since this is a site all about CSS, I’d bet many of you are already familiar with the interactive states of links and buttons. We can :hover
the mouse cursor over them, tab them into :focus
, click on them to make them :active
. Heck, there’s even a :visited
state to give users a visual indication that they’ve clicked this before.
Those are CSS pseudo-classes and we use them to target a link’s or button’s interactions.
In CSS, we might style a :hover
state like this:
a:hover { /* Styles */ }
In theme.json
, we’re going to extend our existing button declaration with these pseudo-classes.
{ "version": 2, "styles": { "elements": { "button": { "color": { "background": "#17a2b8", "text": "#ffffff" } ":hover": { "color": { "background": "#138496" } }, ":focus": { "color": { "background": "#138496" } }, ":active": { "color": { "background": "#138496" } } } } } }
Notice the “structured” nature of this. We’re basically following an outline:
- Elements
- Element
- Object
- Property
- Value
- Property
- Object
- Element
We now have a complete definition of our button’s default and interactive styles. But what if we want to style certain buttons that are nested in other blocks?
Styling buttons nested in individual blocks
Let’s imagine that we want all buttons to have our base styles, with one exception. We want the submit button of the Post Comment Form block to be blue. How would we achieve that?
This block is more complex than the Button block because it has more moving parts: the form, inputs, instructive text, and the button. In order to target the button in this block, we have to follow the same sort of JSON structure we did for the button
element, but applied to the Post Comment Form block, which is mapped to the core/post-comments-form
element:
{ "version": 2, "styles": { "elements" { "button": { // Default button styles } } "blocks": { "core/post-comments-form": { // etc. } } } }
Notice that we’re no longer working in elements
anymore. Instead, we’re working inside blocks
which is reserved for configuring actual blocks. Buttons, by contrast, are considered a global element since they can be nested in blocks, even though they are available as a standalone block too.
The JSON structure supports elements within elements. So, if there’s a button
element in the Post Comment Form block, we can target it in the core/post-comments-form
block:
{ "version": 2, "styles": { "elements" { "button": { // Default button styles } } "blocks": { "core/post-comments-form": { "elements": { "button": { "color": { "background": "#007bff" } } } } } } }
This selector means that not only are we targeting a specific block — we’re targeting a specific element that is contained in that block. Now we have a default set of button styles that are applied to all buttons in the theme, and a set of styles that apply to specific buttons that are contained in the Post Comment Form block.

The CSS generated by WordPress has a more precise selector as a result:
.wp-block-post-comments-form .wp-element-button, .wp-block-post-comments-form .wp-block-button__link { background-color: #007bff; }
And what if we want to define different interactive styles for the Post Comment Form button? It’s the same deal as the way we did it for the default styles, only those are defined inside the core/post-comments-form
block:
{ "version": 2, "styles": { "elements" { "button": { // Default button styles } } "blocks": { "core/post-comments-form": { "elements": { "button": { "color": { "background": "#007bff" } ":hover": { "color": { "background": "#138496" } }, // etc. } } } } } }
What about buttons that are not in blocks?
WordPress automagically generates and applies the right classes to output these button styles. But what if you use a “hybrid” WordPress theme that supports blocks and full-site editing, but also contains “classic” PHP templates? Or what if you made a custom block, or even have a legacy shortcode, that contains buttons? None of these are handled by the WordPress Style Engine!
No worries. In all of those cases, you would add the .wp-element-button
class in the template, block, or shortcode markup. The styles generated by WordPress will then be applied in those instances.
And there may be some situations where you have no control over the markup. For example, some block plugin might be a little too opinionated and liberally apply its own styling. That’s where you can typically go to the “Advanced” option in the block’s settings panel and apply the class there:

Wrapping up
While writing “CSS” in theme.json
might feel awkward at first, I’ve found that it becomes second nature. Like CSS, there are a limited number of properties that you can apply either broadly or very narrowly using the right selectors.
And let’s not forget the three main advantages of using theme.json
:
- The styles are applied to buttons in both the front-end view and the block editor.
- Your CSS will be compatible with future WordPress updates.
- The generated styles work with block themes and classic themes alike — there’s no need to duplicate anything in a separate stylesheet.
If you have used theme.json
styles in your projects, please share your experiences and thoughts. I look forward to reading any comments and feedback!
Styling Buttons in WordPress Block Themes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Styling Comment Threads
Comment threads are one of those things that look really simple when executed right. When designing them yourself, you may find that they are rather deceptively simple. There is a lot that goes into designing nice and usable comment threads, and in this article, I will try my best to walk you through the steps to building a comment thread, that is great to look at, and a joy to use.
What makes a good comment thread?
Before diving into designing and writing code, let’s break down what actually makes a good comment thread. A good comment thread will have the following characteristics:
As you can see, that’s quite a lot to consider! There are also some nice-to-haves that we won’t cover in this article, but are certainly good enhancements:
The above features would require at least a bit of JavaScript to pull off. Moreover, depending on the tech stack, these features could just as likely be implemented server side, especially keeping track of the number of votes and flag status of comments. That is why we will focus only on styling the comment threads in this article. With that out of the way, let’s knock out the first set of points and design our comment thread.
A basic comment thread
A comment by itself has a pretty simple structure. Here’s a skeleton of a single comment:
So far, so good. Notice how the replies have an extra margin to the left. This is meant to satisfy the visual hierarchy (point #3 above). The markup for the above structure could look something like this:
There is nothing to explain here — just a bunch of containers with some basic elements. So instead, let’s look at a real-life example, with a comment that also has a reply.
Looks pretty nice, right? The above example satisfies the first three points, so we are already on our way. A few things to understand about the code above:
.sr-only
hides elements on all devices except on screen readers. This makes the voting buttons accessible. If you are using a framework, like Bootstrap or Halfmoon, this class comes automatically packed in.Adding links that jump to comments
Now that we have a basic comment thread going, let’s add a feature to help users quickly scroll to a comment. As mentioned above, this is especially useful when someone wants to jump to the parent comment of a reply.
In order to build it, we need to decide what these links look like. While this is entirely subjective, one particular design I really like is a clickable “border” on the left hand side of the comment, something like this:
In order to accommodate the link, we are pushing the comment body to the right and aligning it with the replies. This design also has the added benefit of reinforcing the hierarchy between the comments, because you can simply look at the number of border links to the left and determine the level of nesting of the comment you are currently reading. And of course, you can also immediately jump to any upper level by clicking on the border links.
To actually create these border links, we need to add anchor elements (
<a href="...">
) inside each of our comments, and style these anchor elements to look like the borders that can be clicked. Let’s add them to our first code example above.Here are a few things to understand about the changes made:
.comment-heading
(which contains the votes, author, and time added) has a fixedheight
of50px
. Therefore, by giving the border links the properties ofposition:absolute
,top: 50px
, andheight: calc(100% - 50px)
, we are ensuring that they will start right below the heading, and go all the way down to the end of the comment. If you are not familiar with thecalc()
function, you can read this cool guide by Chris.width
of12px
along with aborder-width
of4px
on the left and right. This means that while the visible area is only4px
wide, the actual clickable area is12px
wide. A wider surface is to help the users have an easier time actually positioning their pointers on the link and clicking it, because4px
is a little too narrow, but anything wider would look off visually.With all that, we have knocked out the first four of the points mentioned above. Let’s add more comments to the code example to see how it would look.
Allowing users to hide/show comments with a click
At this point, we have a pretty darn satisfactory comment thread. This design by itself can work for quite a lot of real life use cases. However, let’s go one step farther and add our toggle feature, i.e. hiding and showing comments with a click.
The quickest and easiest way to allow users to hide and show comments with a click is to make use of the
<details>
and<summary>
elements. To put it simply, the visibility of the entire<details>
can be toggled by clicking on the<summary>
. There is no JavaScript involved, and these tags are supported by ~96% of browsers at this moment. Once again, if you are unfamiliar with these concepts, you can learn more in yet another article from Chris.Anyway, to actually implement this, we need to make the following changes to our code:
<div>
to<details>
, i.e. all the elements with the class.comment
will now be a<details>
element..comment-heading
) inside of a<summary>
tag.Seems easy enough. Anyway, here’s our new implementation:
Here are the final things to understand about the changes made:
<details>
, and they are all given theopen
attribute, so they are visible (i.e. open) by default.<summary>
tags. The default arrow is also removed.<details>
element has theopen
attribute or not. The text itself is a simple pseudo-element created using the::after
selector. Moreover, closed comments also have a border on the bottom to show the users that there is more to see.@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {...}
selector. This is a hack that only targets Internet Explorer. You see, IE does not support<details>
, so they are always open by default there. By removing the text and resetting the cursor, IE users will see a regular comment thread, only without the ability to actually toggle the comment visibility. So, no harm, no foul.Honorable mentions
Before we end this article, let’s talk about a few more things that is worth considering when designing comment threads in real-life applications.
What if the comment thread has no comments to show?
This may be a very simple problem to solve, but it is often easy to overlook these simple things. If a comment thread does not have any comments (empty state), we need to communicate that clearly to the user. A simple paragraph containing the line “There are no comments yet.” along with a form containing a text box and submit button can go a very long way, and should be the bare minimum when dealing with empty states. If you want to go the extra mile, you can also have a nice image (that communicates the message) accompanying the form.
How to handle the form for replying to a comment?
When it comes to the form for replying to a comment, different websites have different implementations. Some use the old fashioned way of redirecting users to a new page which contains the form — a simple text box with a submit button. Others open up a form right within the comment thread itself, usually with a simple toggle. The latter paradigm obviously requires JavaScript, but it is more more popular these days. For instance, in our example above, we could have a simple form which can be toggled by clicking on the Reply button, like so:
In the above example, we added simple forms inside the comment bodies, and gave them the class
.d-none
by default, which setsdisplay: none;
and hides them from view. Thanks to the simple event listener, any button with the attributesdata-toggle="reply-form"
anddata-target="{{ comment_reply_form_id }}
can be clicked to toggle the visibility of the forms. This is a very simple example of handling the reply forms with ease.Where to place a new reply after a user is done posting it?
Let’s say a user replies to a comment using a form similar to the one shown above. Do you show it above the existing replies or below it? The answer is that it should always be shown above the other replies right after the user posts it for the first time. When a person fills out a form and submits it, they want immediate feedback to tell them that it worked. Therefore, by placing the new reply above the others, we are providing this feedback to the user without them needing to scroll down. On subsequent loads, you can of course arrange your comment replies according to whatever algorithm you see fit for your website.
Handling Markdown and code blocks
Many websites, particularly developer blogs, need to support markdown and code blocks in their comments. This is a much bigger discussion, perhaps warranting a dedicated article on this topic. However, for the sake of this article, let’s just say that there are plenty of Markdown editors out there that you can attach to a text box quite easily. Most of them work with JavaScript, so they should be fairly easy to integrate in our examples. One such plugin is markdown-it, which has a permissive MIT license. You can also look into WYSIWYG editors, which also serve a very similar purpose when it comes to comments on the web.
Spam prevention and user authentication
If you give users a form to provide their inputs, you can guarantee that you will find spam coming your way, so this is obviously an issue to address when building comment threads. A great way to reduce spam is to use services like reCAPTCHA from Google. For instance, in our example above, a reCAPTCHA box could be placed right below the Submit button in the reply forms. This would protect our website from abuse.
Another way to prevent spam is to only allow authenticated users to post comments, i.e. a user must have an account and be logged in to post a comment. Every comment would obviously be linked to an account, so this has the benefit of allowing moderators to handle users who continuously post spam or low effort content. In terms of handling it in the UI, a great way of doing it is by redirecting users to a login page when they click on the Reply or Post comment button if they are not logged in. Once they complete the authentication process, we can simply redirect them back to the comment thread and open up the form.
And we are done! We have fulfilled all five of our points, and have designed a nice-looking comment thread that is highly usable and accessible, with some cool features like jumping to comments, and togging the visibility of each comment. We also talked about forms inside comment threads, and discussed other things to consider in real-life applications. The bulk of our comment thread works using only CSS (no JavaScript), which goes to show you how far CSS has actually come.
The post Styling Comment Threads appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.
CSS-Tricks