Streamlit-dnd: add drag and drop behavior to streamlit containers

Hey everyone,

I am excited to share a new custom component I have been working on: streamlit-dnd which makes things inside your Streamlit containers drag and droppable. You make sure the container(s) you want to target are each given a key and then pass the key(s) to the dnd function and the direct children of those containers become draggable. You can reorder items inside a single container, or drag them between containers. When a drop happens, you get an event dictionary back with info describing the move so you can update your session_state which you can then use to update the app so that the app reflects the change.

streamlit-dnd-demo

Check out the project here:

Demo

GitHub repo

Installation

Install is the usual one-liner:

pip install streamlit-dnd

Quick start

The whole idea is that you are using normal keyed containers, then call dnd() with their keys after rendering them. Here is a minimal single-list reorder:

import streamlit as st
from streamlit_dnd import dnd, apply_move

if "items" not in st.session_state:
    st.session_state.items = {"list": ["Apples", "Bananas", "Cherries", "Dates"]}

# 1. Render a keyed container whose children come from session state
with st.container(key="list", border=True):
    for it in st.session_state.items["list"]:
        with st.container(key=f"item_{it}", border=True):
            st.write(it)

# 2. Turn on drag and drop (call this AFTER rendering the container)
event = dnd("list")

# 3. Apply the drop to session state and rerun
if event:
    # You can use the provided utility function apply_move for updating session_state
    # OR update session state using the info in the event dictionary yourself
    apply_move(event, st.session_state.items)
    st.rerun()

That is the entire loop. Render from state, enable dnd, apply the move on a drop. To allow dragging between containers, just pass more keys: dnd("list_a", "list_b").

There are a few options for how it looks and behaves (a border handle by default so buttons and inputs inside your items stay clickable, a “ghost” drop preview, source/destination restrictions for one-way moves, and so on). The README has the full table and a couple of bigger examples.

Background

Some of you might remember a post I made almost two years ago about a proof of concept for draggable Streamlit containers. That one got a lot of interest, and I always meant to turn it into something you could actually install and use. But I kept hitting roadblocks in two areas.

The first was due to me trying to code the drag-and-drop behavior completely from scratch, and I did not have much experience coding the inner workings of dnd. The second stumbling block was Streamlit itself. Over time, updates changed the layout structure of containers enough that the way I was targeting elements to attach dnd behavior stopped working. Between those two things, the proof of concept stayed a proof of concept.

What finally got me past both walls was using AI to fill in the gaps in my knowledge, specifically the dnd internals I was weak on. I want to be clear that this is not a vibe coded project. I had a direct hand in solving the crucial problems. I provided the targeting approach for finding and hooking into the right container elements, and the logic for making this play nicely with how Streamlit actually works. A good example of the latter: the component does not force a rerun on every drag event. It reports the drop event back to you and leaves how the app should respond/change up to you, because that is the level of control I would want for this type of component. That was a deliberate design call based on my experience building streamlit apps and custom components.

I mostly relied on AI for the dnd plumbing and some code organization. And just to underline the point: I did attempt to one-shot the whole thing with AI (no code) using multiple Opus and Codex models and failed to get it working.

Truth be told, the delay in finishing this is also due to me focusing on other major projects such as PyNote which is based off of prototypes I build using Streamlit and has dnd container behavior…


If you find this interesting or useful, please give it a try, and let me know if you build anything with it (ill be glad to give stars and recs etc). Issues and PRs welcome on GitHub.

sir see my code it run fine but when i drag and drop the frame it give me the error

see code

import streamlit as st

import pandas as pd

from streamlit_dnd import dnd, apply_move

if ‘my_dnd_list’ not in st.session_state:

st.session_state\['my_dnd_list'\] = \[\]  # Initialize with empty list or default values

# Initialize 5 dataframes in session state

if “dfs” not in st.session_state:

st.session_state.dfs = {

    "df1": pd.DataFrame({"Data": \["Item A", "Item B"\]}),

    "df2": pd.DataFrame({"Data": \["Item C", "Item D"\]}),

    "df3": pd.DataFrame({"Data": \["Item E", "Item F"\]}),

    "df4": pd.DataFrame({"Data": \["Item G", "Item H"\]}),

    "df5": pd.DataFrame({"Data": \["Item I", "Item J"\]}),

}

# Display each dataframe in a draggable container

container_key = “my_dnd_list”

with st.container(key=container_key, border=True):

for df_key, df_val in st.session_state.dfs.items():

    with st.container(key=f"item\_{df_key}", border=True):

        st.write(f"### {df_key.upper()}")

        st.dataframe(df_val)

# Handle drag-and-drop events

drop_event = dnd(container_key)

if drop_event:

apply_move(drop_event, st.session_state.dfs)



st.rerun()

see error

KeyError: ‘my_dnd_list’

File “D:\pyff\dndwdf.py”, line 27, in
apply_move(drop_event, st.session_state.dfs)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “C:\Users\user\AppData\Local\Programs\Python\Python314\Lib\site-packages\streamlit_dnd_init_.py”, line 267, in apply_move
src = lists[event.from_container]
~~~~~^^^^^^^^^^^^^^^^^^^^^^

