diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.js
new file mode 100644
index 00000000000..6ef9969bee5
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.js
@@ -0,0 +1,110 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { createTheme } from '@mui/material/styles';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import BarChartIcon from '@mui/icons-material/BarChart';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { Button } from '@mui/material';
+
+const NAVIGATION = [
+ {
+ segment: 'dashboard',
+ title: 'Dashboard',
+ icon: ,
+ },
+ {
+ segment: 'orders',
+ title: 'Orders',
+ icon: ,
+ },
+ {
+ segment: 'reports',
+ title: 'Reports',
+ icon: ,
+ },
+];
+
+const demoTheme = createTheme({
+ cssVariables: {
+ colorSchemeSelector: 'data-toolpad-color-scheme',
+ },
+ colorSchemes: { light: true, dark: true },
+ breakpoints: {
+ values: {
+ xs: 0,
+ sm: 600,
+ md: 600,
+ lg: 1200,
+ xl: 1536,
+ },
+ },
+});
+
+function DemoPageContent({ pathname, toggleSidebar }) {
+ return (
+
+ Dashboard content for {pathname}
+
+
+ );
+}
+
+DemoPageContent.propTypes = {
+ pathname: PropTypes.string.isRequired,
+ toggleSidebar: PropTypes.func.isRequired,
+};
+
+function DashboardLayoutSidebarCollapsedProp(props) {
+ const { window } = props;
+
+ const [pathname, setPathname] = React.useState('/dashboard');
+ const [navigationMenuOpen, toggleSidebar] = React.useState(true);
+ const router = React.useMemo(() => {
+ return {
+ pathname,
+ searchParams: new URLSearchParams(),
+ navigate: (path) => setPathname(String(path)),
+ };
+ }, [pathname]);
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+
+
+ toggleSidebar(!navigationMenuOpen)}
+ />
+
+
+ );
+}
+
+DashboardLayoutSidebarCollapsedProp.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default DashboardLayoutSidebarCollapsedProp;
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx
new file mode 100644
index 00000000000..f00eadeb905
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx
@@ -0,0 +1,112 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { createTheme } from '@mui/material/styles';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import BarChartIcon from '@mui/icons-material/BarChart';
+import {
+ AppProvider,
+ type Router,
+ type Navigation,
+} from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { Button } from '@mui/material';
+
+const NAVIGATION: Navigation = [
+ {
+ segment: 'dashboard',
+ title: 'Dashboard',
+ icon: ,
+ },
+ {
+ segment: 'orders',
+ title: 'Orders',
+ icon: ,
+ },
+ {
+ segment: 'reports',
+ title: 'Reports',
+ icon: ,
+ },
+];
+
+const demoTheme = createTheme({
+ cssVariables: {
+ colorSchemeSelector: 'data-toolpad-color-scheme',
+ },
+ colorSchemes: { light: true, dark: true },
+ breakpoints: {
+ values: {
+ xs: 0,
+ sm: 600,
+ md: 600,
+ lg: 1200,
+ xl: 1536,
+ },
+ },
+});
+
+function DemoPageContent({
+ pathname,
+ toggleSidebar,
+}: {
+ pathname: string;
+ toggleSidebar: () => void;
+}) {
+ return (
+
+ Dashboard content for {pathname}
+
+
+ );
+}
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function DashboardLayoutSidebarCollapsedProp(props: DemoProps) {
+ const { window } = props;
+
+ const [pathname, setPathname] = React.useState('/dashboard');
+ const [navigationMenuOpen, toggleSidebar] = React.useState(true);
+ const router = React.useMemo(() => {
+ return {
+ pathname,
+ searchParams: new URLSearchParams(),
+ navigate: (path) => setPathname(String(path)),
+ };
+ }, [pathname]);
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+
+
+ toggleSidebar(!navigationMenuOpen)}
+ />
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx.preview
new file mode 100644
index 00000000000..3e5869c9ede
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutSidebarCollapsedProp.tsx.preview
@@ -0,0 +1,6 @@
+
+ toggleSidebar(!navigationMenuOpen)}
+ />
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
index dc46918b677..b27e92c67dd 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
+++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
@@ -126,6 +126,12 @@ The layout sidebar can be hidden if needed with the `hideNavigation` prop.
{{"demo": "DashboardLayoutSidebarHidden.js", "height": 400, "iframe": true}}
+### Toggle sidebar
+
+The sidebar can be toggled if needed with the `navigationMenuOpen` prop.
+
+{{"demo": "DashboardLayoutSidebarCollapsedProp.js", "height": 400, "iframe": true}}
+
## Full-size content
The layout content can take up the full available area with styles such as `flex: 1` or `height: 100%`.
diff --git a/docs/pages/toolpad/core/api/dashboard-layout.json b/docs/pages/toolpad/core/api/dashboard-layout.json
index fb900ae907a..217260c8f13 100644
--- a/docs/pages/toolpad/core/api/dashboard-layout.json
+++ b/docs/pages/toolpad/core/api/dashboard-layout.json
@@ -11,6 +11,9 @@
"defaultSidebarCollapsed": { "type": { "name": "bool" }, "default": "false" },
"disableCollapsibleSidebar": { "type": { "name": "bool" }, "default": "false" },
"hideNavigation": { "type": { "name": "bool" }, "default": "false" },
+ "navigationMenuOpen": { "type": { "name": "bool" }, "default": "false" },
+ "onNavigationMenuClose": { "type": { "name": "func" } },
+ "onNavigationMenuOpen": { "type": { "name": "func" } },
"sidebarExpandedWidth": {
"type": { "name": "union", "description": "number
| string" },
"default": "320"
diff --git a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json
index 3cd8c58bad6..c95c460fb05 100644
--- a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json
+++ b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json
@@ -12,6 +12,15 @@
"hideNavigation": {
"description": "Whether the navigation bar and menu icon should be hidden"
},
+ "navigationMenuOpen": {
+ "description": "A prop that controls the collapsed state of the sidebar."
+ },
+ "onNavigationMenuClose": {
+ "description": "Callback function to be executed on navigation menu state changes to closed"
+ },
+ "onNavigationMenuOpen": {
+ "description": "Callback function to be executed on navigation menu state changes to open"
+ },
"sidebarExpandedWidth": { "description": "Width of the sidebar when expanded." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." },
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
index 8881519cc84..2d7b6f4f411 100644
--- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
@@ -410,4 +410,62 @@ describe('DashboardLayout', () => {
// Ensure that main content is still rendered
expect(screen.getByText('Hello world')).toBeTruthy();
});
+
+ test('renders the sidebar in collapsed state when navigationMenuOpen is false', () => {
+ render(
+
+ Test Content
+ ,
+ );
+
+ // Expect that menu button has expand action
+ expect(screen.getAllByLabelText('Expand menu')).toBeTruthy();
+ expect(screen.queryByLabelText('Collapse menu')).toBeNull();
+ });
+
+ test('renders the sidebar in expanded state when navigationMenuOpen is true', () => {
+ render(
+
+ Test Content
+ ,
+ );
+
+ expect(screen.getAllByLabelText('Collapse menu')).toBeTruthy();
+ });
+
+ test('calls onNavigationMenuOpen callback when navigationMenuOpen state changes to open', () => {
+ const mockToggleSidebar = vi.fn();
+ const { rerender } = render(
+
+ Test Content
+ ,
+ );
+
+ // Trigger sidebar open action
+ rerender(
+
+ Test Content
+ ,
+ );
+
+ expect(mockToggleSidebar).toHaveBeenCalledOnce();
+ });
+
+ test('calls onNavigationMenuClose callback when navigationMenuOpen state changes to close', () => {
+ const mockToggleSidebar = vi.fn();
+ const { rerender } = render(
+
+ Test Content
+ ,
+ );
+
+ // Trigger sidebar close action
+ rerender(
+
+ Test Content
+ ,
+ );
+
+ expect(mockToggleSidebar).toHaveBeenCalledOnce();
+ });
});
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
index da545108989..14ef364bd8d 100644
--- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
@@ -89,6 +89,18 @@ export interface DashboardLayoutProps {
* @default false
*/
hideNavigation?: boolean;
+ /** A prop that controls the collapsed state of the sidebar.
+ * @default false
+ */
+ navigationMenuOpen?: boolean;
+ /**
+ * Callback function to be executed on navigation menu state changes to open
+ */
+ onNavigationMenuOpen?: () => void;
+ /**
+ * Callback function to be executed on navigation menu state changes to closed
+ */
+ onNavigationMenuClose?: () => void;
/**
* Width of the sidebar when expanded.
* @default 320
@@ -131,6 +143,9 @@ function DashboardLayout(props: DashboardLayoutProps) {
slots,
slotProps,
sx,
+ navigationMenuOpen,
+ onNavigationMenuOpen,
+ onNavigationMenuClose,
} = props;
const theme = useTheme();
@@ -174,6 +189,17 @@ function DashboardLayout(props: DashboardLayoutProps) {
const [isNavigationFullyExpanded, setIsNavigationFullyExpanded] =
React.useState(isNavigationExpanded);
+ React.useEffect(() => {
+ if (typeof navigationMenuOpen === 'boolean') {
+ setIsNavigationExpanded(navigationMenuOpen);
+ if (navigationMenuOpen) {
+ onNavigationMenuOpen?.();
+ } else {
+ onNavigationMenuClose?.();
+ }
+ }
+ }, [navigationMenuOpen, setIsNavigationExpanded, onNavigationMenuOpen, onNavigationMenuClose]);
+
React.useEffect(() => {
if (isNavigationExpanded) {
const drawerWidthTransitionTimeout = setTimeout(() => {
@@ -483,6 +509,19 @@ DashboardLayout.propTypes /* remove-proptypes */ = {
* @default false
*/
hideNavigation: PropTypes.bool,
+ /**
+ * A prop that controls the collapsed state of the sidebar.
+ * @default false
+ */
+ navigationMenuOpen: PropTypes.bool,
+ /**
+ * Callback function to be executed on navigation menu state changes to closed
+ */
+ onNavigationMenuClose: PropTypes.func,
+ /**
+ * Callback function to be executed on navigation menu state changes to open
+ */
+ onNavigationMenuOpen: PropTypes.func,
/**
* Width of the sidebar when expanded.
* @default 320