How to correctly save data_editor changes before button actions

Hello Streamlit community,

I’m facing a challenging issue with my Streamlit app that involves a data_editor and action buttons. My goal is to prevent users from clicking action buttons while they’re in the middle of editing data to avoid losing their changes.

The Problem

  1. When a user is actively editing a cell in the data_editor and clicks a button:

    • The app first reruns with the button’s logic
    • Only then does it rerun to update the data_editor value
    • This can lead to data loss or unexpected behavior
  2. I’ve tried implementing CSS to disable buttons while editing:
    body:has([data-testid=“stDataFrame”] *:focus) .stButton {
    pointer-events: none;
    opacity: 0.3;
    cursor: not-allowed;
    }

  3. This works well until the user changes browser tabs or loses focus on the page - when they return, the CSS effect is canceled even if they were in the middle of editing.

Is there a reliable way to:

  • Force the data_editor to save changes before any button action processes?
  • OR maintain the disabled state of buttons even when the page loses focus?
  • OR detect when a cell is being edited, even after tab change?

Any help would be greatly appreciated!

Here is a code sample for a CSS solution (not working if the user switches to another tab/application and return, the CSS protection is lost) :

import streamlit as st
import pandas as pd

data = pd.DataFrame({
‘Nom’: [‘Jean’, ‘Marie’, ‘Pierre’, ‘Sophie’],
‘Âge’: [28, 34, 42, 25],
‘Ville’: [‘Paris’, ‘Lyon’, ‘Marseille’, ‘Bordeaux’]
})

st.markdown(“”"

[data-testid="stDataFrame"]:has(*:focus), [data-testid="stDataFrame"]:has(*:focus-within), [data-testid="stDataFrame"]:has([role="gridcell"][aria-selected="true"]), [data-testid="stDataFrame"]:has(input:focus) { box-shadow: 0 5px 20px rgba(0, 0, 255, 0.4); border: 3px solid blue !important; z-index: 1000; position: relative; } /* Unable to click on buttons when the table is in focus */ body:has([data-testid="stDataFrame"] *:focus) .stButton, body:has([data-testid="stDataFrame"] *:focus-within) .stButton, body:has([data-testid="stDataFrame"] [role="gridcell"][aria-selected="true"]) .stButton, body:has([data-testid="stDataFrame"] input:focus) .stButton { pointer-events: none; opacity: 0.3; cursor: not-allowed; filter: grayscale(100%); }

“”", unsafe_allow_html=True)

edited_df = st.data_editor(
data,
key=“unique_editor”
)

if st.button(“Test”, key=“test_button”):
st.balloons()

I ran your code without the css and didn’t experience any data loss. Howevr, the balloons not always appear when I click the button, which looks like a bug.

I’ve successfully resolved this issue on my own by implementing a CSS-based solution that prevents button actions from executing before the data_editor updates are committed.

I discovered that Streamlit creates an element inside a portal element ([data-testid="portal"]) when a user is actively editing a cell. This element persists even when the user switches to another application and returns, making it a reliable indicator of editing state.

Here’s the CSS I implemented:

/* Highlight the data frame when editing */
body:has([data-testid="portal"] .gdg-input) [data-testid="stDataFrame"],
body:has([data-testid="portal"] .gdg-style) [data-testid="stDataFrame"],
body:has([data-testid="portal"] [id^="gdg-overlay"]) [data-testid="stDataFrame"] {
    box-shadow: 0 2px 15px rgba(0, 82, 204, 0.2);
    border: 2px solid rgba(0, 82, 204, 0.6) !important;
    z-index: 1000;
    position: relative;
    transition: all 0.8s ease-in-out;
    background-color: rgba(240, 247, 255, 0.1);
}
                
/* Disable buttons only during cell editing */
body:has([data-testid="portal"] .gdg-input) .stButton,
body:has([data-testid="portal"] .gdg-style) .stButton,
body:has([data-testid="portal"] [id^="gdg-overlay"]) .stButton {
    pointer-events: none;
    opacity: 0.5;
    cursor: not-allowed;
    filter: grayscale(70%);
    transition: all 0.8s ease-in-out;
}

This approach effectively resolves a frustrating issue when combining data_editor with buttons in a complex application:

  1. When a user is not editing the table, clicking a button triggers just one rerun
  2. When a user is editing the table, clicking a button would normally trigger two reruns:
  • First for the button action
  • Then for the data_editor state update

My Specific Use Case: Table Navigation with Data Preservation

In my application, I needed to save the data_editor content when users click navigation buttons to switch between tables. This created a particularly challenging scenario because:

  • If I implemented saving during the first rerun, it would work fine when not editing, but would save outdated data if the user was actively editing (since the data_editor update happens on the second rerun)
  • I attempted to implement saving after the table change (essentially always saving the previous table), but this approach didn’t align well with the rest of my application’s logic
  • The core issue was that having two reruns with the table updating last was a fundamental blocker, making it impossible to design a consistent logical flow that worked in all scenarios

The CSS solution elegantly forces users to complete their edits before switching tables, ensuring data integrity across the entire application flow.

By the way, I’m currently using Streamlit 1.37.1, and this solution works perfectly. However, I’m unsure if the [data-testid="portal"] selector will remain in future Streamlit versions, so this might require monitoring as Streamlit evolves.

This approach has been remarkably effective for my use case, but I’d welcome any feedback on whether this is considered a proper solution or if there are more “official” ways to handle this interaction.