diff --git a/.changeset/plugin-header-remove-toolbar-wrapper.md b/.changeset/plugin-header-remove-toolbar-wrapper.md
new file mode 100644
index 0000000000..084eab9b61
--- /dev/null
+++ b/.changeset/plugin-header-remove-toolbar-wrapper.md
@@ -0,0 +1,9 @@
+---
+'@backstage/ui': minor
+---
+
+**BREAKING**: Removed the `toolbarWrapper` element from `PluginHeader` and dropped `toolbarWrapper` from `PluginHeaderDefinition.classNames`. Toolbar layout styles now live on `toolbar` (`.bui-PluginHeaderToolbar`). Update custom CSS that targeted `.bui-PluginHeaderToolbarWrapper` to use `.bui-PluginHeaderToolbar` instead.
+
+**Affected components:** PluginHeader
+
+`PluginHeader` now establishes a neutral background provider (same rules as `Box` with `bg="neutral"`) so controls in the toolbar and tabs resolve `data-on-bg` correctly.
diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css
index 3cad0fba10..47706497d8 100644
--- a/.storybook/themes/spotify.css
+++ b/.storybook/themes/spotify.css
@@ -183,21 +183,6 @@
font-weight: var(--bui-font-weight-regular);
}
- .bui-PluginHeaderToolbarWrapper {
- padding: 0;
- height: 32px;
- border: none;
- background: none;
- }
-
- .bui-PluginHeaderTabsWrapper {
- margin: 0;
- padding: 0;
- margin-left: -8px;
- border: none;
- background: none;
- }
-
.bui-Input {
border-radius: var(--bui-radius-3);
}
@@ -228,14 +213,6 @@
--bui-border-success: #53db83;
--bui-ring: rgba(0, 0, 0, 0.2);
-
- .bui-HeaderToolbarWrapper {
- border: 1px solid var(--bui-border-2);
- }
-
- .bui-HeaderTabsWrapper {
- border: 1px solid var(--bui-border-2);
- }
}
[data-theme-mode='dark'][data-theme-name='spotify'] {
@@ -266,3 +243,34 @@
--bui-ring: rgba(255, 255, 255, 0.2);
}
+
+/*
+ * Plugin header (@backstage/ui) and story shell header — kept at the bottom of
+ * this file for easier scanning alongside other component overrides above.
+ */
+[data-theme-name='spotify'] {
+ .bui-PluginHeaderToolbar {
+ padding: 0;
+ height: 32px;
+ border: none;
+ background: none;
+ margin-bottom: var(--bui-space-2);
+ }
+
+ .bui-PluginHeaderTabsWrapper {
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: none;
+ }
+}
+
+[data-theme-mode='light'][data-theme-name='spotify'] {
+ .bui-HeaderToolbarWrapper {
+ border: 1px solid var(--bui-border-2);
+ }
+
+ .bui-HeaderTabsWrapper {
+ border: 1px solid var(--bui-border-2);
+ }
+}
diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md
index 511074f8bb..fcfacb7412 100644
--- a/packages/ui/report.api.md
+++ b/packages/ui/report.api.md
@@ -1949,7 +1949,6 @@ export const PluginHeaderDefinition: {
readonly classNames: {
readonly root: 'bui-PluginHeader';
readonly toolbar: 'bui-PluginHeaderToolbar';
- readonly toolbarWrapper: 'bui-PluginHeaderToolbarWrapper';
readonly toolbarContent: 'bui-PluginHeaderToolbarContent';
readonly toolbarControls: 'bui-PluginHeaderToolbarControls';
readonly toolbarIcon: 'bui-PluginHeaderToolbarIcon';
diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.module.css b/packages/ui/src/components/PluginHeader/PluginHeader.module.css
index f016ef7674..3b8aa9a698 100644
--- a/packages/ui/src/components/PluginHeader/PluginHeader.module.css
+++ b/packages/ui/src/components/PluginHeader/PluginHeader.module.css
@@ -17,15 +17,16 @@
@layer tokens, base, components, utilities;
@layer components {
- .bui-PluginHeaderToolbarWrapper {
+ .bui-PluginHeaderToolbar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-inline: var(--bui-space-5);
- border-bottom: 1px solid var(--bui-border-1);
color: var(--bui-fg-primary);
height: 52px;
+ background-color: var(--bui-bg-neutral-1);
+ border-bottom: 1px solid var(--bui-border-1);
}
.bui-PluginHeaderToolbarContent {
@@ -46,13 +47,19 @@
}
.bui-PluginHeaderToolbarIcon {
- width: 16px;
- height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 2rem;
+ height: 2rem;
+ border-radius: var(--bui-radius-2);
+ background-color: var(--bui-bg-neutral-2);
color: var(--bui-fg-primary);
& svg {
- width: 100%;
- height: 100%;
+ width: 1rem;
+ height: 1rem;
}
}
@@ -66,5 +73,6 @@
.bui-PluginHeaderTabsWrapper {
padding-inline: var(--bui-space-3);
border-bottom: 1px solid var(--bui-border-1);
+ background-color: var(--bui-bg-neutral-1);
}
}
diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx
index d0d3475dbb..faadd4fc67 100644
--- a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx
+++ b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx
@@ -19,8 +19,6 @@ import type { StoryFn } from '@storybook/react-vite';
import { PluginHeader } from './PluginHeader';
import type { HeaderTab } from './types';
import {
- Button,
- Header,
Container,
Text,
ButtonIcon,
@@ -36,7 +34,6 @@ import {
RiCloudy2Line,
RiMore2Line,
} from '@remixicon/react';
-import { HeaderBreadcrumb } from '../Header/types';
const meta = preview.meta({
title: 'Backstage UI/PluginHeader',
@@ -82,24 +79,6 @@ const tabs: HeaderTab[] = [
},
];
-const tabs2: HeaderTab[] = [
- {
- id: 'Banana',
- label: 'Banana',
- href: '/banana',
- },
- {
- id: 'Apple',
- label: 'Apple',
- href: '/apple',
- },
- {
- id: 'Orange',
- label: 'Orange',
- href: '/orange',
- },
-];
-
const menuItems = [
{
label: 'Settings',
@@ -120,86 +99,6 @@ const menuItems = [
},
];
-const breadcrumbs: HeaderBreadcrumb[] = [
- {
- label: 'Home',
- href: '/',
- },
- {
- label: 'Dashboard',
- href: '/dashboard',
- },
- {
- label: 'Settings',
- href: '/settings',
- },
-];
-
-// Extract layout decorator as a reusable constant
-const layoutDecorator = [
- (Story: StoryFn) => (
- <>
-
-
-
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
- quos.
-
-
-
- >
- ),
- withRouter,
-];
-
export const Default = meta.story({
args: {},
decorators: [withRouter],
@@ -249,90 +148,6 @@ export const WithAllOptionsAndTabs = WithCustomActions.extend({
},
});
-export const WithHeader = meta.story({
- args: {
- ...WithAllOptionsAndTabs.input.args,
- },
- decorators: [withRouter],
- render: args => (
- <>
-
- } />
- } />
- } />
- >
- }
- />
- Custom action}
- breadcrumbs={breadcrumbs}
- />
- >
- ),
-});
-
-export const WithLayout = meta.story({
- decorators: layoutDecorator,
- render: args => (
- <>
-
- Custom action}
- breadcrumbs={breadcrumbs}
- />
- >
- ),
-});
-
-export const WithLayoutNoTabs = meta.story({
- decorators: layoutDecorator,
- render: args => (
- <>
-
-
- >
- ),
-});
-
-export const WithEverything = meta.story({
- args: {
- tabs,
- titleLink: '/',
- },
- decorators: layoutDecorator,
- render: args => (
- <>
-
- } />
- } />
- } />
- >
- }
- />
-
-
-
- >
- }
- />
- >
- ),
-});
-
export const WithMockedURLCampaigns = meta.story({
args: {
tabs,
@@ -341,7 +156,7 @@ export const WithMockedURLCampaigns = meta.story({
-
+
Current URL is mocked to be: /campaigns
@@ -363,7 +178,7 @@ export const WithMockedURLIntegrations = meta.story({
-
+
Current URL is mocked to be: /integrations
@@ -385,7 +200,7 @@ export const WithMockedURLNoMatch = meta.story({
-
+
Current URL is mocked to be: /some-other-page
@@ -435,7 +250,7 @@ export const WithTabsMatchingStrategies = meta.story({
-
+
Current URL: /mentorship/events
@@ -490,7 +305,7 @@ export const WithTabsExactMatching = meta.story({
-
+
Current URL: /mentorship/events
@@ -534,7 +349,7 @@ export const WithTabsPrefixMatchingDeep = meta.story({
-
+
Current URL: /catalog/users/john/details
diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx
index 2414e22975..9cc3161ef7 100644
--- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx
+++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx
@@ -17,12 +17,13 @@
import type { PluginHeaderProps } from './types';
import { Tabs, TabList, Tab } from '../Tabs';
import { useDefinition } from '../../hooks/useDefinition';
+import { useBgProvider, BgProvider } from '../../hooks/useBg';
import { PluginHeaderDefinition } from './definition';
import { type NavigateOptions } from 'react-router-dom';
import { Children, useMemo, useRef } from 'react';
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
import { Box } from '../Box';
-import { Link } from 'react-aria-components';
+import { Link } from '../Link';
import { RiShapesLine } from '@remixicon/react';
import { Text } from '../Text';
@@ -33,8 +34,9 @@ declare module 'react-aria-components' {
}
/**
- * A component that renders a plugin header with icon, title, custom actions,
- * and navigation tabs.
+ * Renders a plugin header with icon, title, custom actions, and optional tabs.
+ * Always participates in the background context system so descendants (e.g. buttons)
+ * get the correct `data-on-bg` styling inside the toolbar and tabs.
*
* @public
*/
@@ -50,11 +52,9 @@ export const PluginHeader = (props: PluginHeaderProps) => {
onTabSelectionChange,
} = ownProps;
+ const providerBg = useBgProvider('neutral');
const hasTabs = tabs && tabs.length > 0;
const headerRef = useRef(null);
- const toolbarWrapperRef = useRef(null);
- const toolbarContentRef = useRef(null);
- const toolbarControlsRef = useRef(null);
const animationFrameRef = useRef(undefined);
const lastAppliedHeightRef = useRef(undefined);
@@ -123,37 +123,29 @@ export const PluginHeader = (props: PluginHeaderProps) => {
};
}, []);
- const titleContent = (
- <>
-
- {icon || }
-
- {title || 'Your plugin'}
- >
- );
+ const titleText = title || 'Your plugin';
- return (
-
+ const inner = (
+ <>
-
-
-
- {titleLink ? (
-
- {titleContent}
-
- ) : (
- {titleContent}
- )}
-
-
-
- {actionChildren}
-
+
+
{icon || }
+
+ {titleLink ? (
+
+ {titleText}
+
+ ) : (
+
+ {titleText}
+
+ )}
+
+
{actionChildren}
{tabs && (
-
+
{tabs?.map(tab => (
@@ -170,6 +162,14 @@ export const PluginHeader = (props: PluginHeaderProps) => {
)}
+ >
+ );
+
+ const { bg: surfaceBg } = providerBg;
+
+ return (
+
+ {surfaceBg ? {inner} : inner}
);
};
diff --git a/packages/ui/src/components/PluginHeader/definition.ts b/packages/ui/src/components/PluginHeader/definition.ts
index c9a35aa956..66414efbee 100644
--- a/packages/ui/src/components/PluginHeader/definition.ts
+++ b/packages/ui/src/components/PluginHeader/definition.ts
@@ -27,7 +27,6 @@ export const PluginHeaderDefinition = defineComponent()({
classNames: {
root: 'bui-PluginHeader',
toolbar: 'bui-PluginHeaderToolbar',
- toolbarWrapper: 'bui-PluginHeaderToolbarWrapper',
toolbarContent: 'bui-PluginHeaderToolbarContent',
toolbarControls: 'bui-PluginHeaderToolbarControls',
toolbarIcon: 'bui-PluginHeaderToolbarIcon',
diff --git a/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx
new file mode 100644
index 0000000000..b3fc2efc57
--- /dev/null
+++ b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import preview from '../../../../.storybook/preview';
+import type { StoryFn } from '@storybook/react-vite';
+import { MemoryRouter } from 'react-router-dom';
+import { BUIProvider } from '../provider';
+import {
+ PluginHeader,
+ Header,
+ Button,
+ ButtonIcon,
+ Card,
+ Container,
+ Flex,
+ MenuTrigger,
+ Menu,
+ MenuItem,
+} from '..';
+import {
+ RiBookOpenLine,
+ RiBox3Line,
+ RiCodeSSlashLine,
+ RiDownloadLine,
+ RiEdit2Line,
+ RiGitBranchLine,
+ RiMore2Line,
+ RiPlayLine,
+ RiRefreshLine,
+ RiSettings4Line,
+ RiShieldCheckLine,
+ RiShareBoxLine,
+ RiTerminalLine,
+} from '@remixicon/react';
+
+// ---------------------------------------------------------------------------
+// Shared page content placeholder
+// ---------------------------------------------------------------------------
+
+const PageContent = () => (
+
+
+
+
+
+
+
+);
+
+// ---------------------------------------------------------------------------
+// Shared layout decorator
+// ---------------------------------------------------------------------------
+
+const withLayout = (Story: StoryFn) => (
+
+
+
+
+
+);
+
+// ---------------------------------------------------------------------------
+// Meta
+// ---------------------------------------------------------------------------
+
+const meta = preview.meta({
+ title: 'Recipes/PluginHeader and Header',
+ parameters: {
+ layout: 'fullscreen',
+ },
+});
+
+// ---------------------------------------------------------------------------
+// Story: Catalog entity page
+// ---------------------------------------------------------------------------
+
+export const CatalogEntityPage = meta.story({
+ decorators: [withLayout],
+ render: () => (
+ <>
+ }
+ title="Catalog"
+ titleLink="/"
+ tabs={[
+ { id: 'catalog', label: 'Catalog', href: '/catalog' },
+ { id: 'apis', label: 'APIs', href: '/apis' },
+ { id: 'resources', label: 'Resources', href: '/resources' },
+ { id: 'templates', label: 'Templates', href: '/templates' },
+ { id: 'docs', label: 'Docs', href: '/docs' },
+ ]}
+ customActions={
+ <>
+ }
+ aria-label="Settings"
+ />
+
+ }
+ aria-label="More options"
+ />
+
+
+ >
+ }
+ />
+
+ }>
+ Edit
+
+ }>
+ Unregister
+
+ >
+ }
+ />
+
+ >
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// Story: CI/CD pipeline view
+// ---------------------------------------------------------------------------
+
+export const CICDPipelineView = meta.story({
+ decorators: [withLayout],
+ render: () => (
+ <>
+ }
+ title="CI/CD"
+ titleLink="/"
+ tabs={[
+ { id: 'builds', label: 'Builds', href: '/builds' },
+ { id: 'pipelines', label: 'Pipelines', href: '/pipelines' },
+ { id: 'deployments', label: 'Deployments', href: '/deployments' },
+ { id: 'settings', label: 'Settings', href: '/settings' },
+ ]}
+ customActions={
+ <>
+ }
+ aria-label="Refresh"
+ />
+ >
+ }
+ />
+
+ }>
+ Download logs
+
+ }>
+ Re-run pipeline
+
+ >
+ }
+ />
+
+ >
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// Story: TechDocs page
+// ---------------------------------------------------------------------------
+
+export const TechDocsPage = meta.story({
+ decorators: [withLayout],
+ render: () => (
+ <>
+ }
+ title="TechDocs"
+ titleLink="/"
+ tabs={[
+ { id: 'explore', label: 'Explore', href: '/explore' },
+ { id: 'owned', label: 'Owned by me', href: '/owned' },
+ { id: 'starred', label: 'Starred', href: '/starred' },
+ ]}
+ />
+
+ }
+ aria-label="More options"
+ />
+
+
+ }
+ />
+
+ >
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// Story: Security / compliance audit page
+// ---------------------------------------------------------------------------
+
+export const SecurityAuditPage = meta.story({
+ decorators: [withLayout],
+ render: () => (
+ <>
+ }
+ title="Security"
+ titleLink="/"
+ tabs={[
+ { id: 'overview', label: 'Overview', href: '/overview' },
+ { id: 'vulnerabilities', label: 'Vulnerabilities', href: '/vulns' },
+ { id: 'policies', label: 'Policies', href: '/policies' },
+ { id: 'audits', label: 'Audits', href: '/audits' },
+ ]}
+ customActions={
+ <>
+ }
+ aria-label="Refresh scan"
+ />
+ }>
+ Run scan
+
+ >
+ }
+ />
+
+ }>
+ Export report
+
+ >
+ }
+ />
+
+ >
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// Story: Minimal — no tabs, no actions
+// ---------------------------------------------------------------------------
+
+export const Minimal = meta.story({
+ decorators: [withLayout],
+ render: () => (
+ <>
+ } title="APIs" titleLink="/" />
+
+
+ >
+ ),
+});