A XenForo widget is not a plugin. It is a named block, a type that knows how to render, and a hole in a template that will show it. Mix those three up and you install a “stats widget” add-on to print three numbers the core already ships, or you paste HTML into PAGE_CONTAINER and spend the next upgrade merging a layout you did not need to touch.
This is not the portal article. That one decides the first screen and stops at three blocks. This one is the machinery: how a widget instance differs from a definition, how a position is registered, how <xf:widgetpos> and <xf:widget> differ, which stock types the official 2.3 developer manual actually names, and when you should write an add-on instead of another HTML box.
If you came here because an old shop page promised an Ultimate Stats Widget, stay. The core forum_statistics definition is the honest replacement. A custom widget is for a query or a layout the core will not do.
Three nouns, one ACP
XenForo’s official developer materials never sit you down and define the three nouns. The template tags and the Development → Widget positions screen only make sense if you do.
| Noun | What it is | Where you touch it |
|---|---|---|
| Definition | The type: HTML, New posts, Members online, or a PHP class an add-on registered | Appearance → Widgets → Add widget, then pick the type. Developers also install definitions with the add-on |
| Instance | One configured copy of a type: its widget key, title, options, display condition, cache lifetime, and the positions it is assigned to | Appearance → Widgets. You can have three New posts instances with three keys |
| Position | A named hole in a template. Instances assigned here render when that template runs | Stock positions ship with the style. Custom ones: Development → Widget positions, then a <xf:widgetpos> in a template |
The official manual index lists Appearance and layout → Widgets. Community 2.3 threads confirm the working path: Appearance → Widgets. Add, edit, deactivate. That screen is how an admin lives. The Development screen is how a designer or add-on author creates a new hole.
Two instances can share a definition. That is the whole point of a type. Two New posts widgets — sidebar_new limited to five public rooms, portal_new limited to the article forum — are two rows, one definition. Official docs do not spell that sentence out. The <xf:widget key="…"> tag only makes sense if it is true.
Give every instance a stable widget key you would type in a template: guest_welcome, sidebar_stats, portal_featured. Auto-generated widget3 is how you break a page-node portal six months later.
Positions: holes, not decorations
A position is a placeholder. Official 2.3 developer docs: you register it under Development → Widget positions. The form asks for:
- Position ID — machine name, for example
demo_portal_view_sidebar - Title and Description — what an admin sees in the widget manager
- Add-on — the add-on that owns the position, so uninstall removes it
The ID is what the template will call. The title is what a tired admin will search for at 23:00. Sidebar on the staff handbook page beats pos2.
To render everything assigned to that hole, official docs put this in the public template:
<xf:widgetpos id="demo_portal_view_sidebar" position="sidebar" />
Documented attributes on <xf:widgetpos>:
| Attribute | Official meaning |
|---|---|
id |
The position ID. Required |
position |
Layout hint: sidebar or sidenav |
context-* |
Values passed into the widgets, for example context-forum_id="{$forum.node_id}" |
The developer tutorial’s default example is forum_list_sidebar. Other stock names — forum view, What’s new, member view, above/below content — are not listed in the official notebook we queried. They exist in the product (the portal article already uses Forum list: Above nodes and Forum list: Sidebar). Do not invent IDs. Open Appearance → Widgets, add a stock widget, and read the position picker. Those labels are the map.
A style that never calls <xf:widgetpos id="forum_list_sidebar"> will not show anything you assign there. That is the usual “I added a widget and nothing happened” ticket: the active style replaced the sidebar and forgot the tag. Fix the style. Do not clone PAGE_CONTAINER to paper over a missing position. Community style practice (and the portal article) is explicit: large layout shifts in PAGE_CONTAINER are a merge you will hate. The position tag is the injection point XenForo already gave you.
Positions move when the first screen moves
Change Setup → Options → Basic board information → Index page route, or flip Forums default page to New posts, and the forum-list positions stop running on /. The widgets are not deleted. They are assigned to a template that is no longer the first screen. The portal article already documented this. If you are building widgets for a page-node home, either assign them to that page’s position or call them by key. Do not assign them only to forum_list_sidebar and then steal the index.
Calling one widget by key
Positions are “everything assigned here.” Sometimes you want this instance, on this page, with no extra ACP assignment.
Official template syntax:
<xf:widget key="portal_featured" />
<xf:widget key="portal_featured" position="sidenav" />
<xf:widget class="Some\\AddOn\\Widget\\Thing" title="Staff notes" />
Documented attributes on <xf:widget>:
| Attribute | Official meaning |
|---|---|
key |
The widget key from the instance settings |
position |
Override the default render position |
class |
A PHP class that is a widget definition. Cannot be combined with key |
title |
Title override, used when you call via class |
That is how a page-node portal stays a page: create the instances in Appearance → Widgets, give them keys, leave forum-list positions empty, and call the keys from the page HTML. The portal article already showed the markup. Do not register a custom position just to print two stock widgets on one page.
Stock types the official manual actually names
The 2.3 “let’s build an add-on” tutorial instantiates these definition keys and assigns them to a custom sidebar. Treat this list as the documented core, not as a catalogue of every row in your widget manager.
| Key | What official docs say it shows |
|---|---|
members_online |
A real-time list of online members |
new_posts |
Recently published replies |
new_profile_posts |
Recent profile status updates |
forum_statistics |
Totals: threads, posts, members |
share_page |
Social sharing controls |
Birthdays, staff online, latest watched, HTML, Featured content — the official notebook does not document those types or their ACP option screens. They still appear in a stock 2.3 widget manager. Use them. Do not pretend this article reverse-engineered every checkbox from a source that did not list them.
Two that matter in practice, already covered on this site so we will not rebuild them:
- HTML. The admin type. Title, template body, optional Advanced mode (drops the block chrome). Guest-only display condition. This is your welcome, your short rules, your “start here.” It is not a CMS.
- Featured content. 2.3 curator. Thread tools feature/unfeature; the
featured/route and the widget share the queue. The official developer manual in the notebook demonstrates featuring via a repository finder on a custom page and mentions a RESTfeature:readscope. It does not document a dedicated featured widget. The product still has one. Use the product. See the portal article for how to keep that widget from rotting.
forum_statistics is the honest stats widget. Threads, messages, members, newest member. If you need “players online on FiveM” you need a different query — that is a custom widget or the server listing work, not a second statistics block.
HTML widget: the first custom widget you should ship
Most boards never need a PHP class. They need one HTML instance with a key and a condition.
- Appearance → Widgets → Add widget.
- Definition: HTML.
- Widget key:
guest_welcome(orstaff_handbook_note). Notwidget7. - Title: what a member should see on the block. Empty title plus Advanced mode if you are writing your own heading.
- Body: a few sentences. Template syntax is compiled. You can use
$xf.visitorand the usual XenForo helpers. You cannot run arbitrary PHP. - Display condition: guests only is
$xf.visitor.user_id == 0. Staff only is a group check you already trust from permissions. Official docs document the criteria system for trophies, promotions, and notices — not as a widget form. The widget manager’s condition field is template syntax, not that criteria UI. If your board’s condition box is empty, the widget shows to everyone who can see the page. - Positions: one. Or none, if you will call it by key.
- Cache lifetime: if the editor offers it, use it for anything that is not personal. A welcome paragraph can live for hours.
members_onlineshould not.
Advanced mode is for when the wrapping .block fights a layout you already wrote. It is not a reason to dump a landing-page’s worth of CSS into a widget. That CSS belongs in a child style, split the way the performance article described (<xf:include> / <xf:css>), so you do not invalidate the global sheet for a box that only the handbook page needs.
Deactivate, do not delete, while you test. The key stays. The page that calls it stays. You flip the instance off.
Custom PHP: what official docs will and will not write for you
The official “let’s build an add-on” tutorial does not show a custom widget class. It does not name AbstractWidget, render(), getOptionsTemplate(), or a cache TTL API. It shows this, and this is enough for a lot of products:
- Register a position in Development → Widget positions (or create it from the add-on during install).
- Put
<xf:widgetpos>in your public template. - In the add-on’s
Setup.php, instantiate stock widgets and assign them to that position. - Run the install step from the CLI so the rows land in the database.
That is a custom layout with stock types. It is the right first add-on. You get a sidebar on demo_portal_view that already contains Members online and New posts, and an admin can still edit those instances in Appearance → Widgets.
A custom type — a widget that runs your finder, has its own options form, and appears in the definition dropdown — is a different job. Official notebook sources do not document that class. Do not copy a method list from a blog post this article will not stand behind. If you need a type:
- Confirm the current 2.3 widget abstract in
src/XF/Widget/on your install (class names move; this is why the notebook’s silence matters). - Register the definition with the add-on, not by clicking around on production.
- Give it options an admin can edit without deploying PHP.
- Filter the finder. This next paragraph is official, and it is the one that will get you posted on Reddit.
Permissions are not automatic
Official warning, worth quoting in spirit: when you query threads or posts in a custom template or position, XenForo does not automatically restrict the result to what the visitor may view. You filter the collection with entity permission checks. Skip that and a guest widget becomes a leak of the private staff forum.
Stock widgets (new_posts, featured, and the rest of the core list) apply visitor permissions. A guest does not see a thread they could not open. That still does not save you from featuring a private thread — the slot is empty and the hero looks broken. Feature public rooms. Filter custom finders. The permissions article is the model; this one will not re-teach Yes / No / Never.
Caching, guests, and “who is online”
Official 2.3: $config['pageCache']['enabled'] stores entire pages for guests. It is off by default because it consumes cache resources. The official notebook does not document how members_online behaves under that cache.
The implication is mechanical, not mystical. A guest page cache is a finished HTML file. A “12 members online” line inside that file is as old as the cache entry. If you enable guest page cache (or LiteSpeed / Nginx FastCGI in front of it — the speed article already covered those), either:
- keep
members_onlineoff the guest first screen, or - accept that guests see a slightly stale count, or
- exclude that route from the page cache.
Do not invent a hole in the HTML cache for one widget on day one. Measure. Most guests do not come for the online list. Members, who bypass the page cache, still see a live one.
If a widget editor offers a cache lifetime, a New posts block that is ninety seconds stale is fine. A notice that names this visitor is not. HTML that includes $xf.visitor.username must not be cached as if it were public.
What not to do
Do not clone PAGE_CONTAINER to add a sidebar. Register a position. Call <xf:widgetpos>. Merge pain on PAGE_CONTAINER is how boards skip 2.3.
Do not put the same instance in two positions on one page. You pay for the query twice. The visitor reads the same five titles twice.
Do not stack twelve widgets on the forum list. Each is a query, a render, often a permission pass. Three is a homepage. Twelve is an application. The performance article is the other half of this sentence.
Do not install a stats add-on to print thread / post / member counts. Use forum_statistics. If the old Styles Factory product page for a stats widget sent you here, that is the mapping.
Do not trust a custom finder. Filter it. Then test as guest, as a new Registered account, and as a moderator who can see the private node. Three sessions. Same rule as every other article on this site.
Do not leave outdated templates. Official upgrade note: XenForo does not overwrite customized templates. A widget position you pasted into a modified forum_list will go stale. Appearance → Outdated templates, then the merge tool, after every upgrade.
Checklist: one HTML widget, one add-on position
A. HTML widget an admin can ship today
- Appearance → Widgets → Add widget → HTML
- Key
guest_welcome, title a guest can read - Four sentences, no custom CSS novel
- Display condition: guests only
- One position (or no position +
<xf:widget key="guest_welcome" />on a page) - Advanced mode off unless you wrote the wrapper
- Guest session, Registered session, admin session
B. Add-on that only rearranges stock widgets
- Development → Widget positions → Add widget position
- Position ID
youraddon_page_sidebar, add-on set to yours -
<xf:widgetpos id="youraddon_page_sidebar" position="sidebar" />in the public template -
Setup.phpcreates instances ofnew_posts/forum_statistics/members_onlineand assigns them - CLI install step run once
- Admin can still edit those instances under Appearance → Widgets
- Finder (if you added any) filtered by visitor permissions
- Outdated-templates check after the next upgrade
C. A new widget type (only if A and B failed)
- You have a query or an options form the HTML widget cannot express
- You will read
src/XF/Widget/on this exact 2.3 build before you name a parent class - Definition ships with the add-on, not as a one-off on production
- Options an admin can change without deploy
- Cache lifetime for public output; no personalization inside a cached payload
- Three-session permission test
When to stop
If the block is words, ship HTML. If the block is “latest posts from these nodes,” ship new_posts with a key. If the block is “how big is the board,” ship forum_statistics. If the block is “this page needs a sidebar that an admin can populate later,” ship a position and <xf:widgetpos>.
A custom PHP type is for a query the core will not run — a queue depth, a live server count, a staff SLA — and for an options form you want in the widget manager. It is not for a nicer number next to “Members.”
The portal is still three widgets and a decision. This article is how those widgets exist. Use the manager. Name the keys. Register a hole only when a template needs a hole. Filter every finder you write. That is a custom widget system. The add-on store can wait.

