Portal SDK — Design Token Reference¶
Write theme-adaptive styles without importing MUI or Backstage APIs.
Overview¶
The Portal injects CSS custom properties (design tokens) onto a display: contents wrapper around every contributed component. These tokens reflect the active Backstage theme — they update automatically when the user switches between light, dark, and high-contrast themes.
Plugin authors should use these tokens for all colour values instead of hardcoding hex values or importing MUI's useTheme().
How tokens are injected¶
When the host renders a contributed tab or card, it wraps the component in a ContributionWrapper:
// Simplified from portal-extension-host/src/ExtensionRenderer.tsx
<ContributionWrapper>
<ErrorBoundary>
<YourContributedComponent />
</ErrorBoundary>
</ContributionWrapper>
ContributionWrapper renders a div with display: contents and sets all 10 CSS custom properties from useTheme().palette. Because display: contents generates no layout box, the wrapper is invisible to flex and grid containers — it does not affect your component's layout.
Theme-adaptive tokens¶
These 10 tokens are available in every contributed component:
| CSS custom property | MUI source | Description |
|---|---|---|
--portal-color-primary |
palette.primary.main |
Brand primary (Red Hat red) |
--portal-color-primary-light |
palette.primary.light |
Lightened primary for hover states |
--portal-color-primary-dark |
palette.primary.dark |
Darkened primary for active states |
--portal-color-error |
palette.error.main |
Error red |
--portal-color-warning |
palette.warning.main |
Warning amber |
--portal-color-success |
palette.success.main |
Success green |
--portal-color-text-primary |
palette.text.primary |
Primary text — high contrast |
--portal-color-text-secondary |
palette.text.secondary |
Secondary text — muted |
--portal-color-background |
palette.background.default |
Page background |
--portal-color-surface |
palette.background.paper |
Card / surface background |
Using tokens in components¶
// No MUI import needed:
export function TrustSignalsCard() {
return (
<div
style={{
backgroundColor: 'var(--portal-color-surface)',
border: '1px solid var(--portal-color-primary)',
borderRadius: '4px',
padding: '16px',
}}
>
<h3 style={{ color: 'var(--portal-color-primary)', margin: 0 }}>
Trust Score
</h3>
<p style={{ color: 'var(--portal-color-text-secondary)' }}>
Supply chain verification status
</p>
<span style={{ color: 'var(--portal-color-success)' }}>✓ Verified</span>
</div>
);
}
Using tokens in CSS-in-JS¶
If your plugin uses makeStyles (MUI v4), you can still reference the custom properties:
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles({
card: {
backgroundColor: 'var(--portal-color-surface)',
borderLeft: '4px solid var(--portal-color-primary)',
},
title: {
color: 'var(--portal-color-text-primary)',
},
subtitle: {
color: 'var(--portal-color-text-secondary)',
fontSize: '0.875rem',
},
});
Reading tokens programmatically¶
If you need the current token values in JavaScript (e.g. for a canvas or chart library), use usePortalCssTokens from @ansible/portal-extension-host:
import { usePortalCssTokens } from '@ansible/portal-extension-host';
function MyChartComponent() {
const tokens = usePortalCssTokens();
// tokens['--portal-color-primary'] → '#ee0000' (example)
return (
<canvas
ref={canvasRef}
style={tokens} // spread onto any element to inherit tokens
/>
);
}
Note:
usePortalCssTokensrequires@material-ui/coreas a dependency. It is available fromportal-extension-host, which is a host package. Plugin authors who need the values in JavaScript should add@ansible/portal-extension-hostas a dependency or usegetComputedStyle(element).getPropertyValue('--portal-color-primary')on a rendered DOM node.
Theme light / dark comparison¶
| Token | Light theme (example) | Dark theme (example) |
|---|---|---|
--portal-color-primary |
#ee0000 |
#ff4d4d |
--portal-color-background |
#ffffff |
#1b1b1b |
--portal-color-surface |
#f5f5f5 |
#2c2c2c |
--portal-color-text-primary |
rgba(0,0,0,0.87) |
rgba(255,255,255,0.87) |
--portal-color-text-secondary |
rgba(0,0,0,0.54) |
rgba(255,255,255,0.60) |
Exact values depend on the Backstage theme configured in the host. The tokens always reflect the active theme — you do not need to implement your own dark mode toggle.
Static tokens¶
For fixed sizing values (spacing, border radii) that do not need to adapt to the theme, @ansible/portal-plugin-sdk exports static constants:
import { PORTAL_TOKENS } from '@ansible/portal-plugin-sdk';
// Use in inline styles or makeStyles:
padding: PORTAL_TOKENS.spacing.card; // '12px'
padding: PORTAL_TOKENS.spacing.page; // '24px'
borderRadius: PORTAL_TOKENS.borderRadius.card; // '4px'
These are compile-time constants, not CSS custom properties, and do not change at runtime.
Anti-patterns to avoid¶
❌ Don't hardcode colours:
❌ Don't import MUI useTheme in contributed components:
// Wrong — creates a MUI singleton dependency in the plugin bundle
import { useTheme } from '@material-ui/core/styles';
const { palette } = useTheme();
❌ Don't use Backstage's UnifiedThemeProvider or createUnifiedTheme in plugins:
Backstage's unified theme is configured by the host app. Plugins should consume the host's theme via tokens, not re-configure it.
✅ Do use CSS custom properties:
// Correct — theme-adaptive, no import needed
<div style={{ color: 'var(--portal-color-text-primary)' }}>
✅ Do use static tokens for sizing:
import { PORTAL_TOKENS } from '@ansible/portal-plugin-sdk';
<div style={{ padding: PORTAL_TOKENS.spacing.card }}>
Adding new tokens¶
If you need a colour that is not covered by the current 10 tokens, open a proposal in docs/next/ rather than hardcoding values. Token additions are non-breaking (existing consumers are unaffected) and are reviewed for naming consistency before being added to ContributionWrapper and this reference.