Commands 1#
Examples#
First setup: tune the header live and then make it permanent#
Quickly see what your header looks like without restarting the server, and understand that command edits are temporary while permanent ones live in tablist.json.
/tablist info
/tablist set header &6&l{server_name} &8| &e{online}&8/&e{max} &7players
/tablist preview
/tablist reload
/tablist info prints the status, mode, number of header and footer frames, refreshInterval, sorting settings and the list of groups with overrides. set header instantly changes the header for all online players and collapses the animation to a single frame. preview redraws the header and footer for you. reload re-reads tablist.json from disk and restores everything from the file — the temporary header disappears without a trace.
Test a group's text before writing it into tablist.json — a group override is seen only by players in that group; everyone else keeps seeing the global header.
/tablist group vip header <gradient:00CFFF-0080FF>&l★ VIP ★&r &8| &e{online}&8/&e{max}
/tablist group vip footer �CFFF{player}&7 — &aBalance: &f{balance}
/tablist info
/tablist group vip reset
Players in the vip group immediately see the new header and footer, others the old ones. /tablist info shows vip in the line of groups with overrides. group vip reset removes the header, footer and nametag prefix/suffix overrides at once. For the text to survive a reload, move it into tablist.json → tablist.groups.vip.
Decorative separator lines in the player list#
Frame the block of players with lines above and below and label it — a common BungeeTabListPlus trick; here these are fakePlayers entries.
/tablist fakeplayer add sep1 &8&m──────────────────────
/tablist fakeplayer add title1 &6&l★ Our server ★
/tablist fakeplayer list
/tablist fakeplayer refresh
Two lines appear in the tab with no real player behind them. fakeplayer list prints their id, display, latency and listed. Entries created by the command always get latency=0 (green ping) and listed=true, without a skin — latency: -1, listed, skinOwner, skinTexture can be set only in tablist.json → tablist.fakePlayers. fakeplayer refresh removes all entries from all clients and inserts them again — this also fixes stuck lines after editing the config.
Columns by group (BTLP grid)#
Lay out players in columns: each group gets its own column with its own heading. In tablist.json → tablist.layout set groupSections: true, columns: 4, maxSlotsPerColumn: 20, fillEmptySlots: true and fill sectionHeaders with one text per group.
/tablist reload
/tablist layout info
/tablist fakeplayer refresh
layout info prints columns, the total slot count (columns × maxSlotsPerColumn), sorting state, groupSections, playersByServer and the server lists. In the tab, groups go by descending weight, each starts a new column with a heading from sectionHeaders inserted before it, and the rest of the grid is padded with invisible lines — so the client keeps exactly 4 columns regardless of how many players are online. A group with nobody in it right now is skipped together with its heading.
Put a frame animation from animations.json into the footer — its frames cycle by their own frameDuration in milliseconds, independently of the tablist's refreshInterval.
/tablist animations list
/tablist set footer &7Status: {animation:StatusDot} &8| &7TPS: {tps} &8| &7Ping: &a{ping}&7ms
/tablist reload
animations list lists the loaded animations (shipped: Rainbow, PulseStar, StatusDot, LoadingDots, GoldBanner, Spinner, HeartBeat) with their frame count and frame duration. The footer starts blinking with the indicator. /tablist reload re-reads both tablist.json and animations.json — it is the only tablist command that updates animations — and also wipes the temporary footer, so move the line into tablist.json → tablist.footer.
Edge cases#
The frame is chosen strictly along the chain player override → group override → global value, and each level has its own frame list. There is a single frame counter for the whole server: it advances once every refreshInterval ticks based on the longest list among the global one and all overrides, and each list is indexed by remainder (frame % list length). Practical effect: a static single-frame global header does not prevent animation for the vip group — the counter keeps running. The global list, however, does not loop but sticks at its last frame (min(frame, size-1)) if it is shorter than the longest override.
What to do: Frame lists of equal (or multiple) length animate in sync; if you need different speeds for different groups — the tablist does not have that, use {animation:NAME} from animations.json; each animation has its own frameDuration in milliseconds.
`columns` without `groupSections` does nothing#
The vanilla 1.21.1 client computes the number of columns itself from the number of lines in the tab — there is no server-side "make N columns" field. So columns, maxSlotsPerColumn and fillEmptySlots take part only in the grid calculation inside recomputeColumnLayout, and that calculation is skipped entirely when groupSections: false. With groupSections off, columns: 4 is visible in /tablist info and /tablist layout info but has no effect on display at all.
What to do: Columns work only with the combination groupSections: true + columns + maxSlotsPerColumn + (optionally) sectionHeaders. Filler lines are added as invisible tab entries — this is exactly the BungeeTabListPlus fixed-grid trick.
`hideVanished` affects only the `{online}` counter#
In the tablist code hideVanished is used in exactly one place — the filter that counts {online} (chat reuses the same counter). The tablist does not remove vanished players' lines from the tab. A player with neoessentials.vanish.see sees the full count.
What to do: Removing the line from the tab is the vanish system's job: vanishSettings.hideFromTabList in config.json. hideVanished in tablist.json is needed in addition so that a vanished player is not counted in the {online}/{max} line.
The AFK suffix attaches to the scoreboard team suffix, not the tab line#
With showAfkIndicator: true, afkSuffix is appended to the suffix of the same scoreboard team that gives the player their prefix/suffix. In Minecraft a team's prefix/suffix is one property for both the tab line and the nametag, so [AFK] appears in both places at once; they cannot be separated. The {afk} placeholder in the header/footer is the state of the viewing player (the header is built per viewer), not someone else's line.
What to do: If you do not want [AFK] above heads, the only way is showAfkIndicator: false; there is no separate "tab only" switch. The old AFK indicator keys in config.json (enableTablistIndicator, tablistAfkPrefix/Suffix) have been removed from the shipped config and have no effect — the handler that read them sends nothing.
A player's line is drawn by the vanilla prefix + real name + suffix mechanism, which has no other order. So playerFormat is split into four pieces of text — before {prefix}, between {prefix} and {player}, between {player} and {suffix}, after {suffix} — and these pieces are attached to the prefix and suffix. The space in "{prefix} {player} {suffix}" moves to the end of the prefix and the start of the suffix. If the template has no {player} token at all, all four pieces are treated as empty and the template is simply ignored.
What to do: You cannot put the name before the prefix or insert text "inside" the name. For text that must follow the name, use the tail after {suffix} or set nametagSuffix for the group/player.
`sortByGroupWeight: false` is not alphabetical order#
Line order in the tab is set by the scoreboard team name: with sorting on the name looks like ne_<weight sort key>_<prefix+suffix hash>, with it off — just ne_<hash>. The client sorts by team name, then by player name. So with sortByGroupWeight: false players with different prefixes are still split into clusters by hash, and the order of those clusters is arbitrary (stable within a session, but unrelated to the alphabet or group).
What to do: A truly alphabetical list happens only when all players have the same prefix and suffix. The hash in the team name exists so that players with different text do not share one team and overwrite each other's prefix.
Removing a section from tablist.json does not restore defaults#
The layout and proxy loaders work as "no section — return", so if you cut the layout or proxy block from the file and run /tablist reload, the previous values remain in memory until a server restart. The fakePlayers section behaves differently: the entry list is cleared before the check, so removing it really turns off the fake lines.
What to do: Do not delete the section; edit the values inside it ("groupSections": false, "enabled": false). Or restart the server.
Nickname (`/nick`) and the prefix in the tab#
The vanilla client wraps a tab line with the team prefix/suffix only when the entry has no explicit display name. /nick sends a display name, so the tablist builds the prefix + nickname + suffix text itself (from the same playerFormat pieces) and resends it whenever the built line changes — after a group promotion, going AFK or a config reload, without a repeat /nick.
What to do: Nothing to configure. If the nickname shows in the tab without a prefix, that is the behaviour of builds before the fix — update the mod.
nametagPrefix/nametagSuffix (in tablist.json and via /tablist ... nametag ...) go through the same placeholder pipeline as the header and footer: {balance}, {group}, {ping}, {animation:NAME} work there. The exception is {prefix} and {suffix} inside the override itself: that is a reference to its own value, so instead of infinite recursion the text is returned unresolved and a WARN goes to the log.
What to do: Do not use {prefix}/{suffix} inside nametagPrefix/nametagSuffix. If no override is set, exactly the same prefix/suffix as in chat is used (via PermissionAPI — i.e. including LuckPerms/FTB Ranks).
Permissions and access to the command#
The system's only node is neoessentials.tablist.admin; it gates the /tablist root, so for a player without it the command does not even appear in the tree. The console (a source without a player) always passes. The common permission mechanisms also apply: the operator bypass with opsBypassPermissions and the vanilla OP fallback. Separately from permissions, the command can be disabled entirely: modules.tablistEnabled: false in config.json — then /tablist is not registered at all and the tablist tick does not run.
What to do: If /tablist "does not exist" even for an operator, check modules.tablistEnabled and commands.tablist in config.json, not permissions.
The name colour in the tab and above the head is set only by the team colour#
The vanilla client draws the tab line and the nametag as team prefix + <bare name> + team suffix, and the name is a component without its own style. The adjacent prefix does not pass its style to it, so a colour code before {player} in playerFormat has no effect on the name: it ends up at the end of the team prefix and colours nothing. The only thing that reaches the name is the team's own colour, and that can only be one of the sixteen vanilla colours, no hex.
What to do: A personal name colour is set by the meta key from tablist.nameColorMeta (default farmstead-namecolor): /lp user <name> meta set farmstead-namecolor b. The value is a single vanilla code, with or without &; the tablist turns it into the team colour, so it is visible both in the tab and above the head. An empty string in nameColorMeta disables the mechanism entirely. Players with the same prefix but different name colours go into different teams — the colour is part of the team name hash, otherwise one would overwrite the other's name.
Lines anchored to another player (TabAnchors API)#
Another mod can attach a player to a "parent": TabAnchors.attach(child, parent, prefix). This is how Farmstead shows a player's AI agent: its line sits right below the owner's line, indented with ↳, and instead of the permissions prefix it has its own — prefix (which is also the nametag prefix and the prefix in join, quit and death messages, because it is the scoreboard team prefix). The order relies on the team name: the parent and its attached entries have the team ne_<weight><parent name><role>, everyone else keeps ne_<weight>_<hash>, so within one group weight players with attached lines come after the others. If the parent is offline, the attached entry sorts as a regular player, but with its own prefix. With column layout (groupSections) the attachment does not change the order.
Known issues#
`{network_online}`, `{server_online:X}` and `{current_server}` always show local data; `/tablist proxy status` is forever `detected=false`#
The proxy integration does not work in either direction. Outgoing messages: sendBungeeMessage is a stub; its javadoc says outright that encoding via StreamCodec on NeoForge 1.21.1 is not done and postponed; it only writes a debug line to the log and sends nothing. Incoming: the onPluginMessage handler that parses GetServer/GetServers/PlayerCount replies is not called from anywhere — the plugin message channel is never subscribed (the onServerStarted handler in TablistEventHandler is empty, just a comment). So serverPlayerCounts stays empty, {network_online} falls back to the local counter, {server_online:ANYTHING} gives 0, and {current_server} gives the proxy.serverLabel value.
What to do: Do not rely on network placeholders. If you need to show a number right now, set it manually: /tablist proxy setserver Lobby 12 (this is the only working data source for {server_online:Lobby} and the {network_online} sum). proxy.enabled: false is the shipped default for a reason.
`excludeServers`, `hiddenServers`, `playersByServer` and `showNetworkPlayers` have no effect#
The values are read from tablist.json and printed in /tablist layout info, but the methods that should apply them (isHiddenByServer, isExcludedServer, groupedByServer, isShowNetworkPlayers) are not called from anywhere in the mod. Layout by proxy server is not wired into rendering, and since there is no data from the proxy anyway (see above), filters by server name are doubly pointless.
What to do: Treat these four keys as declared for the future. Group players via layout.groupSections by permission groups — that branch is fully implemented.
A removed or renamed `fakePlayers` line keeps hanging in the tab after `/tablist reload`#
On reload the in-memory entry list is cleared and re-read from the config, after which only new entries are sent to players. No removal packet (ClientboundPlayerInfoRemovePacket) is sent, and the per-viewer set of already inserted UUIDs is not cleared, so the client keeps drawing an entry that is no longer in the config. Changing the id of an existing entry has the same effect: the UUID is derived from id, the old one stays and a new one appears next to it. The /tablist fakeplayer remove <id> command does not have this problem — it properly sends the removal packet to all online players.
What to do: After any edit to the fakePlayers section run /tablist fakeplayer refresh — it removes all entries for everyone and inserts them again. A player rejoining the server also clears their tab.
`/tablist layout sort` toggles nothing#
In the command tree the layout sort branch has no on|off argument — the node simply runs and prints an informational message. The class's own header javadoc and the original wiki page claim /tablist layout sort [on|off], but no such branch is registered, and typing on/off is rejected as an unknown argument.
What to do: Change layout.sortByGroupWeight in tablist.json and run /tablist reload. You can check the current state with /tablist layout info or /tablist info.
Manual `/tablist proxy setserver` counters survive `/tablist reload`#
ProxyIntegration has a reset() method with the comment "called on reload", but it has no callers at all. loadConfig() re-reads only the proxy section settings, while the accumulated serverPlayerCounts, the networkOnline sum and the "player → server" map stay in memory.
What to do: Overwrite the value with the same /tablist proxy setserver <server> 0 or restart the server — there is no other way to reset them.
The prefix/suffix disappeared for all players after a reload or after a new player joined#
Historically this was caused by two independent bugs, both already fixed and described in detail in code comments. First: before the main team update, a separate sorting pass ran that moved players to a neL_... team without a prefix; the "already sent" cache did not notice and did not resend the prefix, so after the very first update cycle everyone was left with an empty prefix until /tablist reload. Second: the team was named just by group, so all players in a group shared one team object with one shared prefix/suffix — whoever's update ran last, that text was seen by everyone else (new connections are processed last, so it looked like "a player joined and everyone's suffix reset"). Now the team name includes a hash of the actually built prefix+suffix, and the extra sorting pass is gone.
What to do: If the symptom reproduces, you have a build from before the fix — update. If prefixes still drop on a fresh build, check whether another mod manages scoreboard teams: a player can be in only one team, and any third-party /team join overwrites what the tablist set.
`/tablist disable` does not turn off everything, and not permanently#
The disable branch sets a flag in memory and sends an empty header/footer packet. It does not remove fake lines or grid filler lines, does not dismantle scoreboard teams and does not touch tablist.json. From then on updatePlayer exits on its first line, so nobody will remove the leftover entries. Any /tablist reload, /neoe reload or restart restores the enabled value from the config.
What to do: Before /tablist disable run /tablist fakeplayer refresh… or rather, to really turn the system off, set "enabled": false in tablist.json (or modules.tablistEnabled: false in config.json) and restart the server. Leftover decorative lines disappear for a player when they rejoin.
Configuration#
tablist.json#
Permissions 1#
Detailed guide#
The section below is the project's original documentation (docs/Wiki/TablistSystem.md). Links to other wiki pages are rewritten to this site; the guides themselves are not edited, so upstream syncs stay conflict-free.
Config file: config/neoessentials/tablist.json
Reload in-game: /tablist reload
Admin permission: neoessentials.tablist.admin
Introduced: build.74 — BungeeTabListPlus-inspired rewrite
Overview#
The NeoEssentials tablist system replaces the vanilla player list header and footer with a fully
customizable, animated display inspired by BungeeTabListPlus (BTLP). It operates in
independent mode by default — NeoEssentials owns the tablist entirely, no proxy plugin needed —
while optionally integrating with a BungeeCord/Velocity proxy for cross-server data.
| Feature |
Details |
| Independent mode |
NeoEssentials owns header/footer/player-rows — no proxy plugin required |
| Animated frames |
Header and footer cycle through an array of strings at a configurable tick rate |
| 20+ placeholders |
Standard, proxy-aware, economy, session, stats, and AFK tokens |
| Per-group / per-player |
Different header/footer per permission group or per player UUID |
| Group-weight sorting |
Players sorted by rank weight (admins first) then alphabetically — BTLP ContextAwareOrdering |
| Fake player entries |
Decorative separator rows, section labels, padding, and custom skins/heads — BTLP fakePlayers concept |
| Layout configuration |
1–4 column grid with per-group column sections + headers, playersByServer grouping, excludeServers/hiddenServers |
| Proxy integration |
Optional BungeeCord bridge for {network_online} / {server_online:X} data |
| Hex / gradients / rainbow |
Full RichTextFormatter syntax support — &#RRGGBB, <gradient:…>, <rainbow> |
References: BungeeTabListPlus, TAB, Simple TabList
All color and formatting tags are supported in header, footer, playerFormat, afkSuffix,
groupColors, fakePlayers[].name, and every per-group / per-player override field.
Legacy Codes (&)#
&0–&9 &a–&f — color codes
&k obfuscated &l bold &m strikethrough
&n underline &o italic &r reset
Hex Colors#
&#RRGGBB — inline e.g. &#FF5500
<color:#RRGGBB>text</color> — span with explicit close
Gradients#
<gradient:FF0000-0000FF>text</gradient> — 2-stop
<gradient:FF0000-FFFF00-00FF00>text</gradient> — multi-stop
Rainbow#
<rainbow>text</rainbow>
<red> <gold> <green> <aqua> <blue> <white> <gray> …
<bold> <italic> <underline> <strikethrough> <reset>
Always close your <gradient:...> tags. An unclosed <gradient:...> (no matching
{tps} | Server TPS — auto-coloured green/yellow/red |
| {time} | Server real-world time (HH:mm) |
| {server_name} | Plain server name (general.serverName in config.json) |
| {server_motd} | Configured MOTD (/motd if set, otherwise server.properties) |
| {x} {y} {z} | Viewing player's block coordinates |
| {balance} | Economy balance (requires EconomyManager) |
| {prefix} | Permission group prefix |
| {suffix} | Permission group suffix |
| {group} | Permission group name |
| {newline} | Line break \n |
| {bar} | Decorative strikethrough separator |
Multi-line frames#
Vanilla's tab list header/footer already renders embedded \n as a real line break — you can
either drop {newline} into a plain string frame:
"header": "&6&lWelcome!{newline}&7{online}/{max} online"
or, for a frame with several lines, write it as its own JSON array of lines instead of one long
string — the outer array is still the list of animation frames (unchanged), but any element of
it can itself be an array of lines, which get joined with \n for you:
"header": [
["&6&lWelcome!", "&7{online}/{max} online"],
["&b&lHave fun!", "&7TPS: {tps}"]
]
That's two animation frames, each two lines tall. A flat array of plain strings (no nesting)
keeps meaning exactly what it always has — multiple single-line frames, not lines of one frame.
BTLP-Style (Proxy & Network)#
| Placeholder |
Description |
{network_online} |
Total players on proxy network (requires proxy.enabled=true) |
{server_online:NAME} |
Players on a specific proxy server by name |
{current_server} |
Proxy server name the viewing player is on |
{server_label} |
This server's configured display label (proxy.serverLabel) |
BTLP-Style (Player Stats & Session)#
| Placeholder |
Description |
{rank_weight} |
Numeric permission group weight |
{session_minutes} |
Minutes elapsed in current session (0–59) |
{session_hours} |
Full hours elapsed in current session |
{level} |
Player XP level |
{health} |
Current HP (integer) |
{max_health} |
Maximum HP (integer) |
{afk} |
Blank when active; shows afkSuffix text when AFK |
PlaceholderAPI / NeoEssentials Expansion Tokens#
Any {neoessentials_*} token registered with the PlaceholderAPI system is also resolved in tablist header, footer, and player row fields. This includes all built-in tokens and any custom expansion tokens registered by other mods.
Stat examples usable in tablist:
| Placeholder |
Description |
{neoessentials_deaths} |
Player's total death count |
{neoessentials_player_kills} |
Player's total player kills |
{neoessentials_mob_kills} |
Player's total mob kills |
{neoessentials_play_time} |
Player's formatted play time (e.g. 3d 2h 15m) |
{neoessentials_balance} |
Economy balance |
{neoessentials_prefix} |
Permission prefix |
) is treated as "gradient the rest of the line" — handy when you genuinely want a
gradient to run to the end of a header/footer line, but if you follow it with plain&-codes
(e.g.&r,&8,&e) intending them to reset back to normal color, those codes get folded
into the swallowed gradient region instead. As of the current build, single legacy&-codes
(&0–&f,&k–&r) inside a gradient region are passed through atomically rather than being
split character-by-character, so this degrades gracefully — but closing the tag explicitly is
still the clearest way to control exactly where a gradient starts and stops:
```
<gradient:00FFC8-0080FF>&lGradiented Textewline} | Line break \n |
| {bar} | Decorative strikethrough separator |
Multi-line frames#
Vanilla's tab list header/footer already renders embedded \n as a real line break — you can
either drop {newline} into a plain string frame:
"header": "&6&lWelcome!{newline}&7{online}/{max} online"
or, for a frame with several lines, write it as its own JSON array of lines instead of one long
string — the outer array is still the list of animation frames (unchanged), but any element of
it can itself be an array of lines, which get joined with \n for you:
"header": [
["&6&lWelcome!", "&7{online}/{max} online"],
["&b&lHave fun!", "&7TPS: {tps}"]
]
That's two animation frames, each two lines tall. A flat array of plain strings (no nesting)
keeps meaning exactly what it always has — multiple single-line frames, not lines of one frame.
BTLP-Style (Proxy & Network)#
| Placeholder |
Description |
{network_online} |
Total players on proxy network (requires proxy.enabled=true) |
{server_online:NAME} |
Players on a specific proxy server by name |
{current_server} |
Proxy server name the viewing player is on |
{server_label} |
This server's configured display label (proxy.serverLabel) |
BTLP-Style (Player Stats & Session)#
| Placeholder |
Description |
{rank_weight} |
Numeric permission group weight |
{session_minutes} |
Minutes elapsed in current session (0–59) |
{session_hours} |
Full hours elapsed in current session |
{level} |
Player XP level |
{health} |
Current HP (integer) |
{max_health} |
Maximum HP (integer) |
{afk} |
Blank when active; shows afkSuffix text when AFK |
PlaceholderAPI / NeoEssentials Expansion Tokens#
Any {neoessentials_*} token registered with the PlaceholderAPI system is also resolved in tablist header, footer, and player row fields. This includes all built-in tokens and any custom expansion tokens registered by other mods.
Stat examples usable in tablist:
| Placeholder |
Description |
{neoessentials_deaths} |
Player's total death count |
{neoessentials_player_kills} |
Player's total player kills |
{neoessentials_mob_kills} |
Player's total mob kills |
{neoessentials_play_time} |
Player's formatted play time (e.g. 3d 2h 15m) |
{neoessentials_balance} |
Economy balance |
{neoessentials_prefix} |
Permission prefix |
&r &8| &enormal text again
```
Placeholders#
Standard#
| Placeholder |
Description |
{player} |
Viewing player's username |
{displayname} |
Display name (group colour applied) |
{online} |
Online player count (respects hideVanished) |
{max} |
Server max slots |
{ping} |
Viewing player's ping (ms) |
{world} |
Current dimension path (overworld, the_nether, the_end) |
{tps} |
Server TPS — auto-coloured green/yellow/red |
{time} |
Server real-world time (HH:mm) |
{server_name} |
Plain server name (general.serverName in config.json) |
{server_motd} |
Configured MOTD (/motd if set, otherwise server.properties) |
{x} {y} {z} |
Viewing player's block coordinates |
{balance} |
Economy balance (requires EconomyManager) |
{prefix} |
Permission group prefix |
{suffix} |
Permission group suffix |
{group} |
Permission group name |
{newline} |
Line break \n |
{bar} |
Decorative strikethrough separator |
Multi-line frames#
Vanilla's tab list header/footer already renders embedded \n as a real line break — you can
either drop {newline} into a plain string frame:
"header": "&6&lWelcome!{newline}&7{online}/{max} online"
or, for a frame with several lines, write it as its own JSON array of lines instead of one long
string — the outer array is still the list of animation frames (unchanged), but any element of
it can itself be an array of lines, which get joined with \n for you:
"header": [
["&6&lWelcome!", "&7{online}/{max} online"],
["&b&lHave fun!", "&7TPS: {tps}"]
]
That's two animation frames, each two lines tall. A flat array of plain strings (no nesting)
keeps meaning exactly what it always has — multiple single-line frames, not lines of one frame.
BTLP-Style (Proxy & Network)#
| Placeholder |
Description |
{network_online} |
Total players on proxy network (requires proxy.enabled=true) |
{server_online:NAME} |
Players on a specific proxy server by name |
{current_server} |
Proxy server name the viewing player is on |
{server_label} |
This server's configured display label (proxy.serverLabel) |
BTLP-Style (Player Stats & Session)#
| Placeholder |
Description |
{rank_weight} |
Numeric permission group weight |
{session_minutes} |
Minutes elapsed in current session (0–59) |
{session_hours} |
Full hours elapsed in current session |
{level} |
Player XP level |
{health} |
Current HP (integer) |
{max_health} |
Maximum HP (integer) |
{afk} |
Blank when active; shows afkSuffix text when AFK |
PlaceholderAPI / NeoEssentials Expansion Tokens#
Any {neoessentials_*} token registered with the PlaceholderAPI system is also resolved in tablist header, footer, and player row fields. This includes all built-in tokens and any custom expansion tokens registered by other mods.
Stat examples usable in tablist:
| Placeholder |
Description |
{neoessentials_deaths} |
Player's total death count |
{neoessentials_player_kills} |
Player's total player kills |
{neoessentials_mob_kills} |
Player's total mob kills |
{neoessentials_play_time} |
Player's formatted play time (e.g. 3d 2h 15m) |
{neoessentials_balance} |
Economy balance |
{neoessentials_prefix} |
Permission prefix |
Resolution order: Native tablist tokens (e.g. {tps}, {ping}) are resolved first, then PlaceholderManager.setPlaceholders() handles all remaining {…} tokens. Both can be mixed in the same line.
TPS colouring: {tps} is automatically formatted — &a (≥19), &e (≥15), &c (<15). Use {tps} directly; no manual colour needed.
Configuration#
Full tablist.json structure (v5)#
{
"_configVersion": 3,
"tablist": {
"enabled": true,
"refreshInterval": 20,
"independentMode": true,
"header": [
"<gradient:FFD700-FF8C00>&l✦ {server_name} ✦&r &8| &e{online}&8/&e{max} &7players",
"<gradient:FF8C00-FFD700>&l✦ {server_name} ✦&r &8| &eTPS: {tps}",
"<gradient:FFD700-FF8C00>&l✦ {server_name} ✦&r &8| &e{time}"
],
"footer": [
"&7TPS: {tps} &8| &7Ping: &a{ping}&7ms &8| <green>{world}",
"&7Coords: &e{x}&7, &e{y}&7, &e{z} &8| &7World: &e{world}",
"&7Balance: &a{balance} &8| &7Group: &#FFD700{group}"
],
"playerFormat": "&f{prefix}&r{player}{suffix}",
"hideVanished": true,
"showAfkIndicator": true,
"afkSuffix": " &7[AFK]",
"groupColors": {
"owner": "&#FF4444",
"admin": "&c",
"mod": "&6",
"vip": "�CFFF",
"default": "&f"
},
"groups": {
"admin": {
"header": [
"<gradient:FF0000-AA0000>&l⚑ ADMIN PANEL ⚑&r {newline}&8{player} — ping &e{ping}ms",
"<gradient:AA0000-FF0000>&l⚑ ADMIN PANEL ⚑&r {newline}&8Coords &e{x}&8, &e{y}&8, &e{z}"
],
"footer": "&cAdmin tools: &f/vanish /kick /ban /mute &8| &7{world}"
},
"vip": {
"header": [
"<gradient:00CFFF-0080FF>&l★ VIP ★&r &8| &e{online}&8/&e{max} &7players",
"<gradient:0080FF-00CFFF>&l★ VIP ★&r &8| &7Welcome back, &b{player}!"
],
"footer": "�CFFF{player}&7 — &aBalance: &f{balance} &8| &7Ping: &a{ping}ms"
}
},
"players": {
"00000000-0000-0000-0000-000000000000": {
"header": "<rainbow>Hello, {player}!</rainbow>",
"footer": "&#FF5500Your special footer"
}
},
"proxy": {
"enabled": false,
"serverLabel": "Main",
"pollIntervalTicks": 100,
"showNetworkPlayers": false,
"knownServers": ["Lobby", "Survival", "Creative"]
},
"fakePlayers": [
{ "id": "sep1", "name": "&8&m──────────────────────", "latency": -1 },
{ "id": "header1", "name": "&e&lOur Network", "latency": 0 },
{ "id": "sep2", "name": "&8&m──────────────────────", "latency": -1 }
],
"layout": {
"columns": 1,
"sortByGroupWeight": true,
"groupSections": false,
"playersByServer": false,
"excludeServers": [],
"hiddenServers": [],
"maxSlotsPerColumn": 20
}
}
}
Core Fields#
| Field |
Type |
Default |
Description |
enabled |
boolean |
true |
Enable/disable the whole system |
refreshInterval |
int |
20 |
Update interval in ticks (20 = 1 s) |
independentMode |
boolean |
true |
NeoEssentials owns the tablist entirely |
header |
string or array |
— |
Header frame(s) — see Multi-line frames |
footer |
string or array |
— |
Footer frame(s) — see Multi-line frames |
playerFormat |
string |
"&f{prefix}&r{player}{suffix}" |
Player row display format |
hideVanished |
boolean |
true |
Exclude vanished players from {online} |
showAfkIndicator |
boolean |
true |
Append afkSuffix to AFK players |
afkSuffix |
string |
" &7[AFK]" |
AFK display suffix |
groupColors |
object |
— |
Quick {displayname} colour prefix per group |
groups |
object |
— |
Per-group header/footer/nametagPrefix/nametagSuffix overrides |
players |
object |
— |
Per-player UUID header/footer/nametagPrefix/nametagSuffix overrides |
nametagSettings.enabled |
boolean |
true |
Whether NeoEssentials manages the above-head nametag prefix/suffix at all |
Priority#
When selecting which header/footer to show, the system uses:
per-player override > per-group override > global default
A player in the vip group with a per-player override will always see their personal
header, not the VIP group header.
Nametag prefix/suffix (below) use the same priority order.
The text shown above a player's head (and, since Minecraft ties both to the same scoreboard
team property, their tab-list row) is a prefix/suffix pair. By default this is exactly the
same prefix/suffix chat already uses — whatever your permission system (internal groups, or
an external adapter like LuckPerms) has set — so there's nothing to configure to get a nametag
that matches chat.
To make the nametag say something different from chat, set an override in tablist.json:
"groups": {
"admin": {
"nametagPrefix": "&c[Admin] ",
"nametagSuffix": ""
}
}
Or per-player, under players (keyed by UUID), with the same two keys. Priority is
per-player > per-group > the permission-system default, same as header/footer.
nametagPrefix/nametagSuffix (and a permission-system default value, if it contains any)
are resolved through the same placeholder pipeline as header/footer — every token listed
under Placeholders works here too, e.g.:
"groups": {
"vip": {
"nametagPrefix": "�CFFF[VIP] ",
"nametagSuffix": " &7({balance})"
}
}
The only tokens that don't make sense here are {prefix}/{suffix} themselves (a nametag
override referencing its own value) — if one somehow does, it's returned unresolved rather
than looping forever.
In-game (runtime, not persisted — add to tablist.json manually to survive /tablist reload):
/tablist player <name> nametag prefix <text>
/tablist player <name> nametag suffix <text>
/tablist group <group> nametag prefix <text>
/tablist group <group> nametag suffix <text>
/tablist player <name> reset # also clears nametag overrides
/tablist group <group> reset # also clears nametag overrides
If you'd rather have a different mod/plugin manage nametags entirely, set
nametagSettings.enabled to false — NeoEssentials will still use the scoreboard team for
tablist sorting/columns, it just won't set any prefix/suffix on it.
Discord chat-bridge mods showing the rank prefix twice? As of build 68, this no longer
requires disabling the nametag prefix at all — see
Discord Integration for the real fix.
The short version: vanilla Minecraft folds a scoreboard team's prefix/suffix into
Player.getDisplayName() for any mod to read, and some Discord bridges (e.g. SDLink's
%display_name%/rank-sync) read that already-prefixed name AND separately re-resolve the
same rank prefix themselves, doubling it in Discord even though it's correct in-game and in
the nametag/tab-list. NeoEssentials' own Discord messages now resolve the prefix independently
(via the permission system directly, not the vanilla team), so they can never double — pair
that with disabling the bridge mod's own native relay (its conflict detection already warns
you when this applies) and Discord shows a single correct prefix with the nametag/tab-list
completely unaffected. nametagSettings.enabled: false is only still relevant if you want a
different mod/plugin to own nametags entirely — not as a Discord workaround anymore.
Independent Mode#
independentMode: true (default) means NeoEssentials is the sole owner of the tablist.
No proxy plugin should attempt to manage this server's tab simultaneously.
With independent mode on, proxy integration (proxy) is used for data only — it can
supply {network_online} and {server_online:X} counts without taking over header/footer
rendering. The actual display is always controlled by NeoEssentials.
Toggle in-game:
/tablist independent on
/tablist independent off
Proxy Integration#
When running behind a BungeeCord proxy, enable the proxy section to surface cross-server
player counts in your header/footer.
"proxy": {
"enabled": true,
"serverLabel": "Survival",
"pollIntervalTicks": 100,
"knownServers": ["Lobby", "Survival", "Creative"]
}
| Field |
Description |
enabled |
Activates BungeeCord plugin-messaging queries |
serverLabel |
Display name for this server (used by {server_label}) |
pollIntervalTicks |
How often to query the proxy (100 ticks = 5 s) |
showNetworkPlayers |
Include network players in the tab row list |
knownServers |
Servers to poll for player counts; auto-populated from GetServers reply |
Once enabled, {network_online}, {current_server}, and {server_online:NAME} will
be populated. If the proxy hasn't replied yet or proxy is unreachable, {network_online}
falls back to the local player count.
Note (NeoForge 1.21.1): Outbound BungeeCord plugin-messaging (needed to query the proxy)
requires NeoForge StreamCodec registration which is pending a future build. The proxy
integration currently receives data sent by the proxy but cannot actively poll.
Check /tablist proxy status to see detected state.
Fake Players (BTLP fakePlayers)#
Fake players are decorative tab-list entries that don't correspond to real players. Use them
for separator lines, section headers, or grid padding.
"fakePlayers": [
{ "id": "sep_top", "name": "&8&m──────────────────────", "latency": -1 },
{ "id": "net_label", "name": "&e&lNetwork Players", "latency": 0 },
{ "id": "sep_bottom", "name": "&8&m──────────────────────", "latency": -1 }
]
| Field |
Type |
Default |
Description |
id |
string |
slot_N |
Unique slot identifier (used to derive stable UUID) |
name |
string |
"" |
Display text — supports all color/format syntax |
latency |
int |
0 |
Displayed ping bar ms. -1 = disconnected icon, 0 = green |
listed |
boolean |
true |
Whether the entry appears in the visible tab list |
skinOwner |
string |
— |
Mirror a real player's current skin (resolved async, see below) |
skinTexture |
string |
— |
Raw base64 textures property value for a fully custom skin/head |
skinSignature |
string |
— |
Signature paired with skinTexture (optional — vanilla doesn't re-verify it for tab-list rendering) |
Each fake entry uses a stable UUID derived from its id via UUID.nameUUIDFromBytes(),
so the same entry is always represented by the same UUID across server restarts and reloads.
Custom Skins / Heads#
Fake entries show the default Steve/Alex head unless given a skin:
{ "id": "npc1", "name": "&bNotch", "latency": 0, "listed": true, "skinOwner": "Notch" }
{ "id": "npc2", "name": "&aCustom", "latency": 0, "listed": true,
"skinTexture": "<base64 value from mineskin.org or minecraft-heads.com>",
"skinSignature": "<base64 signature>" }
skinTexture/skinSignature take priority over skinOwner when both are present, and apply
immediately (no network call needed).
skinOwner is resolved asynchronously via the server's profile cache + session service —
the entry briefly shows the default skin until resolution completes, after which the tab list
refreshes automatically. Results are cached until the next /tablist reload.
- If
skinOwner names an unknown player, a warning is logged and the entry keeps the default skin.
Manage at runtime with /tablist fakeplayer:
/tablist fakeplayer list
/tablist fakeplayer add <id> <display text>
/tablist fakeplayer remove <id>
/tablist fakeplayer refresh
Layout & Sorting (BTLP layout)#
"layout": {
"columns": 4,
"sortByGroupWeight": true,
"groupSections": true,
"playersByServer": false,
"excludeServers": [],
"hiddenServers": [],
"maxSlotsPerColumn": 20,
"fillEmptySlots": true,
"sectionHeaders": {
"owner": "&c&l⚑ OWNERS",
"admin": "&6&l⚑ ADMINS",
"mod": "&6✪ STAFF",
"vip": "&b★ VIP",
"member": "&7MEMBERS"
}
}
| Field |
Type |
Default |
Description |
columns |
int (1–4) |
1 |
Visual columns (BTLP supports up to 4×20 = 80 slots) |
sortByGroupWeight |
boolean |
true |
Sort players by descending group weight then alphabetically |
groupSections |
boolean |
false |
Enable the BTLP-style column grid: each permission group gets its own column(s), with padding + optional header row (see below) |
playersByServer |
boolean |
false |
Bucket players by proxy server name (requires proxy.enabled) |
excludeServers |
array |
[] |
Server names fully excluded from this tab (BTLP excludeServers) |
hiddenServers |
array |
[] |
Server names whose players are hidden (BTLP hiddenServers) |
maxSlotsPerColumn |
int |
20 |
Rows per column; total grid size = columns × maxSlotsPerColumn |
fillEmptySlots |
boolean |
true |
Pad the grid out to the full columns × maxSlotsPerColumn size with invisible filler rows (only relevant when groupSections is on) |
sectionHeaders |
object |
{} |
Per-group header row text shown at the top of that group's column (only relevant when groupSections is on) |
Sorting (sortByGroupWeight, groupSections off) encodes group weight into scoreboard team
names (ne_<sortKey>_<group>) so the Minecraft client renders players in the correct order
without packet interception — the same approach BungeeTabListPlus uses via ContextAwareOrdering.
The Column Grid (groupSections)#
Vanilla Minecraft has no packet field for "use N columns" on this mod's target version
(1.21.1) — the client always computes the column count itself from the total number of tab
entries (real players + any decorative ones). BungeeTabListPlus's classic trick, and the one used
here, is to pad the tab list to a fixed total entry count so the client's auto-computed
column count stays stable no matter how many real players are online.
When groupSections: true:
- Online players are bucketed by permission group, highest weight first (same order as
groupColors).
- Each group is packed into consecutive slots. Before a new group starts, the grid is padded
with invisible filler rows up to the next column boundary — so a group's players never
straddle two columns.
- If
sectionHeaders has an entry for that group, a header row is inserted at the very top of
its column, using the same &/hex/gradient syntax as everything else.
- If
fillEmptySlots is true (default), any slots remaining after the last group are padded
with invisible fillers too, so the grid is always exactly columns × maxSlotsPerColumn entries
— keeping the client's column count fixed even when very few players are online.
Example with the config above (4 columns × 20 rows = 80 slots) and players in owner, vip,
and member:
Column 1 Column 2 Column 3 Column 4
⚑ OWNERS ★ VIP MEMBERS (empty, filler-padded)
Steve Alex Bob
(17 filler rows) (19 filler rows) (17 filler rows) (20 filler rows)
Ordering is enforced the same way as plain weight-sorting — via scoreboard team names — but each
slot (including headers and fillers) gets a unique, zero-padded position key so the client's
alphabetical team sort reproduces the exact grid layout computed above. This is recomputed once
per tick cycle (not per-viewer, since scoreboard teams are global state) so it stays in sync as
players join, leave, or switch groups.
Caveats carried over from BTLP-era plugins:
- If real players + header rows exceed columns × maxSlotsPerColumn, the overflow simply spills
past the last column — size the grid generously for your expected peak population.
- Groups with zero online players are skipped entirely (no empty header row for an empty group).
- This feature and playersByServer are independent — you can combine per-group columns with
proxy server bucketing, though the two aren't merged into a single grid automatically.
Commands#
All sub-commands require neoessentials.tablist.admin (or operator level 4).
Core#
| Command |
Description |
/tablist |
Show help |
/tablist reload |
Reload tablist.json and push changes to all players |
/tablist enable |
Enable the tablist system |
/tablist disable |
Disable; restores vanilla appearance |
/tablist preview |
Force-send your own current header/footer |
/tablist info |
Full status — mode, proxy, layout, fake players, group overrides |
/tablist set header <text> |
Set global header at runtime (lost on reload) |
/tablist set footer <text> |
Set global footer at runtime (lost on reload) |
Per-Player Overrides#
| Command |
Description |
/tablist player <name> header <text> |
Set custom header for a player |
/tablist player <name> footer <text> |
Set custom footer for a player |
/tablist player <name> reset |
Clear all per-player overrides |
Per-Group Overrides#
| Command |
Description |
/tablist group <group> header <text> |
Set header for all players in a group |
/tablist group <group> footer <text> |
Set footer for all players in a group |
/tablist group <group> reset |
Clear group overrides |
Proxy (BTLP-style)#
| Command |
Description |
/tablist proxy status |
Show proxy state, server list, and per-server counts |
/tablist proxy setserver <name> <count> |
Manually override a server's player count |
Fake Players (BTLP-style)#
| Command |
Description |
/tablist fakeplayer list |
List all configured fake entries |
/tablist fakeplayer add <id> <display> |
Add a runtime fake entry |
/tablist fakeplayer remove <id> |
Remove a fake entry by id |
/tablist fakeplayer refresh |
Remove and re-inject all fake entries for all players |
Layout & Independent Mode#
| Command |
Description |
/tablist layout info |
Show columns, sort, server config |
/tablist independent |
Show current independent mode state |
/tablist independent on |
NeoEssentials owns the tablist |
/tablist independent off |
Hand tablist control to proxy plugin |
Runtime overrides via /tablist player and /tablist group are not persisted to disk.
Add them to tablist.json → players / tablist.json → groups to survive reloads.
Animation#
Set header or footer to a JSON array to enable frame cycling:
"header": [
"<gradient:FFD700-FF8C00>&l★ My Server ★",
"<gradient:FF8C00-FFD700>&l★ My Server ★",
"<rainbow>&lMy Server"
]
Each frame is displayed for refreshInterval ticks before advancing.
{session_minutes} and other stat tokens update each frame.
Lower refreshInterval → smoother animation, more network packets.
Recommended: 10 (½ s per frame), 20 (1 s, default), 40 (2 s, very light).
Flicker-free at low intervals: As of build.184, the tablist system uses a dirty-check cache for scoreboard team prefix/suffix packets. Team update packets are only sent when the displayed value actually changes, so using refreshInterval: 1 (20 fps animation) will not cause the player prefix to flicker or disappear.
Integration with Other Systems#
Nick System#
When a player is nicknamed via /nick, TablistManager.setCustomName(uuid, nick) is called
and the tablist row updates automatically on the next tick.
AFK System#
The AFK system calls TablistManager.getAfkSuffix() and appends it to the player's scoreboard
team suffix when they go AFK, removing it when they return. The {afk} placeholder also
becomes non-empty when a player is AFK.
Vanish System#
Vanished players are excluded from {online} counts when hideVanished: true.
Staff with neoessentials.vanish.see always see all players.
Permission System#
{prefix}, {suffix}, {group} pull from the permission group system.
{rank_weight} and sorting use PermissionGroup.getPriority().
- Group prefix/suffix support full color syntax.
Economy System#
{balance} pulls from EconomyManager.getBalance(uuid) and is formatted to two decimal places.
Examples#
"header": [
"<gradient:FFD700-FF8C00>&l✦ {server_name} ✦&r &8| &eNetwork: {network_online} players",
"<gradient:FF8C00-FFD700>&l✦ {server_name} ✦&r &8| &eLobby: {server_online:Lobby} online"
]
"footer": "&7Session: &e{session_hours}h {session_minutes}m &8| &7HP: &c{health}&8/&c{max_health} &8| &a{ping}ms"
Fake player separators with section label#
"fakePlayers": [
{ "id": "sep1", "name": "&8&m──────────────────────────", "latency": -1 },
{ "id": "title1", "name": "&6&l ★ Online Players ★", "latency": 0 },
{ "id": "sep2", "name": "&8&m──────────────────────────", "latency": -1 }
]
Gradient server name + rainbow#
"header": [
"<gradient:FF0000-FFFF00-00FF00>&l{server_name}</gradient>",
"<rainbow>&l{server_name}"
]
Per-player VIP badge#
"players": {
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": {
"header": "<gradient:FFD700-FF8C00>&l✦ Super VIP ✦&r{newline}&7Welcome, &6{player}!",
"footer": "&#FFD700★ Beta Tester &8| &7{ping}ms &8| &aHP: {health}/{max_health}"
}
}
Minimal static setup#
"header": "&6&l{server_name} &8| &e{online}&8/&e{max}",
"footer": "&7{tps} TPS &8| &7{ping}ms &8| &7{world}"