Skip to content
AgileLogics
6 min read

Pseudo-Elements

Prerequisites: Styling links and interactive states

Pseudo-elements style a part of an element, or add generated content. They use two colons.

::before and ::after

These create a child box at the start or end of an element's content. They need a content property, even if it's empty:

.required-label::after {
  content: " *";
  color: #c62828;
}

.quote::before {
  content: "“";
  font-size: 3rem;
  line-height: 0;
  vertical-align: -0.4em;
}

.card { position: relative; }
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
  opacity: 0;
  transition: opacity 200ms;
}
.card:hover::after { opacity: 1; }
  • Generated text isn't selectable, and screen readers may or may not read it — never put important content in content. (You can give generated content alternative text, or an empty one for decoration: content: "★" / ""; — supported in all current browsers.)
  • Empty-content pseudo-elements are great for decoration: overlays, icons, underlines, and shadows you can animate cheaply.
  • ::before/::after don't work on void elements like img and input.
<style>
  .price::before { content: "£"; }
  .price::after { content: " " attr(data-unit); color: gray; }
</style>
<p class="price" data-unit="per loaf">4.20</p>
<script>
  const p = document.querySelector(".price");
  console.log(getComputedStyle(p, "::before").content);
  console.log(getComputedStyle(p, "::after").content);
  console.log(JSON.stringify(p.textContent));
</script>
"£"
" per loaf"
"4.20"

The DOM text is still just 4.20 — generated content exists only in rendering. attr() inserted the data-unit attribute value.

Other pseudo-elements

Pseudo-element Styles
::first-line / ::first-letter The first line / letter of a block (drop caps)
::marker List bullets and numbers, summary triangles
::placeholder Placeholder text in inputs
::selection Text the user has selected
::backdrop The layer behind a modal dialog, popover, or full-screen element
::file-selector-button The button of input type="file"
::part() / ::slotted() Parts of web components
.article > p:first-of-type::first-letter {
  float: left;
  font-size: 3.5em;
  line-height: 1;
  margin-inline-end: 0.1em;
}

::selection { background: #ffe58f; color: #111; }

dialog::backdrop { background: rgb(0 0 0 / 50%); backdrop-filter: blur(2px); }

input[type="file"]::file-selector-button {
  border: 0;
  padding: 0.4rem 0.8rem;
  border-radius: 6px;
  background: #eee;
}

Practice

Add an arrow "→" after every link with the class more, spaced a little from the text, and make it move right on hover.

Show solution
.more::after {
  content: "→";
  display: inline-block;
  margin-inline-start: 0.25em;
  transition: transform 150ms;
}

.more:hover::after {
  transform: translateX(3px);
}

display: inline-block is needed because transforms don't apply to inline boxes.