diff --git a/.changeset/grumpy-experts-leave.md b/.changeset/grumpy-experts-leave.md
new file mode 100644
index 0000000000..7504fe6428
--- /dev/null
+++ b/.changeset/grumpy-experts-leave.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-app': patch
+---
+
+Fixed a regression that caused disabled nav items to appear in the navigation bar.
diff --git a/plugins/app/src/extensions/AppNav.test.tsx b/plugins/app/src/extensions/AppNav.test.tsx
new file mode 100644
index 0000000000..387c75a87a
--- /dev/null
+++ b/plugins/app/src/extensions/AppNav.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2026 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 { screen, waitFor, within } from '@testing-library/react';
+import { renderTestApp } from '@backstage/frontend-test-utils';
+import {
+ PageBlueprint,
+ NavItemBlueprint,
+ createRouteRef,
+} from '@backstage/frontend-plugin-api';
+
+const DEFAULT_CONFIG = {
+ app: { baseUrl: 'http://localhost:3000' },
+ backend: { baseUrl: 'http://localhost:7007' },
+};
+
+const mockPage = PageBlueprint.make({
+ name: 'my-plugin',
+ params: {
+ title: 'My Plugin',
+ icon: icon,
+ path: '/my-plugin',
+ routeRef: createRouteRef(),
+ },
+});
+
+const mockNavItem = NavItemBlueprint.make({
+ name: 'my-plugin',
+ params: {
+ title: 'My Plugin',
+ icon: () => icon,
+ routeRef: createRouteRef(),
+ },
+});
+
+describe('AppNav', () => {
+ it('should show a nav item for a page with an enabled nav-item extension', async () => {
+ renderTestApp({
+ extensions: [mockPage, mockNavItem],
+ config: DEFAULT_CONFIG,
+ });
+
+ await waitFor(() => {
+ expect(
+ within(screen.getByRole('navigation')).getByText('My Plugin'),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should hide a nav item when its nav-item extension is disabled via config', async () => {
+ renderTestApp({
+ extensions: [mockPage, mockNavItem],
+ config: {
+ ...DEFAULT_CONFIG,
+ app: {
+ ...DEFAULT_CONFIG.app,
+ extensions: [{ 'nav-item:test/my-plugin': false }],
+ },
+ },
+ });
+
+ await waitFor(() => {
+ expect(
+ within(screen.getByRole('navigation')).queryByText('My Plugin'),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ it('should still show a nav item for a page without a nav-item extension', async () => {
+ renderTestApp({
+ extensions: [mockPage],
+ config: {
+ ...DEFAULT_CONFIG,
+ app: {
+ ...DEFAULT_CONFIG.app,
+ extensions: [{ 'nav-item:test/my-plugin': false }],
+ },
+ },
+ });
+
+ await waitFor(() => {
+ expect(
+ within(screen.getByRole('navigation')).getByText('My Plugin'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx
index 09203303e5..0d6618fc8c 100644
--- a/plugins/app/src/extensions/AppNav.tsx
+++ b/plugins/app/src/extensions/AppNav.tsx
@@ -188,6 +188,14 @@ function NavContentRenderer(props: {
return [];
}
+ // TODO: Nav items are deprecated, and this code covers for a documented ability to use nav items still in config to disable pages in the sidebar.
+ // Remove this code once nav items are completely gone from the codebase.
+ const navItemNodeId = node.spec.id.replace(/^page:/, 'nav-item:');
+ const navItemNode = tree.nodes.get(navItemNodeId);
+ if (navItemNode?.spec.disabled) {
+ return [];
+ }
+
const routeRef = node.instance.getData(coreExtensionData.routeRef);
if (!routeRef) {
return [];