error in this line

apply_move(drop_event, st.session_state.dfs)

please guide me and send right code

best regard

Hey everyone!

I have just released Version 0.2.0 of streamlit-dnd!

Thank you to everyone who posted questions and shared their code. I reviewed the errors reported in this thread and the related help posts.

Some errors came from using the package incorrectly. However, the examples also showed me where the package was confusing or made common tasks harder than necessary.

Version 0.2.0 makes those things easier and handles several ways people naturally organize their Streamlit apps.

Headings can stay inside draggable containers

If you put a heading, caption, divider, or instruction inside a draggable container, it now stays in place.

Only elements with their own Streamlit key are draggable by default.

import streamlit as st
from streamlit_dnd import dnd, apply_move

if "tasks" not in st.session_state:
    st.session_state["tasks"] = [
        "Write specification",
        "Design schema",
        "Build API",
    ]

with st.container(key="task_list", border=True):
    # This heading stays in place because it does not have a key
    st.subheader("Tasks")

    # Each draggable item has its own key
    for task in st.session_state["tasks"]:
        with st.container(key=f"task_{task}", border=True):
            st.write(task)

event = dnd("task_list")

if event:
    apply_move(event, st.session_state["tasks"])
    st.rerun()

Previously, the heading could be counted as another draggable item. Your Python list might contain three tasks while the page appeared to contain four items. That mismatch caused some of the IndexError reports.

Version 0.2.0 ignores the heading and moves only the three keyed task containers.

Lists are simpler to update

If your draggable items come from a list, pass that list directly to apply_move().

apply_move(event, st.session_state["tasks"])

apply_move() changes the order of the list to match what the user did on the page.

For example, if the user moves "Build API" to the top, the list becomes:

[
    "Build API",
    "Write specification",
    "Design schema",
]

You no longer need to wrap one list inside another dictionary just to reorder a single container.

Moving items between two containers

If users can move items between two containers, pass both lists to apply_move().

event = dnd("todo", "done")

if event:
    apply_move(
        event,
        {
            "todo": st.session_state["todo_items"],
            "done": st.session_state["done_items"],
        },
    )
    st.rerun()

The names in the dictionary must match the container keys passed to dnd().

In this example:

  • "todo" matches st.container(key="todo")
  • "done" matches st.container(key="done")

When a user moves an item from Todo to Done, apply_move() removes it from todo_items and adds it to done_items.

Dictionaries of DataFrames can now be reordered

If your draggable items come from a dictionary, pass that dictionary directly to apply_move().

This works well when each item has a name, such as a collection of DataFrames:

import pandas as pd
import streamlit as st
from streamlit_dnd import dnd, apply_move

if "dataframes" not in st.session_state:
    st.session_state["dataframes"] = {
        "df1": pd.DataFrame({"Data": ["Item A", "Item B"]}),
        "df2": pd.DataFrame({"Data": ["Item C", "Item D"]}),
        "df3": pd.DataFrame({"Data": ["Item E", "Item F"]}),
    }

with st.container(key="dataframe_list", border=True):
    st.subheader("DataFrames")

    for name, dataframe in st.session_state["dataframes"].items():
        with st.container(key=f"dataframe_{name}", border=True):
            st.write(f"### {name.upper()}")
            st.dataframe(dataframe)

event = dnd("dataframe_list")

if event:
    apply_move(
        event,
        st.session_state["dataframes"],
        container_key="dataframe_list",
    )
    st.rerun()

container_key="dataframe_list" tells apply_move() that the entire dictionary belongs to that one draggable container.

If the user moves df3 above df1, the dictionary is reordered so the DataFrames appear in that order after Streamlit reruns.

This moves complete DataFrame cards. It does not move individual rows inside a DataFrame.

Empty containers are easier to use

If a container has no draggable items, you can show instructions telling the user where to drop something.

dnd(
    "todo",
    "done",
    placeholder={
        "todo": "Drop a task here",
        "done": "Drop a completed task here",
    },
)

The placeholder now appears even if the container also contains a fixed heading or other instructions.

Existing apps can keep the old behavior

Version 0.2.0 expects each draggable item to have its own Streamlit key.

If your existing app uses unkeyed elements such as separate st.write() calls as draggable items, enable the previous behavior with:

dnd("my_container", item_mode="all")

When using item_mode="all", every element inside the container is treated as an item. Make sure your Python list contains one value for every element displayed in the container.

Errors should be easier to understand

If a container name does not match, the error now tells you which name is missing.

If the items displayed on the page do not match the Python list, the error explains that they are out of sync.

The package also checks for duplicate container names, unknown source or destination names, invalid options, and unsupported data types before trying to perform a move.

Installation

Upgrade to Version 0.2.0 with:

pip install --upgrade streamlit-dnd

Project links:

If you previously encountered an IndexError, a KeyError containing your container name, or a problem reordering DataFrames, please try Version 0.2.0 and let me know if the problem continues.