| name | ui |
| description | How to build UI in this repo — the bui (@backstage/ui) design system vs legacy @backstage/core-components + MUI v4, which to reach for, and how to read Backstage Storybook component/story source. Use when creating or changing pages, cards, layouts, buttons, or any frontend component. |
The three UI layers in this repo
| Layer | Import from | Status |
|---|
| bui — the new Backstage design system | @backstage/ui (aka "Backstage UI", "BUI") | Preferred for new work. The direction we're migrating toward. |
| core-components — classic Backstage components | @backstage/core-components | Legacy but still required where bui has no equivalent (e.g. feature-rich Table, Page/Header/Content scaffolding, Link with route refs). |
| Material UI v4 | @material-ui/core | Legacy primitives + makeStyles. Used for styling and gaps bui doesn't cover. MUI v4 docs. |
Rule of thumb: reach for bui first. Fall back to core-components / MUI v4 only
when bui lacks the piece you need. Mixing all three in one file is normal and
expected during the migration — see ClusterAboutCard.tsx, which imports Grid
from bui, Link from core-components, and Box/Tooltip from MUI v4 together.
@backstage/ui is currently 0.16.0 — a young, fast-moving package. APIs change
between releases and it is not a full replacement for core-components yet.
When unsure whether a bui component exists or what props it takes, check the
Storybook source (see below) rather than guessing.
bui setup (already done)
- The plugin's
package.json must declare "@backstage/ui": "backstage:^"
(already present in gs, ui-react, ai-chat, and app).
- The global stylesheet is imported once in
packages/app/src/index.tsx:
import '@backstage/ui/css/styles.css';. Don't re-import it per component.
- We do not wrap the app in
BUIProvider — bui components render against the
existing app theme + the global CSS. (Storybook recipes use BUIProvider
because they render in isolation; the real app doesn't need it.)
bui components we actually use
Common primitives already in the codebase (import from @backstage/ui):
- Layout:
Flex, Grid, Box, Container
- Surfaces:
Card, CardHeader, CardBody, CardFooter
- Typography:
Text (e.g. <Text as="h3" variant="title-x-small" weight="bold">)
- Controls:
Button, ButtonIcon, Switch, Link
- Overlay/menu:
Tooltip, TooltipTrigger, MenuTrigger, Menu, MenuItem
- Data display:
Table + Cell / CellText / ColumnConfig, List, ListRow, Avatar
- Page chrome:
PluginHeader, Header (the two-tier header pattern — plugin-level
PluginHeader above an entity-level Header). NFS page headers and
SubPageBlueprint tabs are rendered by the custom GSPageLayout swappable
component, not directly — see "Page headers and tabs" in docs/ui.md before
building a tabbed page.
Cards: use the shared InfoCard wrapper
@giantswarm/backstage-plugin-ui-react exports an InfoCard built on bui's
Card/CardHeader/CardBody/CardFooter with our standard title styling and
header/footer action slots. Prefer it over hand-rolling a bui Card or the
core-components InfoCard for new cards. Source:
plugins/ui-react/src/components/InfoCard/InfoCard.tsx.
Tables: see the tables skill
The bui Table (data-driven columnConfig + data, cells must return
Cell/CellText) and the feature-rich core-components Table are both
documented in depth in the tables skill, including the choice matrix and
gotchas (loading skeleton needs data={undefined}). Read that skill for anything
table-related; don't duplicate it here.
Reading Backstage Storybook source
The Backstage Storybook renders components
from the upstream backstage/backstage monorepo — stories are .stories.tsx
files colocated with their components. There is no separate storybook repo,
and we do not run a local storybook. To learn a pattern (how a page/card is
composed, what props a bui component takes), read the story + component source
directly.
The reliable technique: index.json → importPath → GitHub raw
Every Storybook deployment publishes a machine-readable index at
https://backstage.io/storybook/index.json. Each entry carries the exact source
paths:
{
"id": "recipes-pluginheader-and-header--with-tabs",
"title": "Recipes/PluginHeader and Header",
"importPath": "./packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx",
"componentPath": "./packages/ui/src/components/PluginHeader/PluginHeader.tsx"
}
Then fetch the raw source from GitHub (strip the leading ./):
BASE=https://raw.githubusercontent.com/backstage/backstage/master
curl -sfL "$BASE/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx"
curl -sfL "$BASE/packages/ui/src/components/PluginHeader/PluginHeader.tsx"
To find the story ID for a Storybook URL: the ?path=/story/<id> query param
is the entry id. Story IDs can drift between releases (the "Recipes/..."
example also now exists as "Backstage UI/PluginHeader") — search index.json by
title or a keyword rather than trusting an old URL. Quick lookup:
curl -sfL https://backstage.io/storybook/index.json \
| jq '.entries | to_entries[] | select(.value.title|test("PluginHeader";"i")) | .value | {id,importPath,componentPath}'
Live inspection with Chrome DevTools
Use the chrome-devtools MCP when you want to inspect the rendered result
(DOM, computed styles, a11y tree) or grab index.json without CORS friction:
new_page / navigate_page to https://backstage.io/storybook/
evaluate_script running fetch('https://backstage.io/storybook/index.json')
(same-origin, so no CORS block) to pull the index or any raw story text
take_snapshot for the a11y tree of a rendered story
Note: the chrome-devtools MCP has its own filesystem roots — it can't write into
the session scratchpad. Fetch text via evaluate_script and return it inline, or
just curl the GitHub raw URL from Bash instead.
Related skills
tables — bui Table vs core-components Table, columns, filtering, faceted sidebars.
components — component directory structure, named exports, barrel files.
upstream-search — searching a local clone of backstage/backstage (needs BACKSTAGE_UPSTREAM_DIR set; the Storybook technique above needs no clone).