Pseudo-element icons and CSS styling hooks
Render icons through ::before content codes, wire up the font-family rules once, and use v7 CSS custom properties to colour and size icons without touching the markup.
Icons without element markup
<!-- The class you write -->
<a class="btn btn-primary" href="/cart">
<i class="fa-solid fa-cart-shopping" aria-hidden="true"></i>
Cart
</a>
<!-- What Font Awesome expands it into: the glyph is injected as ::before
content, and the font-family is applied to the pseudo-element. -->
<style>
.fa-solid::before {
content: "\f07a"; /* the cart-shopping code point */
font-family: "Font Awesome 7 Free";
font-weight: 900;
}
</style>/* The complete rule set for using a code point on your own selector.
This is what .fa-solid does, written out so you can attach it to anything. */
.btn--cart::before {
display: var(--fa-display, inline-block);
font-style: normal;
font-variant: normal;
line-height: 1;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: "Font Awesome 7 Free";
font-weight: 900; /* the Solid style */
content: "\f07a";
margin-inline-end: 0.5em; /* logical property: flips in RTL */
}
/* The three free families and their weights */
.fa-regular::before { font-family: "Font Awesome 7 Free"; font-weight: 400; }
.fa-solid::before { font-family: "Font Awesome 7 Free"; font-weight: 900; }
.fa-brands::before { font-family: "Font Awesome 7 Brands"; font-weight: 400; }
/* Custom Properties (paid) use the same pattern with a different family name. */
.fa-sharp::before { font-family: "Font Awesome 7 Sharp"; font-weight: 900; }
.fa-sharp-duotone::before { font-family: "Font Awesome 7 Sharp Duotone"; font-weight: 900; }| Family | Weight | Style | Available in |
|---|---|---|---|
Font Awesome 7 Free | 900 | Solid | Free |
Font Awesome 7 Free | 400 | Regular | Free, a limited set |
Font Awesome 7 Brands | 400 | Brands | Free |
Font Awesome 7 Pro | 900 / 400 / 300 | Solid, Regular, Light | Pro |
Font Awesome 7 Sharp | 900 / 400 / 300 | Sharp Solid, Regular, Light | Pro |
Font Awesome 7 Duotone | 900 | Duotone | Pro |
Font Awesome 7 Sharp Duotone | 900 | Sharp Duotone | Pro+ |
⚠️
Overriding
content on an existing .fa-* class requires the same specificity or higher, and the icon glyph is a private-use code point. Copy the code from an authoritative source — guessing an offset by one gives you a completely different icon, and a wrong range shows a missing-glyph box.The v7 CSS custom properties
/* Font Awesome 7 exposes the icon's styling through custom properties,
which means a single class can be re-themed without a new utility class. */
.icon-badge {
--fa-primary-color: #6d28d9;
--fa-secondary-color: #c4b5fd;
/* Opacity per layer of a duotone icon */
--fa-primary-opacity: 1;
--fa-secondary-opacity: 0.4;
/* Size and layout */
--fa-size: 2rem;
--fa-width: 1.25em; /* fixed width for a vertical stack */
--fa-li-width: 2.5em; /* list item width */
--fa-display: inline-block;
/* Rotation and transform */
--fa-rotate-angle: 90deg;
--fa-flip-x: 1;
--fa-flip-y: 1;
--fa-scale: 1.5;
/* Animation timing */
--fa-animation-duration: 2s;
--fa-beat-scale: 1.25;
--fa-fade-opacity: 0.4;
/* The masking colour for fa-layers */
--fa-inverse: #fff;
}
/* Colour inherits from the text colour when no property is set. */
.themed-icon { color: #b91c1c; }| Property | Effect | Inherits from |
|---|---|---|
--fa-primary-color | First layer of a duotone or layered icon | Nothing; explicit only |
--fa-secondary-color | Second layer | Nothing |
--fa-primary-opacity | First layer opacity | --fa-primary-opacity |
--fa-secondary-opacity | Second layer opacity | Same |
--fa-size | Font size of the icon | The element's font-size |
--fa-rotate-angle | Rotation in degrees | 90deg |
color | Single-colour icons | The cascade |
--fa-animation-duration | Clock, spin and beat timing | 2s |
--fa-inverse | The colour of fa-inverse layers | #fff |
<style>
/* Replacing a whole set of utility classes with one themed rule */
.sidebar .icon {
--fa-primary-color: #0ea5e9;
--fa-secondary-color: #94a3b8;
--fa-secondary-opacity: 0.5;
--fa-size: 1.125rem;
}
/* A dark-mode override without changing any markup */
@media (prefers-color-scheme: dark) {
.sidebar .icon {
--fa-primary-color: #7dd3fc;
--fa-secondary-color: #475569;
}
}
</style>
<nav class="sidebar">
<a href="/dashboard"><i class="fa-solid fa-gauge icon" aria-hidden="true"></i> Dashboard</a>
<a href="/reports"><i class="fa-solid fa-chart-line icon" aria-hidden="true"></i> Reports</a>
</nav>Declaring icons in one place
/* A pattern that keeps icon choices out of the markup: one map of code
points, and semantic classes in the HTML. Changing the icon set becomes
a change to this file only. */
[data-icon]::before {
font-family: "Font Awesome 7 Free";
font-weight: 900;
display: inline-block;
font-style: normal;
line-height: 1;
margin-inline-end: 0.4em;
}
[data-icon="save"]::before { content: "\f0c7"; }
[data-icon="delete"]::before { content: "\f2ed"; }
[data-icon="download"]::before { content: "\f019"; }
[data-icon="search"]::before { content: "\f002"; }
/* Icon-only buttons need the margin removed and the name supplied by
the button's own accessible name. */
[data-icon-only]::before { margin-inline-end: 0; }<button type="button" data-icon="save">Save changes</button>
<button type="button" data-icon="delete">Delete</button>
<button type="button" data-icon="search" data-icon-only aria-label="Search"></button>
<!-- The advantage: no <i> element, no class names in the content, and the
pseudo-element is decorative by definition — screen readers do not
announce generated content as a separate item, so no aria-hidden is needed. -->
<a href="/export.csv" data-icon="download">Export</a>| Approach | Markup verbosity | Theming | Risk |
|---|---|---|---|
<i class="fa-solid fa-x"> | Verbose, repeated everywhere | Class-based | None |
::before on a semantic class | One attribute | Custom property or class | Generated content is invisible to the DOM APIs |
| SVG+JS replacement | Class only | CSS on the SVG | MutationObserver cost |
| The React/Vue/Angular component | A component with a name | Props and CSS | Bundle size without tree-shaking |
- Content injected by a pseudo-element is not part of the accessible tree as a separate item, which is why this approach needs no
aria-hidden. It is also why it cannot be tested by querying the DOM — read the computed::beforecontent instead. - If the icon is the only content of a control, the control still needs an accessible name:
aria-labelon the button, or visible text. - A pseudo-element inherits the element's
font-size, so icon sizing follows the text unless--fa-sizeoverrides it.
FAQ
Why is my ::before icon showing a box?
Either the font file is not loaded, or the code point is not in the family you selected. Check that the family name matches the version exactly —
Font Awesome 7 Free not Font Awesome 6 Free — and that you have installed the matching version. A missing glyph renders as a box rather than as nothing.Can I animate a pseudo-element icon?
Yes. The
::before pseudo-element supports transforms, transitions and keyframe animations exactly like a real element, so transition: transform plus a hover rotate works. Keyframe names must not collide with the ones Font Awesome ships if you also use fa-spin.Related
Using, sizing and animating icons Kits, CDN and self-hosting
Last refreshed 2026-09-18.