Action Items
Concrete tasks to bring the Prometheus UI into compliance with the design system and improve visual polish.
Compliance Tasks
These changes are required to make the Prometheus UI compatible with the design system. Each item addresses a concrete inconsistency found during the codebase audit.
The header uses inline bg="rgb(65, 73, 81)" and c="#fff" for both AppShell.Header and AppShell.Navbar. These hardcoded values do not adapt to the user's light/dark mode preference β the header is permanently dark regardless of theme. Replace with CSS custom properties from the design system.
Before / After
Code Diff
src/App.tsx
Three separate health badge implementations exist across the codebase, each rendering differently: AlertsPage uses Badge.module.css classes, RulesPage uses a Mantine color prop, and ScrapePoolsList uses a conditional color prop. Replace all three with the canonical <StatusBadge> component.
Before / After
CSS module classes with light-dark() step-1/step-9 colors. Best dark mode support, avoids Mantine Badge overhead for performance-critical label rendering.
Use Mantine <Badge color="red"> prop directly. Simpler API but uses Mantine's default shade (step-6 filled), less control over dark mode colors.
Conditional color prop: color={health === "up" ? "green" : "red"}. Minimal code but no nuance for warning/unknown states, no dark mode optimization.
Code Diff
<StatusBadge> internally maps all three vocabularies (firing/pending/inactive, ok/err/unknown, up/down/unknown) to canonical health states via a single centralized lookup.
src/pages/AlertsPage.tsx, src/pages/RulesPage.tsx, src/pages/targets/ScrapePoolsList.tsx
AlertsPage uses Panel.module.css classes with step-3 colors for left-border health indicators. RulesPage uses inline styles with step-4 colors β the same visual metaphor implemented twice with different shades. Replace both with <HealthPanel> and delete the now-redundant CSS modules.
Before / After
Step-3 (light) / step-8 (dark) from AlertsPage. Lighter, more subtle border accent. CSS modules, more maintainable.
Step-4 (light) / step-7 (dark) from RulesPage. Bolder borders, more prominent. Uses inline styles.
Code Diff
src/pages/AlertsPage.tsx, src/pages/RulesPage.tsx, src/Badge.module.css (delete), src/Panel.module.css (delete)
Four pages (AlertsPage, RulesPage, TargetsPage, ServiceDiscoveryPage) each copy-paste the same filter bar pattern: Group + StateMultiSelect + TextInput + optional ActionIcon. Extract into a shared <FilterToolbar> component with an optional showCollapseAll prop.
Before / After
Single <FilterToolbar> with optional showCollapseAll prop. DRY, one place to maintain.
Keep the 4 separate implementations but normalize them to use the same spacing, sizing, and state classes. Less coupling, but duplication persists.
Code Diff
src/pages/AlertsPage.tsx, src/pages/RulesPage.tsx, src/pages/targets/TargetsPage.tsx, src/pages/service-discovery/ServiceDiscoveryPage.tsx
Both AlertsPage and RulesPage use "alertsPage.showEmptyGroups" as the localStorage key for the "show empty groups" toggle. This means toggling the option on one page accidentally affects the other page β a real functional bug. The fix is a one-line change: rename RulesPage's key to "rulesPage.showEmptyGroups".
false) is the correct initial state, so this side effect is acceptable.
Code Diff
src/pages/RulesPage.tsx
The label pair hover state in SeriesName uses hardcoded light-mode-only colors: #add6ffa0 (a semi-transparent light blue) and #495057 (dark gray text). In dark mode these produce a washed-out, low-contrast highlight. Switch to light-dark() to support both themes properly while preserving the exact light-mode appearance.
Code Diff
src/pages/query/SeriesName.module.css
The "Prometheus" logo text is rendered by two separate Text elements: one with hiddenFrom="sm" and one with visibleFrom="md". Between breakpoints sm and md, neither text element is visible β the brand name disappears from the header. Replace with a single element that avoids this gap.
Code Diff
src/App.tsx
The header in App.tsx uses numeric pixel values for gap props: gap={40} for the logo-to-nav spacing, gap={12} for the nav links row, and gap={10} for the logo group. These should use Mantine's semantic spacing tokens so they benefit from the theme's spacing scale and scale consistently.
Code Diff
src/App.tsx
The Prometheus logo text uses fz={20} β a bare number which Mantine interprets as a pixel value. This bypasses user font-size preferences (e.g. browser zoom or accessibility font scale). Define a --prom-logo-font-size token at 1.25rem (equivalent at 16px root) and reference it.
Code Diff
src/App.tsx, src/App.module.css
The icon style { width: "0.9rem", height: "0.9rem" } is defined locally in at least 3 files (QueryPanel, RangeInput, TimeInput). The source code even has a // TODO: This is duplicated everywhere, unify it. comment acknowledging this. The existing styles.ts already exports icon size constants β add queryIconStyle to it and import from there.
Code Diff
src/pages/targets/TargetsPage.tsx, src/pages/service-discovery/ServiceDiscoveryPage.tsx, src/pages/query/QueryPanel.tsx, src/styles.ts
Polish Proposals
Small, non-controversial improvements from a principal UI designer perspective. These do not change the overall look dramatically β each makes the UI feel more intentional and professional. All are low-risk and easy to review.
The header floats directly above the content area with no visual separator. On light backgrounds this creates an ambiguous boundary β especially once PR #1 removes the dark hardcoded background. Adding a 1px border-bottom at rgba(0,0,0,0.12) creates a clean, subtle edge without adding visual weight.
Before / After
Code Diff
src/App.module.css
When no alert rules are found, the Alerts page shows a bare Mantine Alert component with the text "No rules found." This is functional but sparse. A centered empty state with an icon and brief helpful message communicates intent more clearly and feels more intentional.
Before / After
Try adjusting your search or state filter.
Code Diff
src/pages/AlertsPage.tsx
The card titles on TSDB Status, Runtime Info, and other info pages use fw={600}. At typical body sizes, fw={600} creates insufficient contrast against body text β the title doesn't read as clearly "heavier." Bumping to fw={700} sharpens the hierarchy without changing the visual style.
Code Diff
src/components/InfoPageCard.tsx
The Flags page table has no hover state β rows feel static and it's hard to track your position when scanning. Adding a subtle background highlight on hover makes the table feel interactive and improves scanability, particularly for the many rows a typical Prometheus flags table contains.
Code Diff
src/pages/FlagsPage.module.css (create), src/pages/FlagsPage.tsx
The settings popover uses Mantine Fieldset components but with mixed padding and no visual separator between the Query Settings and Alerts Settings columns. Standardizing padding to p="sm" across all fieldsets and adding a subtle column divider brings the panel into visual harmony.
Before / After
Code Diff
src/components/SettingsMenu.tsx
Label badges on the Targets and Service Discovery pages use light-dark(#eee, #3a3a3a) backgrounds. When many badges are visible simultaneously (as is common on dense target pages), the relatively strong backgrounds compete with the target metadata. Softening to Mantine's canonical gray-1/dark-6 equivalents reduces visual noise.
Before / After
Code Diff
src/pages/targets/ScrapePoolsList.module.css
The TreeNode component used for displaying PromQL AST relabeling rules has interactive elements that only respond to mouse clicks β there is no keyboard support. Users who navigate by keyboard (or assistive technology) cannot expand or collapse tree nodes. Add tabIndex={0}, role="treeitem", and Enter/Space key handlers.
Code Diff
src/components/TreeNode.tsx
The global Suspense fallback in App.tsx uses width={1000} for skeleton lines. On narrower viewports (tablets, small laptops) this causes the skeleton to overflow the visible area horizontally, producing an ugly scroll during the loading state. Switch to width="100%" with maw={1000} so skeletons are bounded by the container but never wider.
Code Diff
src/App.tsx
Migration Guide
How to adopt the Prometheus Design System incrementally. These phases can be followed in order, each building on the previous.
Incremental Adoption
The design system can be adopted incrementally. You don't need to rewrite everything at once. Follow these phases in order, each building on the previous.
Phase 1: Design Tokens
Effort: Low β Replace hardcoded values with CSS variables and TS constants.
Before / After: Header Colors
// Before β hardcoded
<AppShell.Header style={{ background: "rgb(65, 73, 81)" }}>
<Text c="#fff">Prometheus</Text>
// After β tokens
<AppShell.Header style={{ background: "var(--prom-header-bg)" }}>
<Text c="var(--prom-header-text)">Prometheus</Text>
Before / After: Font Sizes
// Before β unitless number
<Text fz={20}>Prometheus</Text> // won't scale with user font prefs
// After β rem-scaled
<Text style={{ fontSize: "var(--prom-logo-font-size)" }}>Prometheus</Text>
Before / After: Spacing
// Before β hardcoded numbers
<Group gap={40}> // hardcoded pixel value
<Group gap={12}> // hardcoded pixel value
// After β named tokens
<Group gap="xl"> // 32px β semantic
<Group gap="sm"> // 12px β semantic
Phase 2: Shared Components
Effort: Medium β Replace the three highest-impact inconsistencies.
StatusBadge (replaces 3+ implementations)
// Before β AlertsPage.tsx
import badgeClasses from "../../Badge.module.css";
<Badge className={badgeClasses.healthErr}>firing</Badge>
// Before β RulesPage.tsx (different approach!)
<Badge color="red">err</Badge>
// Before β ScrapePoolsList.tsx (yet another approach!)
<Badge color={target.health === "up" ? "green" : "red"}>
{target.health}
</Badge>
// After β unified
<StatusBadge status="firing" />
<StatusBadge status="err" />
<StatusBadge status={target.health} />
HealthPanel (replaces 2 inconsistent implementations)
// Before β AlertsPage (CSS modules, step-3 colors)
<Accordion.Item className={panelClasses[`panelHealth\${capitalize(health)}`]}>
// Before β RulesPage (inline styles, step-4 colors β different!)
<Accordion.Item style={{ borderLeft: `5px solid var(--mantine-color-red-4)` }}>
// After β consistent step-3/step-8 colors
<HealthPanel value={name} status={health}>...</HealthPanel>
FilterToolbar (replaces 4 separate implementations)
// Before β copy-pasted Group + StateMultiSelect + TextInput in 4 files
// After
<FilterToolbar
searchValue={search}
onSearchChange={setSearch}
stateOptions={["firing", "pending", "inactive"]}
selectedStates={states}
onStatesChange={setStates}
/>
Phase 3: Domain Components
Effort: Medium β Adopt remaining components.
ErrorAlertβ replace inline<Alert color="red">callsEmptyStateβ standardize empty list displaySeriesNameβ use the dark-mode-fixed versionDataTableβ replace custom table layoutsPoolAccordionβ replace TargetsPage/SD accordion logic
Phase 4: Layout Standardization
Effort: Low-Medium β Adopt layout components.
PrometheusAppShellβ replace the manual AppShell setup in App.tsxInfoPageLayoutβ replace InfoPageStackInfoCardβ replace InfoPageCardNavButtonβ replace manual Button + NavLink compositionThemeToggleβ replace ThemeSelectorSettingsPanelβ replace SettingsMenu
Component Mapping
| Source Component | Design System Component |
|---|---|
InfoPageCard | InfoCard |
InfoPageStack | InfoPageLayout |
LabelBadges | LabelBadgeSet |
ThemeSelector | ThemeToggle |
SettingsMenu | SettingsPanel |
ScrapePoolList (accordion) | PoolAccordion |
Panel.module.css classes | HealthPanel |
Badge.module.css health classes | StatusBadge |
| Inline border styles (RulesPage) | HealthPanel |
CSS Variable Migration
| Hardcoded Value | CSS Variable |
|---|---|
rgb(65, 73, 81) | var(--prom-header-bg) |
#fff (header text) | var(--prom-header-text) |
"DejaVu Sans Mono" | var(--prom-font-mono) |
1000px (info page max) | var(--prom-info-page-max-width) |
5px solid ... (panel border) | var(--prom-panel-border-width) solid var(--prom-panel-border-ok) |
fz={20} (logo) | var(--prom-logo-font-size) |
Bug Fix: localStorage Key Collision
Both AlertsPage and RulesPage used "alertsPage.showEmptyGroups" as the localStorage key. RulesPage now correctly uses "rulesPage.showEmptyGroups".
Existing users will lose their RulesPage toggle state on upgrade, but the default value (false) is the correct initial state.