Dash Crush, part 4: dynamic layout (II) – multiple cards

Post Contents

Welcome to Dash Crush, your walk-through tutorial to advanced interactive Data Viz dashboards using Dash Plotly.
In the previous part, we learned how to create a custom dbc.Card with a selection dropdown, an expand button in the header, and a collapsible body with dynamic content.

In this chapter, we will continue our adventure with dynamic layouts in Dash Plotly. Using the code from the previous chapter, we will learn how to handle not one but multiple cards: add them, remove them, and change their order.


In this chapter:

  1. How to add/remove content dynamically
  2. How to reorder components


The card represents a single layer of material and its properties. We aim to use multiple cards to define the geometry of a multilayered structure.

The code

We will reuse the code from the previous chapter. The crucial parts are the card component and callbacks. Therefore, I will describe the changes made on top of them below.

Install packages

Create and activate your virtual environment and install the following packages:

pip install dash pandas dash-bootstrap-components

The deck of cards

Let's name a collection of cards a deck. It will be a container with all the cards as its children. It will be located in src/layout/deck.py.

# src/layout/deck.py

from dash import html

def deck():
    return html.Div(children=[], id="deck")

Before we add anything to it, it's just an empty div.

The "Add" button

To populate a deck, we need an option for the user to add a new layer. It will be an "Add" button located in the Button bar. The definition will be located in src/layout/add_bar.py.

# src/layout/add_bar.py

def add_bar():
    """Add button bar"""
    return dbc.Row(
                        [html.I(className="fi-plus"), "Layer"],
                        style={"width": "100%"},
                width={"size": 6, "offset": 6},

The spacer

In a similar way as in one of the past chapters, we define the spacer component in src/layout/grid.py:

# src/layout/grid.py

import dash_bootstrap_components as dbc

def spacer(height):
    """Add empty row"""
    return dbc.Row(style={"height": f"{height}px"})

The layout

We modify the top-level layout to contain the "Add" button and the deck in a single column centered on the screen. The code is located in src/layout/layout.py.

# src/layout/layout.py

import dash_bootstrap_components as dbc
from src.layout.add_bar import add_bar
from src.layout.deck import deck
from src.layout.grid import spacer
from src.layout.style import COLORS

def create_layout():
    """Build layout"""
    return dbc.Container(
                        [spacer(10), add_bar(), spacer(10), deck()],
                        width={"size": 2, "offset": 5},
                        style={"backgroundColor": COLORS["light-gray"]},

Modifying the card

We implemented the "Add" button. However, we still have no way of removing the card. Consequently, we will add a "Remove" button to every card to implement that functionality. Additionally, we will add "Move up" / "Move down" buttons to enable the user to change the cards' order.

Let's define button_group in src/layout/card.py containing before the items mentioned:

# src/layout/cards.py

def button_group(index):
    """Group functional buttons together"""
    return dbc.ButtonGroup(
                id={"role": "card_up", "index": index},
                id={"role": "card_down", "index": index},
                id={"role": "card_remove", "index": index},

ButtonGroup is a dbc component rendering several buttons as a continuous group.

Finally, we need to place our new button on card_header. We will do so by adding button_group(index) to the header's InputGroup:

    card_header = dbc.CardHeader(
                            id={"role": "card_toggle", "index": index},
                            id={"role": "material_dropdown", "index": index},

The rest of the layout remains unchanged. However, the most interesting things happen not in the layout but in callbacks.


The deck callback

We will handle all the possible events (addition, removal, or moving a card) in single callbacks as they all modify the deck's children property. The code is located in src/callbacks/deck.py

# src/callbacks/deck.py

import json
import dash
from dash import Input, Output, State, ALL
from src.callbacks.utils import get_caller_id
from src.layout.dropdowns import MATERIAL_OPTIONS
from src.layout.card import empty_layer

def create_callbacks(app):
    """Create deck callbacks"""

        Output("deck", "children"),
        Input("add_card_button", "n_clicks"),
        Input({"role": "card_remove", "index": ALL}, "n_clicks"),
        Input({"role": "card_up", "index": ALL}, "n_clicks"),
        Input({"role": "card_down", "index": ALL}, "n_clicks"),
        State("deck", "children"),
    def _handle_deck(
    ):  # pylint: disable=unused-argument
        """Add, remove or move layer card"""
        caller_id = get_caller_id(dash.callback_context)
        out_cards = current_cards

        if caller_id == "add_card_button":
            out_cards.append(empty_layer(add_clicks, MATERIAL_OPTIONS))
            button = json.loads(caller_id)
            if button["role"] == "card_remove":
                out_cards = []
                for card in current_cards:
                    cid = card["props"]["id"]
                    if cid["index"] != button["index"]:
            elif button["role"] == "card_up":
                deck_idx = _find_index(out_cards, button["index"])
                if deck_idx is not None and deck_idx > 0:
                    _swap_cards(out_cards, deck_idx, deck_idx - 1)
            elif button["role"] == "card_down":
                deck_idx = _find_index(out_cards, button["index"])
                if deck_idx is not None and deck_idx < (len(out_cards) - 1):
                    _swap_cards(out_cards, deck_idx, deck_idx + 1)

        return out_cards

Let's go through the callbacks' code. There are 4 use cases we need to handle:

  1. Addition (i.e., add_card_button clicked): we append a new empty layer card. The card index will be the n_clicks property of the "Add" button. Since this property is monotonically increasing, it provides a unique ID for a new card.
  2. Deletion: we look for a matching ID and copy all input cards except the matching one to the output.
  3. Moving a card up: we search for a matching card index, and if found, swap the card found with the preceding one.
  4. Moving a card down: We look for a matching card index, and if found, swap the card found with the following one.

One more thing to clarify: the "Add" button has a plain-text ID, while all the other IDs are compound IDs stored as a JSON string. That's why we need to handle the "Add" button first. Detection of other cases requires loading the JSON string to dict and inspecting the role field.

At this point, we are still missing helper functions _find_index and swap_cards. They are straightforward and look like this:

def _find_index(card_list, button_index):
    """find card id in list"""
    for idx, card in enumerate(card_list):
        cid = card["props"]["id"]
        if cid["index"] == button_index:
            return idx
    return None

def _swap_cards(card_list, aidx, bidx):
    """swap two cards"""
    card_list[aidx], card_list[bidx] = card_list[bidx], card_list[aidx]

The first function (_find_index) looks for the card's list index idx in the deck's children list of that index field, the same as the triggering button. (Note, that the index field and list index idx are different entities). The second function (swap_cards) swaps two cards in the list.

Using the callback

Finally, we need to create all the callbacks. In dashboard.py, change the callback creation to:

# dashboard.py
import src.callbacks.card.create_callbacks
import src.callbacks.deck.create_callbacks

def create_callbacks(app):
    """Create all callbacks"""

And creating callbacks in the usual way:


Starting the app:

Start the app with python dashboard.py. You should see an indication of a running app in your console. The dashboard is available under http://localhost:8050.


A quick recap:

  1. Stick to the strict IDs convention when adding dynamic components
  2. Pass ALL relevant components' properties to a patching-matching callback
  3. Evaluate dash.context to find out what action triggered the callback

The complete code from this chapter can be found on GitHub:

There is also the file requirements.txt which lists all packages used. You can install it to your virtual env with pip install -r requirements.txt.

I would be happy to hear your opinions about Dash Crush.

See you in the next chapter of Dash Crush! We will continue exploring dynamic layouts and learn how to extract data from dynamic components.

Share this Post:

Related posts