This is the CSS class for forum admins who are not front-end developers. You will learn enough selectors, cascade, and XenForo Less to change a header, a node row, or a button without forking a template. The loop is always the same: inspect the live page, copy a stable class, put the rule in the child style’s extra.less, and prefer @xf- tokens over pasted hex so the 2.3 light / dark switch still works.
This is not the upgrade-safety process. Child-versus-parent, template modifications versus a cloned PAGE_CONTAINER, and the Outdated templates merge after a 2.3.x drop are a later article. Stay here for the language you type into that child. It is also not the typography / self-hosted font walkthrough, not the responsive / mobile pass, and not a permission to paint the node tree before the tree itself makes sense.
Official sources: Appearance → Styles, style properties (docs.xenforo.com/manual/appearance/style-properties), inheritance (a style with no parent inherits the hidden Master style). extra.less itself is not in the official docs notebook. It is community standard — XenForo community threads and ThemeHouse-style install notes — and it is the file this article assumes you will use.
What you are actually editing
A XenForo style is three things that compile together:
| Layer | Where you touch it | What it is for |
|---|---|---|
| HTML templates | Appearance → Styles → [style] → Templates | Markup. Avoid this until you have a reason. |
| CSS / Less templates | Same list (app.less, extra.less, per-page sheets) |
Rules. This article lives here. |
| Style properties | Appearance → Styles → [style] → Style properties | Colours, type, numbers+units, switches, CSS groups. Change these first. |
Official inheritance: a new style with no parent inherits Master (hidden). A child inherits its parent unless you override a property or customize a template. Official Appearance options pick the Default style for visitors and a separate default style for HTML email. Those are which style is served, not where you type CSS.
The operational rule that every other style article on this site already uses:
- Do not edit Master.
- Do not edit the vendor parent you will overwrite on the next paid update.
- Create a child. Put experiments in that child’s
extra.less.
Create the child the official way before you write a rule:
- Appearance → Styles → Add style.
- Title it something you will still understand in a year (
Board, notcss2). - Set the parent to Default style, or to the vendor 2.3 parent you actually serve.
- Leave user-selectable off until you mean guests to pick it.
- Open that child → Templates → search
extra.less→ edit.
If you already serve a child and you have been typing into the parent, stop. Move the rules. The parent is for the vendor. The child is for you.
The inspect → extra.less loop
This is the whole skill. Everything else is vocabulary.
- Open the public board in a normal browser, not the ACP. Use a guest window and a throwaway Registered account. The configuration mistakes habit stands: if you only inspect as admin, you style admin’s chrome.
- Right-click the thing you want to change → Inspect.
- In the Elements / Inspector panel, look at the class list on the highlighted node and its parents. You want a class XenForo will still emit next year, not a generated
nth-child. - In the Styles pane, see which rule currently wins and which file it came from. Core rules come from compiled Less (
app.lessand friends). Your job is to write a more specific rule, or the same selector later inextra.less. - Copy the stable class. Type a rule in the child’s
extra.less. Save. Hard-refresh the public page (Ctrl+F5/ empty cache). XenForo compiles Less on save; a stalecss.phpat a CDN is a different problem — purge if you cache that URL.
A good first paste looks like this:
.p-header {
// one change, then save, then look
box-shadow: none;
}
A bad first paste looks like this:
html body .p-body-pageContent > div:nth-child(3) .block:nth-of-type(2) .block-body > .node:nth-child(4) .node-title a {
color: #c00;
}
The second selector is a photograph of today’s DOM. Add a widget, change display order, or enable a portal and it misses. Prefer a class that names the thing: .node--id12, .p-header-logo, .button--cta.
Work one rule at a time. A novel you cannot bisect is how you spend Saturday “just reverting.”
Selectors a forum admin actually needs
You do not need the entire CSS spec. You need the six shapes XenForo pages are built from.
Element. a, img, h1. Too broad for a board. You will restyle quotes, staff bars, and the editor by accident.
Class. .p-header, .message, .button. This is 90 percent of the job. XenForo’s public classes are stable hooks. Learn the family, not a random generated string.
Descendant. .message .bbWrapper a — links inside post bodies. Useful. .p-body a — every link in the content column, including node titles and breadcrumbs. Too wide.
Child. .p-nav-inner > .p-nav-scroller — only a direct child. Use it when the descendant version catches a nested copy.
Combined class. .node.node--unread — an element that is both a node and unread. This is how XenForo marks state. Keep unread on .node--unread / .node--read. Do not invent a second unread mechanism with opacity.
Attribute / ID (rare). XenForo public markup is class-first. Node IDs appear as classes (.node--id12), not as #12. If you find yourself writing #XF tricks, you are usually in the ACP or in a widget you should have given a class.
State selectors you will use:
| Selector | When |
|---|---|
:hover |
Buttons, nav items, node titles |
:focus / :focus-visible |
Keyboard users. Do not remove outlines unless you replace them. |
:first-child / :last-child |
Tightening a list you control |
::before / ::after |
Decorative rules. Not a substitute for 2.3 SVG icons. |
A raw content: "\f019" in extra.less is invisible to the Icon Usage Analyzer. Use the Less mixin the nodes article already documented (.m-faContent(@fa-var-…)) or <xf:fa /> in a template. This primer will not re-teach icons. It will tell you not to paste a 2022 webfont snippet into a 2.3 child.
Cascade, specificity, and !important
CSS is a fight about who wins, not about who typed first in your notebook.
Origin + importance. A normal author rule loses to an !important author rule. An !important author rule still loses to an !important user-style, which you do not control. You almost never need !important in extra.less. If you do, your selector is too weak or you are fighting a property that should have been a style property.
Specificity. IDs beat classes beat elements. Two classes (.node.node--unread) beat one (.node). A descendant (.p-body .node) beats a lone class if the counts go that way. Count IDs, then classes, then elements. Do not memorize a 0-0-0-0 lecture. Inspect the winning rule. Raise specificity by one class, not by repeating the same class five times.
Order. Same specificity: the rule that comes last wins. extra.less is compiled into the public sheet after a lot of core Less. That is why a matching selector in extra.less usually overrides core without !important. If it does not, core used more classes, or a later page-specific sheet loaded after you. Inspect. Do not spray !important.
Inheritance. color and font-family inherit. margin and border do not. Setting color on .p-body paints everything in the column that does not set its own colour — including muted meta, which you usually wanted dimmed. Prefer the role tokens in the next section.
A practical debug order when “my CSS does nothing”:
- Are you editing the child that is actually the Default style (or the style your test user selected)?
- Did you save? Did you hard-refresh? Is a CDN still holding
css.php? - Inspect: is your selector present in the compiled sheet at all? A Less syntax error can drop the rest of the file. A missing
}above your new rule is the classic. - Inspect: is your selector present but crossed out? Something more specific won. Read that winner. Match it, then change one property.
- Are you looking at the other style variation? Hard-coded
#111on a dark canvas is “CSS that works” and a board nobody can read.
Style properties first, CSS second
Official style properties inherit from the parent unless you customize them. Types: colours, text, numbers with units, switches, multiple-choice, and CSS groups (text / background / border / padding / extra CSS).
Color palette defines chips. Basic colors assign chips to roles (textColor, content background). Official reference syntax in properties and Less:
@xf-paletteNeutral3
@xf-textColor
Prefix @xf- plus the property ID. In the Default style, Text color (textColor) uses @xf-paletteNeutral3. Change the chip, not fifty templates.
Before you write a rule for “make the header darker,” open Style properties → Header and navigation and Header/logo row. Official walkthrough energy: point text at a palette chip and the background at another chip, save, load a public page. That is a rebrand. CSS is for the leftover five percent the property groups do not expose.
CSS-type properties also have an extra CSS box. That box is still a style property: it inherits, it can be variation-aware, and it is the right place for “two extra declarations on this component.” extra.less is the right place for selectors the property screen does not own (a single node, a widget, a one-off landing block).
2.3 style variations sit inside one style: slots default and alternate, optional prefers-color-scheme, a footer gadget, cookies. Some properties become two-valued once variations are enabled. extra.less is never variation-aware by itself. Tokens and xf-intensify / xf-diminish are how a single rule serves both halves. The dark-mode article owns the switch. This article owns the tokens you type.
@xf- tokens instead of hex
Hard-coded #1a1a1a in extra.less does not flip when the member hits the variation control. The mistakes article already has the ticket titled “dark mode broke extra.less.”
Use the tokens the property system already compiled:
.p-footer {
background: @xf-pageBg;
color: @xf-textColorMuted;
border-top: 1px solid @xf-borderColor;
}
.node-title a {
color: @xf-linkColor;
&:hover {
color: @xf-linkHoverColor;
}
}
Less nesting (&:hover) compiles to .node-title a:hover. XenForo’s Less templates are allowed to nest. Keep the nest shallow. Three levels is a smell; five is a rewrite.
Common roles you will actually reach for (IDs as they appear after @xf-):
| Token family | Job |
|---|---|
@xf-paletteColor1 … and friends |
Brand chips. Recolour the chip, not the rule. |
@xf-paletteNeutral1 / 2 / 3 |
Greys. Neutral 3 is the default text chip on stock. |
@xf-textColor, @xf-textColorDimmed, @xf-textColorMuted |
Readable type on the content canvas |
@xf-contentBg, @xf-pageBg |
Surfaces. Content sits on content; page is the frame. |
@xf-borderColor, @xf-borderColorLight, @xf-borderColorHeavy |
Lines that still exist on both variations |
@xf-linkColor, @xf-linkHoverColor |
Links. Do not invent a third blue. |
If a token name is not in your Style properties list, do not invent it. Open the property, look at the ID in the URL or the input name, prefix @xf-. The public style-properties page does not reprint every ID. The ACP is the catalogue.
xf-intensify and xf-diminish
Official colour machinery: the Color palette has a switch that sets the style type Light or Dark. That switch does not mean “this style is the dark theme.” It tells intensify / mix / diminish which way to push a colour.
- Style type Light + intensify → the colour darkens.
- Style type Dark + intensify → the colour lightens.
Get the type backwards and every hover state goes the wrong way. Set Light on the light variation and Dark on the dark one before you write mixins.
In Less:
.block-header {
background: xf-intensify(@xf-contentBg, 5%);
border-bottom-color: xf-diminish(@xf-borderColor, 10%);
}
.button:hover {
background: xf-intensify(@xf-buttonBg, 8%);
}
Percentages are “how hard to push,” not “opacity.” Start at 4–8 percent. 30 percent is a different colour, not a hover.
These helpers are variation-aware. One rule produces a darker hover on the light half and a lighter hover on the dark half, because the style type flipped. That is the entire reason to use them instead of #333 on hover.
If @xf-buttonBg is not a property you have, do not invent the ID. Use the button style property group, or intensify a token you can see (@xf-contentBg, a palette chip). Honesty beats a guessed variable that compiles to nothing.
extra.less is a community standard, not a secret core file
The official styles manual describes templates + properties + inheritance. It does not, in the notebook we used, dedicate a chapter to extra.less. Community practice is consistent enough to treat as operational fact:
- Every child style has an
extra.lesstemplate. - Core upgrades do not overwrite your customized
extra.lessthe way they would overwrite an untouched core sheet. That is why people put custom CSS there. - You can split a novel into extra files and pull them in:
<xf:include template="your_custom_file.less" />
Create your_custom_file.less as a template in the same child. Include it from extra.less. Name the file after the job (extra_nodes.less, extra_header.less), not new2.
- Template modifications can target
extra.lesswhen several colour-children need the same tweak. TMS is Appearance → Template modifications. Find/replace, simple or regex. The core template stays uncustomized; upgrades still apply. You can bind a modification to an add-on ID. That is the shared tweak tool. Directly editingextra.lessin each child is the this brand only tool.
2.3 unbundles CSS. Official materials: core global styles are separate from page-specific templates, and HTTP/2 multiplexes them. Updating a page template no longer invalidates the global CSS cache. A random edit in global extra.less that should have been a page-only file is expensive — you invalidate the global sheet for a rule only the landing page needed.
For add-on or page-only CSS, put this at the top of that template:
<xf:css src="your_css.less" />
That is the speed article’s cheap-sheet rule. This primer repeats it so you do not start your CSS career by making every guest re-download the universe because you tweaked a portal hero.
Designer mode ($config['designer']['enabled'] = true) exists. It is advanced. You do not need it to edit extra.less in the ACP. Leave it off unless you already live in files on disk.
Worked rules (copy, then change one thing)
These are patterns, not a theme. Swap tokens. Do not ship them as a skin.
1. Kill a header shadow you hate
.p-header {
box-shadow: none;
}
If a style property already has a shadow switch, use that. CSS is the leftover.
2. Recolour one node title without painting the index
.node.node--id12 .node-title a {
color: @xf-paletteColor1;
}
Keep the node ID in a staff thread next to the snippet. The next admin will not remember that 12 is Support.
3. Unread row that still works on both variations
.node.node--unread .node-icon i {
color: xf-intensify(@xf-paletteColor1, 6%);
}
Do not set opacity: 0.4 on .node--read as your unread system. You will fight core CSS on every upgrade.
4. Primary button hover that follows the palette
.button.button--primary:hover,
.button.button--cta:hover {
background: xf-intensify(@xf-paletteColor1, 8%);
}
Confirm the class names on your style. Inspect a real Post thread button. Vendor styles rename things. Believe the inspector.
5. Tighten a sidebar widget you do not want to fork
[data-widget-key="online_now"] .block-body {
font-size: @xf-fontSizeSmall;
color: @xf-textColorMuted;
}
data-widget-key is a stable hook if you set the key in Appearance → Widgets. The widgets article is the rest of that job. If @xf-fontSizeSmall is not in your property list, use the typography group or a unit you already set (@xf-fontSizeNormal). Do not invent a size token.
6. Hide something only guests should not see? Stop.
CSS is not access control. display: none on a staff link still ships the HTML to anyone who views source. Private rooms are node permissions. The permissions primer is that article. The security baseline is the other one. This file does not hide staff.
What usually looks like a CSS bug
| Symptom | Likely cause | First move |
|---|---|---|
| Save does nothing | Wrong style, or CDN holding css.php |
Confirm Default style; hard-refresh; purge |
Half of extra.less vanished |
Less syntax error above the new rule | Comment out the last edit; watch the sheet return |
| Rule in the sheet, crossed out | Weaker selector than core / vendor | Add one stable class; inspect again |
| Works in light, dies in dark | Hex, or style type backwards | Tokens + intensify; check Light/Dark type |
| Works as admin, not as guest | You styled ACP or a staff-only block | Guest window |
| Icons missing after a “quick FA snippet” | 2.2 webfont CSS on a 2.3 sprite | Nodes article mixin; run the analyzer |
| Every page re-downloads CSS after a portal tweak | Novel in global extra.less |
<xf:css> on that page; split includes |
| Upgrade “ate my CSS” | You edited the vendor parent or a core template | Child + extra.less; merge outdated |
UI.X and other heavy frameworks can fail a rebuild with too many open files. Community fix: raise open_files_limit (they cite 50,000) or rebuild via CLI. That is an ops ticket, not a selector ticket. If your Less save never finishes on a huge parent, do not keep hammering Save.
A Discourse footnote (so you do not mix the products)
Discourse themes are a different compiler. Community / notebook practice splits CSS by @media and ships theme components (the discourse-full-width-component is the named wide-desktop one). Quoting popups are known-flaky on mobile. None of that is extra.less. If you run Discourse, the responsive article is the CSS chapter; the beginner’s guide is the product. Do not paste XenForo Less mixins into a Discourse theme.
What this is not
It is not permission to clone PAGE_CONTAINER for a hero. Official: customized templates are not overwritten; they go outdated. After upgrade: Appearance → Outdated templates → Merge. Blue = auto-merged, yellow = conflict. A cloned container is how 2.3’s jQuery removal lands on you as a white screen. The later customization-process article owns that merge. This article’s job is to keep you out of that queue.
It is not a second style for mobile. Responsive is already the core layout. A mobile-only style is a second chooser entry and a second bug.
It is not a font-hosting guide. @font-face, woff2, unicode-range, and font-display: swap live in the typography piece. Official 2.3 already self-hosts its UI fonts. Do not add a remote Google Fonts <link> “just to try a typeface.”
It is not Core Web Vitals. Unbundled CSS, icon sprites, and guest cache are the speed article.
Checklist
- You are editing a child, not Master, not the vendor parent.
- Style properties (palette, header, type) tried before
extra.less. - Rules live in the child’s
extra.less(or an included Less file, or a page<xf:css>). - Selectors use stable classes (
.p-header,.node--id12,.button--cta), notnth-childphotographs. - Colours are
@xf-tokens orxf-intensify/xf-diminish, not#111. - Color palette style type is Light on the light variation and Dark on the dark one.
- No
!importantunless you can name the winner you had to beat — and you still try a class first. - Guest + throwaway Registered + both variations after every save.
- Global sheet stays small; page-only CSS is not in
extra.less. - No 2.2 Font Awesome
content: "\f…"snippets. - CDN purged if
css.phpis cached at the edge.
Takeaways
- Inspect the public page. Copy a class XenForo still intends to ship. Paste it into the child
extra.less. Save. Look. That is the skill. - Official style properties are the rebrand. Less is the leftover five percent.
@xf-textColorand friends follow the variation switch. Hex does not.xf-intensify/xf-diminishpush a colour the right way once the style type is honest.extra.lessis community standard, not a documented core chapter. It is still the file you should use. Split it with<xf:include />when it becomes a novel. Put page-only rules in<xf:css>.- CSS does not hide staff rooms, does not survive a forked
PAGE_CONTAINER, and does not replace the later upgrade-safe process article. Learn the language here. Keep the process boring there.
Open a guest window. Inspect the header. Change one token. If you can do that without !important, you are done with the primer.

