Navigation
A navigation entry is the way into your app. Required fields: label and icon.
"navigation": [
{
"id": "pim.suppliers",
"label": { "en": "Suppliers", "de": "Lieferanten" },
"icon": "building",
"route": "/suppliers",
"position": 50,
"group": "pim"
}
]
| Field | Required | Meaning |
|---|---|---|
label | Yes | Sidebar label. Keep it to one or two words. |
icon | Yes | A Solar icon name, kebab-cased. |
route | No | Route registered in Cockpit, must start with /. Omit it to declare a pure container that only expands its children. |
position | No | Sort order. Lower first. |
group | No | Sidebar section key. |
id | No | Stable anchor this entry claims. |
parent | No | Anchor id this entry attaches to. |
children | No | Nested sub-navigation, same shape. |
tabs | No | "hosted" turns the entry into a tab strip. |
badge | No | A count or dot indicator. |
tab_aliases, tab_retired | No | Renamed and retired tab keys, for a hub that changed shape. |
The field names are route and position — not path and order.
route nor children has nothing to show and is dropped. No error, no warning in the sidebar — it simply does not appear. If your entry is missing, check that first.Routes are studio-relative
Routes are mounted under the studio hosting the app, which for Commerce Studio is /commerce. So "/suppliers" becomes /commerce/suppliers in the browser. Declare the studio-relative path.
The four recognised group keys
pim crm order-management configuration
| Key | Section |
|---|---|
pim | Product information — catalog data |
crm | Customers — organizations and people |
order-management | Orders, carts, stock, payments, shipping |
configuration | Markets, channels, settings and other setup |
group the sidebar does not recognise is not an error and does not drop the entry — it is collected into an unlabelled "other" bucket at the bottom. A typo like "order_management" therefore shows up as an entry in the wrong place rather than as a failure. Use one of the four.Entries sharing a key cluster under one section header, ordered across all installed apps by position. That is why the sidebar is a shared resource: your entry sits next to twelve other apps' entries.
Anchors — composing a sidebar across apps
id and parent are how apps compose one sidebar instead of each claiming a top-level entry.
"navigation": [
{
"parent": "order-management.stock",
"label": { "en": "Supplier deliveries" },
"icon": "delivery",
"route": "/supplier-deliveries",
"position": 50,
"group": "order-management"
}
]
id | The stable anchor an entry claims. The claiming app owns that entry's label, icon, route and position. Claim one on any entry a different app might reasonably extend. |
parent | The anchor id an entry attaches to, so its view sits under another app's heading. |
Naming an anchor rather than matching on a label means renaming or translating a label cannot silently break the link.
Two behaviours worth knowing:
- If no installed app claims the id, the item stays top-level rather than disappearing. Attaching to a heading that is not there degrades to a visible entry, not a lost one.
- Two apps claiming the same
idis a conflict the Cockpit reports rather than resolves.
This is the reliable route to cross-app UI today — more so than action_buttons, which no shipped app uses.
Hosted tab hubs
{
"id": "order-management.stock",
"label": { "en": "Stock", "de": "Bestand" },
"icon": "box",
"route": "/inventories",
"position": 34,
"group": "order-management",
"tabs": "hosted",
"children": [
{ "label": { "en": "Stock levels" }, "icon": "box", "route": "/inventories/stock", "position": 10 },
{ "label": { "en": "Locations" }, "icon": "map-point", "route": "/inventories/locations", "position": 20 },
{ "label": { "en": "Reservations" }, "icon": "bookmark", "route": "/inventories/reservations", "position": 30 },
{ "label": { "en": "Movements" }, "icon": "clipboard-list", "route": "/inventories/movements", "position": 40 }
]
}
That is the real shipped inventories entry.
tabs: "hosted" — the only accepted value — turns the entry into a tab strip over the children other apps attach by parent, each body coming from the contributing app's own view. It is what lets an app own a hub rather than merely join one.
tab_aliases and tab_retired map old tab keys onto new ones and mark keys that are gone, so a hub can change shape without breaking a bookmark.
Badges
"badge": { "type": "count", "source": "/suppliers?status=eq.pending&select=count" }
| Field | Meaning |
|---|---|
type | count shows a number; dot shows an indicator dot. |
source | The endpoint that returns the badge value. |
Use a badge for something an operator has to act on. A count that is always non-zero is noise.
Icons are Solar names
map-point → MapPoint, folder-tree → FolderTree) and falls back to a generic box icon when the name does not resolve. So a typo shows up as the wrong icon, never as a blank sidebar or an error.Names in use across the platform's own commerce apps:
bill-list bookmark box building card cart-large chart-2
clipboard-list database delivery folder-tree gallery global
hashtag import layers link-round list map-point
medal-ribbon ruler scale settings shield-user shop
shopping-cart tag tag-price users users-group-rounded widget
shopping-cart happens to exist in both icon sets; most of these do not. If you copy a Lucide name here you will get the fallback box.
Practical guidance
Claim one top-level entry, not one per page. Detail and form views are reached from the list, not from the sidebar. Every extra entry is one an operator scans past to reach somebody else's app.
Give the entry an id if anything might extend it. It costs nothing and it is the only way another app can contribute under your heading.
Use position in tens. 10, 20, 30 leaves room to slot something between later without renumbering — the same reason an order's positions step by ten.
Set group explicitly. An entry without one falls into the generic section, which is almost never what you want.
Where to go next
- cockpit.json reference — the other three top-level keys.
- List views — what a nav entry's route usually points at.
- How it renders — how the merge across apps works.
- Widgets — the dashboard, which uses the same studio-relative
link_to.