Building UI with bui design system (Backstage)

Guidance for building UI in this repo: use @backstage/ui (bui) first, fall back to core-components/MUI v4, and find icons/storybook sources. Apply when creating or changing frontend components.

Sby Skills Guide Bot
DevelopmentIntermediate
108/27/2026
Claude CodeCursorWindsurfCopilotCodex
#ui#backstage#design-system#frontend#storybook

Recommended for


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.

Picking an MUI v4 icon

references/mui-v4-icons.md lists all 1,120 @material-ui/icons base names with search synonyms (e.g. AccountBalance — bank building court money payment structure temple transaction), generated from the installed package version merged with MUI's own docs-search synonym data. Grep it by concept when you need an icon name but don't know it (e.g. grep -i wallet finds AccountBalanceWallet) instead of guessing or opening the MUI v4 docs site.

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.

Code blocks: use the shared CodeBlock

@giantswarm/backstage-plugin-ui-react exports a CodeBlock that renders a monospace <pre> with a neutral copy-to-clipboard button (bui ButtonIcon, tertiary variant) aligned to the top-right corner, plus a "Copy"/"Copied" tooltip. Prefer it over hand-rolling a code block or reaching for core-components' CopyTextButton — that button renders an oversized, primary-colored ButtonIcon that needs margin hacks to sit right. Source: plugins/ui-react/src/components/CodeBlock/CodeBlock.tsx.

Overriding bui component sizes

bui components size themselves via data-attribute selectors (e.g. .bui-ButtonIcon[data-size="small"] sets a fixed 32px square). Those selectors out-specify a plain makeStyles class, so a MUI-style height/width override silently doesn't take — bump specificity with !important (or &&). The icon inside is sized separately and renders larger than body text, so shrink it with a nested rule: '& svg': { width: '1rem', height: '1rem' }. CodeBlock.tsx is a worked example of both.

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.

Our shared library: the ui-react Storybook

Before hand-rolling a component, check whether @giantswarm/backstage-plugin-ui-react already has one. It is fully documented in its own Storybook — the canonical reference for what the shared library contains and how each piece is meant to be used.

  • In-repo stories are the source of truth. Each shared component has a co-located story: plugins/ui-react/src/**/*.stories.tsx. Read the story as the authoritative usage example rather than guessing from the component source. Every story also records a migration-status note (MUI v4 vs bui, via plugins/ui-react/src/storybook/docs.ts) — use it to avoid extending a deprecated MUI v4 component when bui is preferred.
  • Hosted site (for humans): published to GitHub Pages on merge to main (https://giantswarm.github.io/backstage/) — link to a component in reviews.
  • Run it locally: yarn storybook (dev server on http://localhost:6006); yarn storybook:build for a static build. The theme toolbar toggles the real GS light/dark themes.
  • Coverage gate: every exported visual component must have a story (yarn storybook:coverage, enforced in CI); config lives in the root .storybook/. See plugins/ui-react/README.md for the full component/hook/util inventory.

Components confirmed to live here (prefer them over rebuilding): InfoCard, CodeBlock (both covered above), plus AsyncValue, DateComponent, ExternalLink, GSMarkdownContent, JsonHighlight, StructuredMetadataList, the select/filter controls (SingleSelect, MultipleSelect, Autocomplete, MultiplePicker, display/FiltersLayout), YamlEditor/YamlEditorFormField, StackedBarChart, and more.

Reading upstream Backstage (bui) Storybook source

The Backstage Storybook renders upstream bui components from the backstage/backstage monorepo — stories are .stories.tsx files colocated with their components. There is no separate upstream storybook repo, and we do not run the upstream bui storybook locally (that's distinct from our own ui-react Storybook above). To learn a bui pattern (how a page/card is composed, what props a bui component takes), read the story + component source directly.

The reliable technique: index.jsonimportPath → 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).
Related skills