How to Control Drawer Overlap?

Hello all! Im facing an issue where two drawers that are being opened concurrently have fixed orders, where one is always layered on top of the other, regardless of the order in which they were opened. I tried to write a callback that would manually reassign zIndex values to the drawers on click, but it flat out refuses to work. Any advice as to how I could make the most recently clicked drawer sit on top of the earlier called drawer would be appreciated!

Here is the screenshot of overlapping drawers, where no matter what order I press the buttons in, the event log drawer is in the front.

Here are my versions: dash version: 3.1.1, dcc version: 3.1.0, html version: 3.0.3, dash table version: 6.0.3

import dash
from dash import State, callback, dcc, html, Dash, Input, Output, dash_table
from dash_iconify import DashIconify
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc

from plotly.graph_objs import Figure
from plotly.subplots import make_subplots
import plotly.graph_objs as go

import plotly.io as pio

app = Dash(__name__, suppress_callback_exceptions=True)

app.layout = dmc.MantineProvider(
    html.Div([
        html.Div([
            html.Button(
                id="warning-button",
                className="tdesign--error-triangle",  # Default icon
                n_clicks=0,  # Needed for callback
                style={
                    "position": "fixed",
                    "top": "10px",
                    "left": "10px",
                    "zIndex": 9999,
                    "backgroundColor": "black",
                    "padding": "10px",
                    "borderRadius": "5px",
                    "border": "white",
                    "boxShadow": "0 0 10px rgba(0,0,0,0.3)",
                    "width": "15vh",
                    "height": "15vh",
                    "cursor": "pointer"
                }
            )
        ], id="button-wrap", hidden=False),

        html.Button(
            id="event-button",
            className="mingcute--paper-line",  
            n_clicks=0,  # Needed for callback
            style={
                "position": "fixed",
                "bottom": "0px",
                "left": "0px",
                "zIndex": 9999,
                "backgroundColor": "black",
                "padding": "10px",
                "borderRadius": "5px",
                "border": "white",
                "boxShadow": "0 0 10px rgba(0,0,0,0.3)",
                "width": "7.5vh",
                "height": "7.5vh",
                "cursor": "pointer"
            }
        ),

        dmc.Drawer(  # this thing is to show all the error info.
            title="Error Page",
            id="error-page",
            padding="md",
            opened=False,  
            size="50%",
            position="bottom",
            # style={"position": "fixed"},
            children=[
                html.Div(id="error_check"),
            ]
        ),

        dmc.Drawer(  # this to display the event log
            title="Event Log",
            id="event-page",
            padding="md",
            opened=False,  
            size="30%",
            position="right",
            # style={"position": "fixed"},
            children=[
                html.Div(id="event_table"),
            ]
        ),

        dcc.Interval(
            id="tab-interval",
            interval=1 * 60 * 1000,  # 5 minutes
            n_intervals=0,
        ),

        dcc.Interval(
            id="flash-interval",
            interval=0.2 * 1000, 
            n_intervals=0,
        )
    ])
)

app.clientside_callback( #asks for window width every interval
     """
    function(n_intervals) {
        if(typeof window !== 'undefined' && window.innerWidth) {
            return window.innerWidth;
        } else {
            return 1000;  // fallback width
        }
    }
    """,
    Output('window-size', 'data'),
    Input('interval-component', 'n_intervals')
)

power_offline = True #so that the error button shows up

#ERROR BUTTON APPEARANCE
@callback(
    Output("button-wrap", "hidden"),
    Input("flash-interval", "n_intervals"),
    prevent_initial_call=False
)
def show_error_icon(n_intervals):
    print("Offline status check:", power_offline, current_offline, powfact_offline)
    return False
    #return not (power_offline or current_offline or powfact_offline)

#ERROR PAGE (DRAWER)
@callback(
    Output("error-page", "opened"),
    Input("warning-button", "n_clicks"),
    State("error-page", "opened"),
    prevent_initial_call=True
)
def toggle_drawer(n_clicks, is_open):
    if n_clicks is None:
        raise dash.exceptions.PreventUpdate
    return not is_open

#updates the error info that needs to be shown
POWmissing_boards = ["Waiting..."]
CURmissing_boards = ["Waiting..."]
PFmissing_boards = ["Waiting..."]
power_offline = False
current_offline = False
powfact_offline = False
danger_current = ["No dangerous level of current"]

@callback(
    Output("error_check", "children"),
    Input("error-page", "opened"),
    prevent_initial_call=True
)
def update_drawer_content(is_open):
    if not is_open:
        raise dash.exceptions.PreventUpdate
    return html.Div([
        html.H4("System Error Information:"),
        html.P(f"Power Data Offline for Boards {POWmissing_boards}"),
        html.P(f"Current Data Offline for Boards {CURmissing_boards}"),
        html.P(f"Power Factor Data Offline for Boards {PFmissing_boards}"),
        html.P(f"The current level is {danger_current} amps"),
    ])

#EVENT LOG (Drawer) switch
@callback(
    Output("event-page", "opened"),
    Input("event-button", "n_clicks"),
    State("event-page", "opened"),
    prevent_initial_call=True
)
def toggle_log(n_clicks, is_open):
    if n_clicks is None:
        raise dash.exceptions.PreventUpdate
    return not is_open

def generate_logs(data, columns):
    return html.Table([
        html.Thead(
            html.Tr([html.Th(col) for col in columns])
        ),
        html.Tbody([
            html.Tr([
                html.Td(row[col]) for col in columns
            ]) for row in data
        ])
    ], style={'width': '100%', 'border': '1px solid black', 'borderCollapse': 'collapse', 'padding': '8px'})

