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?

1 Like

Thats a good idea, I’ll look into that! Ive already got tabs for switching pages, so hopefully they won’t cause issues.

@AnnMarieW Using cards ended up working best, I used cards generating in the same place as the drawers, keeping the function that generates the content within the drawer/card, and just swapping the buttons from opening drawers to making cards appear. Switching to cards also made the function that swapped z-indices work!

For whomever may follow in my footsteps, here’s the code of cards that mimicked drawers successfully:


app.layout = dmc.MantineProvider([
    dcc.Store(id="last-opened", data=""), #to remember top card instead of zindex shenanigans
    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 = True,
        ),
        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.Card(
            id="error-card",
            children=html.Div(id="error_check"),
            withBorder=True,
            shadow="xl",
            style={
                "position": "fixed",
                "bottom": "0px",
                "left": "0px",
                "width": "100vw",
                "height": "50vh",
                "zIndex": 999,
                "display": "none",
                "backgroundColor": "white",
            }
        ),
        dmc.Card(
            id="event-card",
            children=html.Div(id="event_table"),
            withBorder=True,
            shadow="xl",
            style={
                "position": "fixed",
                "right": "0px",
                "width": "50vw",
                "height": "100vh",
                "zIndex": 998,
                "display": "none",
                "backgroundColor": "white",
            }
        ),

        dcc.Store(id='window-size', data=1000),

        dcc.Interval(
            id='interval-component',
            interval=5 * 1000,  # 10 seconds
            n_intervals=0
        ),
        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,
        ),

        dmc.Tabs(
            id="tabs",
            value="1",  # default selected tab
            children=[
                # Tab content first
                dmc.TabsPanel(
                    html.Div([
                        graphpow,

                        html.Div(
                            [graphcurry, graphpf],
                            style={'display': 'flex', 'flexDirection': 'row', 'marginTop': '1vh'}
                        ),
                    ]),
                    value="1"
                ),

                dmc.TabsPanel(
                    children=[ 
                        html.Div(
                            [graphmoola, graphdough],
                            style={'display': 'flex', 'flexDirection': 'row', 'marginTop': '1vh', 'paddingLeft': '3vw', 'paddingRight': '3vw'}
                        ),
                        html.Div(id="trees", style={"textAlign": "center", "marginTop": "1vh", "paddingLeft": "0vw", "paddingRight": "0vw"}),
                        html.Div(style={"height": "25vh"})  
                    ],
                    value="2"
                ),

                # Tabs list (the clickable tab buttons) at the bottom
                dmc.TabsList(
                    [
                        dmc.TabsTab("Main Graphs", value="1"),
                        dmc.TabsTab("New Stuff", value="2"),
                    ],
                    style={
                        "marginTop": "4rem",         # push the tabs visually down
                        "position": "relative",
                        "bottom": "0"
                    }
                ),
                html.Div(
                    html.Button('Manually Refresh', id='manual-refresh-button'),
                    style={
                        'textAlign': 'center',
                        'marginTop': '4rem',
                        'marginBottom': '2rem'
                    }
                ),

            ],
            style={
                "minHeight": "120vh",  # Ensure scroll space
                "paddingBottom": "2rem"
            }
        )
    ])
])
...
#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 True
    return not (power_offline or current_offline or powfact_offline)


#ERROR BUTTON FLASHING
@callback(
    Output("warning-button", "className"),
    Input("flash-interval", "n_intervals"),
    prevent_initial_call=False
)
def flash_error_icon(n_intervals):
    return "tdesign--error-triangle" if n_intervals % 2 == 0 else "tdesign--error-triangle-FLASH"

@callback(
    Output("error-card", "style"),
    Output("last-opened", "data"),
    Input("warning-button", "n_clicks"),
    State("error-card", "style"),
    prevent_initial_call=True
)
def toggle_error_card(n_clicks, current_style):
    if current_style["display"] == "none":
        return {**current_style, "display": "block"}, "error"
    else:
        return {**current_style, "display": "none"}, ""

@callback(
    Output("event-card", "style"),
    Output("last-opened", "data", allow_duplicate=True),
    Input("event-button", "n_clicks"),
    State("event-card", "style"),
    prevent_initial_call=True
)
def toggle_event_card(n_clicks, current_style):
    if current_style["display"] == "none":
        return {**current_style, "display": "block"}, "event"
    else:
        return {**current_style, "display": "none"}, ""

#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("warning-button", "n_clicks"),
    prevent_initial_call=True
)
def update_error_card_content(n_clicks):
    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"),
    ])


def generate_logs(data, columns):
    # Define the columns for dash_table.DataTable with IDs and display names
    dt_columns = [{'name': col, 'id': col} for col in columns]
    today_str = datetime.now().strftime('%Y-%m-%d')

    return dash_table.DataTable(
        data=data,
        columns=dt_columns,
        style_cell={
            'padding': '5px',
            'textAlign': 'left',
            'fontSize': '12px',
            'whiteSpace': 'normal',
            'height': 'auto',
        },
        style_cell_conditional=[
            {'if': {'column_id': 'Time'}, 'width': '150px', 'fontSize': '10px'},
            {'if': {'column_id': 'Event Type'}, 'width': '120px', 'fontSize': '14px'},
            {'if': {'column_id': 'Details'}, 'width': 'auto', 'fontSize': '10px'},
        ],
        style_data={
            'color': 'black',
            'backgroundColor': 'white'
        },
        style_data_conditional=[
            {
                'if': {'row_index': 'odd'},
                'backgroundColor': 'rgb(220, 220, 220)',
            },
            {
                'if': {
                        'filter_query': f'{{Time}} contains "{today_str}"',
                        'column_id': 'Time',
                    },
                    'backgroundColor': '#ffefc1',
            }
        ],
        style_header={
            'backgroundColor': 'rgb(210, 210, 210)',
            'color': 'black',
            'fontWeight': 'bold',
            'fontSize': '15px',
        },

        page_size=50, 
        sort_action='native',
        filter_action='native',
        # Add other DataTable props if needed
    )
@callback(
    Output("event_table", "children"),
    Input("event-button", "n_clicks"),
    prevent_initial_call=True
)
def update_log_content(n_clicks):
    try:
        conn = sqlite3.connect('event_log.db', timeout=5, check_same_thread=False)
        cursor = conn.cursor()
        cursor.execute('SELECT TIME, EVENT_TYPE, EVENT_DEETS FROM events ORDER BY TIME DESC LIMIT 50')
        rows = cursor.fetchall()
        conn.close()

        data = [{
            'Time': row[0],
            'Event Type': row[1],
            'Details': row[2]
        } for row in rows]

        columns = ['Time', 'Event Type', 'Details']
        return html.Div([generate_logs(data, columns)])

    except Exception as e:
        return html.Div(f"Error loading event log: {str(e)}")

        #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-card", "style", allow_duplicate=True),
    Output("event-card", "style", allow_duplicate=True),
    Input("last-opened", "data"),
    State("error-card", "style"),
    State("event-card", "style"),
    prevent_initial_call=True
)
def update_z_index(last_opened, error_style, event_style):
    if last_opened == "error":
        return {**error_style, "zIndex": 1001}, {**event_style, "zIndex": 999}
    elif last_opened == "event":
        return {**error_style, "zIndex": 999}, {**event_style, "zIndex": 1001}
    return error_style, event_style

1 Like