Color-Picker Unexpected Behavior

Summary

I am using a color picker in combination with buttons to allow users to select a color and have an easy color reset. In the simplest form, the unexpected behavior is that the reset button doesn’t always work immediately after choosing a new color with the color picker. However, it does work if some other activity happens in between changing the color picker and using the reset button.

I can work around this in and of itself, but I am more interested in the underlying inconsistency using st.session_state and how the app refreshes when it reloads with the button click. I’m planning to submit a bug report unless it is revealed I have misused st.session_state somehow. :upside_down_face:

Pathological Example: The Setup

UI elements in bold. Variables in monospace.

I have two color pickers. I am storing information in st.session_state.color1 and st.session_state.color2, both initialized at the beginning of the script with an assignment to variables color1 and color2, respectively. Note that picker1 outputs directly into st.session_state.color1, but picker2 outputs to the variable color2 which is then copied into st.session_state.color2. This difference highlights an extra bit of discrepancy that can occur between color1 and st.session_state.color1 as displayed below the button. picker2 can still exhibit the error, but doesn’t display mismatched information below the button like picker1 does.

Finally, I put a dummy button at the top of the app that outputs all the color information along with a key derived from a timestamp to force it to rebuild the button from scratch with every page load. This is key to my question since I am wondering if there is an issue with how the delta is computed to update the page.

Steps to Reproduce

From a fresh load of the app:

  1. change picker1,
  2. click red1 see it work,
  3. change picker1,
  4. click red1 see it not work.
  • You could instead use blue1, green1, or reset instead of red1 to see the issue. The error affects whatever next button is used if it applies to that same color picker.
  • Note the differences between the variable and the session state info, both in the dummy button at top and below the color pickers where it is repeated.

I have included a gif showing other variations and highlighting where the color information does and doesn’t update correctly.

example

Code snippet:

import streamlit as st
import time

def initialize():
    if 'color1' not in st.session_state:
        st.session_state.color1 = '#3A5683'

    if 'color2' not in st.session_state:
        st.session_state.color2 = '#73956F'
    return

initialize()

color1 = st.session_state.color1
color2 = st.session_state.color2

st.button(f'time={time.time()%10}, c1={color1}, ss.c1={st.session_state.color1}, c2={color2}, ss.c2={st.session_state.color2}', key=str(time.time()))

def set_color(color, player):
    if player == 1:
        st.session_state.color1 = color
    else:
        st.session_state.color2 = color
    return

def reset():
    set_color('#3A5683',1)
    set_color('#73956F',2)
    return

st.button('reset', key='reset', on_click = reset)

columns = st.columns(2)

with columns[0]:
    st.session_state.color1 = st.color_picker('picker1', value = color1)
    st.write(f'color1 is {color1}')
    st.write(f'st.session_state.color1 is {st.session_state.color1}')
    st.button('red1', key='r1', on_click = set_color, args=('#FF0000',1))
    st.button('blue1', key='b1', on_click = set_color, args=('#0000FF',1))
    st.button('green1', key='g1', on_click = set_color, args=('#00FF00',1))


with columns[1]:
    color2 = st.color_picker('picker2', value = color2)
    st.session_state.color2 = color2
    st.write(f'color2 is {color2}')
    st.write(f'st.session_state.color2 is {st.session_state.color2}')
    st.button('red2', key='r2', on_click = set_color, args=('#FF0000',2))
    st.button('blue2', key='b2', on_click = set_color, args=('#0000FF',2))
    st.button('green2', key='g2', on_click = set_color, args=('#00FF00',2))

Behavior:

I am expecting the color1 == st.session_state.color1 and color2 == st.session_state.color2 and for that to be correctly displayed both in the dummy button above and text below the buttons. I am expecting the buttons to correctly assign color to the picker and for all the displayed data to update in unison to a change of the picker. However, the data gets out of sync as displayed above.

Debug info

  • Streamlit version: 1.13.0
  • Python version: 3.10.6
  • Using Conda environment (also Streamlit Cloud)
  • OS version: WIndows 10
  • Browser version: Firefox, Edge, Chrome

Requirements file

Just using streamlit and time

Links

Additional information

For context, I came across this issue while playing around with statefulness and making a tic tac toe game. That is here, for completeness: https://mathcatsand-tictactoe.streamlitapp.com/

There’s some complex interaction happening there, and you may have uncovered a bug – I can’t quite tell. However, if you switch to using a key on the color pickers to keep them in sync with st.session_state, that seems to resolve the out-of-sync issue.

color1 = st.color_picker("picker1", key="color1")
...
color2 = st.color_picker("picker2", key="color2")
...

Here’s the full code

import time

import streamlit as st


def initialize():
    if "color1" not in st.session_state:
        st.session_state.color1 = "#3A5683"

    if "color2" not in st.session_state:
        st.session_state.color2 = "#73956F"
    return


initialize()

color1 = st.session_state.color1
color2 = st.session_state.color2

st.button(
    f"time={time.time()%10}, c1={color1}, ss.c1={st.session_state.color1}, c2={color2}, ss.c2={st.session_state.color2}",
    key=str(time.time()),
)


def set_color(color, player):
    if player == 1:
        st.session_state.color1 = color
    else:
        st.session_state.color2 = color
    return


def reset():
    set_color("#3A5683", 1)
    set_color("#73956F", 2)
    return


st.button("reset", key="reset", on_click=reset)

columns = st.columns(2)

with columns[0]:
    color1 = st.color_picker("picker1", key="color1")
    st.write(f"color1 is {color1}")
    st.write(f"st.session_state.color1 is {st.session_state.color1}")
    st.button("red1", key="r1", on_click=set_color, args=("#FF0000", 1))
    st.button("blue1", key="b1", on_click=set_color, args=("#0000FF", 1))
    st.button("green1", key="g1", on_click=set_color, args=("#00FF00", 1))


with columns[1]:
    color2 = st.color_picker("picker2", key="color2")
    st.write(f"color2 is {color2}")
    st.write(f"st.session_state.color2 is {st.session_state.color2}")
    st.button("red2", key="r2", on_click=set_color, args=("#FF0000", 2))
    st.button("blue2", key="b2", on_click=set_color, args=("#0000FF", 2))
    st.button("green2", key="g2", on_click=set_color, args=("#00FF00", 2))

Excellent! That does indeed fix it for me.

Additionally, I had been looking for how to access an output of a widget in a callback function and this solves that, too. I had misunderstood that key was used only as an id for the widget and wasn’t sure how to use it to actually access the output value. So, nifty! I can just access a widget value (even in its own callback function) from a lookup in st.session_state.<widget key>. (At least I’ve confirmed this for color picker and slider, but it gives me something more to explore.) Thanks!

1 Like