@callback(
    Output("event_table", "children"),
    Input("event-page", "opened"),
    prevent_initial_call=True
)
def update_log_content(is_open):
    if not is_open:
        raise dash.exceptions.PreventUpdate
    data = [
        {'Board': 'Board 1', 'Status': 'Offline', 'Last Update': '2025-07-17 10:00'},
        {'Board': 'Board 2', 'Status': 'Online', 'Last Update': '2025-07-17 10:05'},
        {'Board': 'Board 3', 'Status': 'Offline', 'Last Update': '2025-07-17 09:50'},
    ]
    columns = ['Board', 'Status', 'Last Update']

    return html.Div([
        html.H4("System Error Information:"),
        generate_logs(data, columns),
        # You can add more info below if you want
    ])
        #table with the timestamp, an ID of what happened, and what that ID means (abnormal value, returns value, null, 0,)

#MAK THE MOST RECENT CLICKED DRAWER SHOW UP IN FRONT
@callback(
    Output("error-page", "style"),
    Output("event-page", "style"),
    Input("error-page", "opened"),
    Input("event-page", "opened"),
    prevent_initial_call=True
)
def prioritize_drawers(error_open, event_open):
    if error_open and not event_open:
        return {"zIndex": 1001}, {"zIndex": 1000}
    elif event_open and not error_open:
        return {"zIndex": 1000}, {"zIndex": 1001}
    elif error_open and event_open:
        return {"zIndex": 1000}, {"zIndex": 1001}  # Or swap depending on desired stacking
    return {"zIndex": 1000}, {"zIndex": 1000}


if __name__ == '__main__':
    print("dash version:", dash.__version__), print("dcc version:", dcc.__version__), print("html version:", html.__version__), print("dash table version:", dash_table.__version__), 
    app.run(port=8055, debug=True, use_reloader=False)

And while I doubt it has any effect on the buttons, just in case, here’s the css file for icons.

.tdesign--error-triangle {
  display: flex;
  align-items: center;
  justify-content: center;
  /* width: 40px; 
  height: 40px; */
  background-repeat: no-repeat;
  background-size: 80% 80%; /* can be % or pixels */
  background-position: center;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fcf000' d='m12 1l11.951 20.7H.05zM3.513 19.7h16.974L12 5zM13 9.5V15h-2V9.5zm-2 7h2.004v2.004H11z' stroke-width='0.5' stroke='%23ff0909'/%3E%3C/svg%3E");
}

.tdesign--error-triangle-FLASH {
  display: flex;
  align-items: center;
  justify-content: center;
  /* width: 40px; 
  height: 40px; */
  background-repeat: no-repeat;
  background-size: 80% 80%; /* can be % or pixels */
  background-position: center;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0909' d='m12 1l11.951 20.7H.05zM3.513 19.7h16.974L12 5zM13 9.5V15h-2V9.5zm-2 7h2.004v2.004H11z' stroke-width='0.5' stroke='%23fcf000'/%3E%3C/svg%3E");
}

.mingcute--paper-line {
  display: flex;
  align-items: center;
  justify-content: center;
  /* width: 40px; 
  height: 40px; */
  background-repeat: no-repeat;
  background-size: 80% 80%; /* can be % or pixels */
  background-position: center;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none'%3E%3Cpath d='m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z'/%3E%3Cpath fill='%23e6feff' d='M16 3a3 3 0 0 1 2.995 2.824L19 6v10h.75c.647 0 1.18.492 1.244 1.122l.006.128V19a3 3 0 0 1-2.824 2.995L18 22H8a3 3 0 0 1-2.995-2.824L5 19V9H3.25a1.25 1.25 0 0 1-1.244-1.122L2 7.75V6a3 3 0 0 1 2.824-2.995L5 3zm0 2H7v14a1 1 0 1 0 2 0v-1.75c0-.69.56-1.25 1.25-1.25H17V6a1 1 0 0 0-1-1m3 13h-8v1c0 .35-.06.687-.17 1H18a1 1 0 0 0 1-1zm-7-6a1 1 0 1 1 0 2h-2a1 1 0 1 1 0-2zm2-4a1 1 0 1 1 0 2h-4a1 1 0 0 1 0-2zM5 5a1 1 0 0 0-.993.883L4 6v1h1z' stroke-width='0.5' stroke='%2300fff6'/%3E%3C/g%3E%3C/svg%3E");
}

Hi @Pine_Owple

Try the new DrawerStack component available in DMC 2.1.0

Use DrawerStack component to render multiple drawers at the same time. DrawerStack keeps track of opened drawers, manages z-index values, focus trapping and closeOnEscape behavior.

This image is from the ModalStack, but the DrawerStack works the same way

1 Like

Thank you! That solved the layering issue, but it seems to have caused a couple new ones. Namely, I can no longer see the earlier drawer when I click onto the second one, and I can’t find a way to override these aspects of Drawer Stack: “Drawers that are not currently visible are present in the DOM but are hidden with opacity: 0 and pointer-events: none” and “Only one overlay is rendered at a time”. I tried manually changing the opacity in the app layout, and in the css file, but neither worked. Do you know whether its possible to change the Drawer Stack so that both drawers are visible at once, even if overlapping and only one is “in focus” at the front?

Css code I tried:

.mantine-Drawer-overlay {
  background-color: rgba(0, 0, 0, 0.3) !important; /* less dark */
  pointer-events: auto;
}
div[class*="Drawer-overlay"] {
  background-color: rgba(0, 0, 0, 0.3) !important;
}
div[class*="ManagedDrawer-root"][style*="opacity: 0"] {
  opacity: 0.5 !important;
  pointer-events: none !important;
}

@Pine_Owple
Would the Tabs component work better for your use-case?