Retain button click color

Summary

When I hover on the button, I get the color
Screenshot 2023-09-28 at 6.32.08 PM

when I click the button, I get the purple color over full button

Screenshot 2023-09-28 at 6.32.12 PM

Is there a way to retain or persist the button color (purple) after the click? Similarly, when I click on the next button first button should come back to its original backgorund color (grey in my case) and the second button should persist the purple color and so on for other buttons as well.

Steps to reproduce

Code snippet:

# Module preferences buttons
    with st.container():
        col1, col2 = st.columns(
            [1, 1]
        )

        with col1:
            st.button(
                "General",
                key=None,
                help="help info",
                on_click=clicked,
                args=[1],
                disabled=False,
                use_container_width=True,
            )
        with col2:
            st.button(
                "Whole Engine",
                key=None,
                help="help info",
                on_click=None,
                kwargs=None,
                disabled=False,
                use_container_width=True,
            )

m = st.markdown(
            """
            <style>
                button[data-testid="baseButton-secondary"]{  
                    background-color: #B8CEDB;
                }
            </style>""",
            unsafe_allow_html=True,
        )

Debug info

  • Streamlit version: (get it with $ streamlit version) – 1.27.0
  • Python version: (get it with $ python --version) – 3.10
  • Using Conda? PipEnv? PyEnv? Pex? – Conda
  • OS version: macOS 13.6
  • Browser version: Chrome 117.0

Hi @Preetham_Manjunatha, are you looking for something like this?

import streamlit as st
import streamlit.components.v1 as components

mystate = st.session_state
if "btn_prsd_status" not in mystate:
    mystate.btn_prsd_status = [False] * 2

btn_labels = ["General", "Whole Engine"]
unpressed_colour = "#E8EAF6"
pressed_colour = "#64B5F6"

def ChangeButtonColour(widget_label, prsd_status):
    btn_bg_colour = pressed_colour if prsd_status == True else unpressed_colour
    htmlstr = f"""
        <script>
            var elements = window.parent.document.querySelectorAll('button');
            for (var i = 0; i < elements.length; ++i) {{ 
                if (elements[i].innerText == '{widget_label}') {{ 
                    elements[i].style.background = '{btn_bg_colour}'
                }}
            }}
        </script>
        """
    components.html(f"{htmlstr}", height=0, width=0)

def ChkBtnStatusAndAssignColour():
    for i in range(len(btn_labels)):
        ChangeButtonColour(btn_labels[i], mystate.btn_prsd_status[i])

def btn_pressed_callback(i):
    mystate.btn_prsd_status = [False] * 2
    mystate.btn_prsd_status[i-1] = True

with st.container():
    col1, col2 = st.columns((1, 1))
    col1.button("General", key="b1", on_click=btn_pressed_callback, args=(1,) )
    col2.button("Whole Engine", key="b2", on_click=btn_pressed_callback, args=(2,) )
    ChkBtnStatusAndAssignColour()

Cheers

2 Likes

@Shawn_Pereira, thank you very much! Perfect. This is exactly what I was looking for. Is there a way I can get rid of the click state to false when I click on the same button? Meaning, when the button is active and the color will change to pressed_colour = "#64B5F6". If I click the same button, I should get unpressed_colour = "#E8EAF6" and the state for that particular button shold be False.

@Shawn_Pereira, when I integrate the buttons with the chat mechanism, the chat interface starts from the bottom (please refer to image below).

However, it should start from the top as shown below. Please note chat starts from top only when the line ChkBtnStatusAndAssignColour() is commented out in code below.

Here is the minimal working code:

import streamlit as st
import streamlit.components.v1 as components
import random
import time

# Set page title
st.set_page_config(
    page_title="test",
    layout="wide",
)

mystate = st.session_state
if "btn_prsd_status" not in mystate:
    mystate.btn_prsd_status = [False] * 8

btn_labels = ["Button 1", "Button 2", "Button 3", "Button 4", "Button 5", "Button 6", "Button 7", "Button 8"]
unpressed_colour = "#E8EAF6"
pressed_colour = "#64B5F6"

def ChangeButtonColour(widget_label, prsd_status):
    btn_bg_colour = pressed_colour if prsd_status == True else unpressed_colour
    htmlstr = f"""
        <script>
            var elements = window.parent.document.querySelectorAll('button');
            for (var i = 0; i < elements.length; ++i) {{ 
                if (elements[i].innerText == '{widget_label}') {{ 
                    elements[i].style.background = '{btn_bg_colour}'
                }}
            }}
        </script>
        """
    components.html(f"{htmlstr}")

def ChkBtnStatusAndAssignColour():
    for i in range(len(btn_labels)):
        ChangeButtonColour(btn_labels[i], mystate.btn_prsd_status[i])

def btn_pressed_callback(i):
    mystate.btn_prsd_status = [False] * 8
    mystate.btn_prsd_status[i-1] = True

with st.container():
    # Stremalit columns initialize for the buttons
    col1, col2, col3, col4, col5, col6, col7, col8 = st.columns(
        [1, 1, 1, 1, 1, 1, 1, 1]
    )

    # Button 1
    col1.button(
        "Button 1",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(1,),
        use_container_width=True,
    )

    # Button 2
    col2.button(
        "Button 2",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(2,),
        use_container_width=True,
    )

    # Button 3
    col3.button(
        "Button 3",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(3,),
        use_container_width=True,
    )

    # Button 4
    col4.button(
        "Button 4",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(4,),
        use_container_width=True,
    )

    # Button 5
    col5.button(
        "Button 5",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(5,),
        use_container_width=True,
    )

    # Button 6
    col6.button(
        "Button 6",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(6,),
        use_container_width=True,
    )

    # Button 7
    col7.button(
        "Button 7",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(7,),
        use_container_width=True,
    )

    # Button 8
    col8.button(
        "Button 8",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(8,),
        use_container_width=True,
    )

    # Check button status and color
    ChkBtnStatusAndAssignColour()

    st.markdown(
        """
        <style>
           div[data-testid="stHorizontalBlock"] { position:fixed; bottom:126.5px; width: 89.3%; text-align: 
                center; padding-right: 9%; padding-left: 9%; z-index: 99; opacity: 1; background-color: #ece1ec;}
        </style>""",
        unsafe_allow_html=True,
    )

   
    
with st.container():
    # Initialize chat history
    if "messages" not in st.session_state:
        st.session_state.messages = []

    # Display chat messages from history on app rerun
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])

    # Accept user input
    if prompt := st.chat_input("What is up?"):
        # Add user message to chat history
        st.session_state.messages.append({"role": "user", "content": prompt})
        # Display user message in chat message container
        with st.chat_message("user"):
            st.markdown(prompt)

        # Display assistant response in chat message container
        with st.chat_message("assistant"):
            message_placeholder = st.empty()
            full_response = ""
            assistant_response = random.choice(
                [
                    "Hello there! How can I assist you today?",
                    "Hi, human! Is there anything I can help you with?",
                    "Do you need help?",
                ]
            )
            # Simulate stream of response with milliseconds delay
            for chunk in assistant_response.split():
                full_response += chunk + " "
                time.sleep(0.05)
                # Add a blinking cursor to simulate typing
                message_placeholder.markdown(full_response + "â–Ś")
            message_placeholder.markdown(full_response)
        # Add assistant response to chat history
        st.session_state.messages.append({"role": "assistant", "content": full_response})

Please let me know if there is a way to get the chat interface to start from the top.

Hey @Preetham_Manjunatha ,

To get the chat interface from the top, you would have to reorder the messages from latest to oldest.

To do this, you would have to maybe store your messages to a list and render the list with an HTML Template (Streamlit docs: API Reference - Streamlit Docs )

If not, I would also suggest you check out Streamlit’s caching feature and cache your messages to it.

When you render it, retrieve your messages from the cache and reverse order them for latest at the top of the chat interface.

Good luck!

  • Jyo

@DevTechJr, what I mean from the top is that the chat messages should align from the top of the page. Please refer to the second image above. However, the chat messages starts from the bottom align as the first image using the included code.

Hi @Preetham_Manjunatha

There are 3 issues:

  1. You needed the Button colour toggle, even for the double-pressed button
  2. You wanted the ability to change the font, possibly depending upon the background colour

The above has been resolved in the current code, and you can adjust the hex codes based on your colour preference.

  1. The last issue is with the vertical origin of the chat, which should actually start from the top (instead from the bottom). This problem is solved with the re-placement of the components.html statement.

Once you load the code, remember to scroll right to the top, and try it out. Hope all should work ok then.

import streamlit as st
import streamlit.components.v1 as components
import random
import time

# Set page title
st.set_page_config(
    page_title="test",
    layout="wide",
)

mystate = st.session_state
if "btn_prsd_status" not in mystate:
    mystate.btn_prsd_status = [False] * 8

btn_labels = ["Button 1", "Button 2", "Button 3", "Button 4", "Button 5", "Button 6", "Button 7", "Button 8"]
unpressed_colour = "#E8EAF6"
pressed_colour = "#64B5F6"
black_font = "#000000"
white_font = "#FFFFFF"

def ChangeButtonColour(widget_label, prsd_status):
    btn_bg_colour = pressed_colour if prsd_status == True else unpressed_colour
    btn_font_color = white_font if btn_bg_colour == pressed_colour else black_font
    htmlstr = f"""
        <script>
            var elements = window.parent.document.querySelectorAll('button');
            for (var i = 0; i < elements.length; ++i) {{ 
                if (elements[i].innerText == '{widget_label}') {{ 
                    elements[i].style.background = '{btn_bg_colour}';
                    elements[i].style.color ='{btn_font_color}';
                }}
            }}
        </script>
        """
    components.html(f"{htmlstr}")

def ChkBtnStatusAndAssignColour():
    for i in range(len(btn_labels)):
        ChangeButtonColour(btn_labels[i], mystate.btn_prsd_status[i])

def btn_pressed_callback(i):
    old_btn_value = mystate.btn_prsd_status[i-1]
    mystate.btn_prsd_status = [False] * 8
    mystate.btn_prsd_status[i-1] = not old_btn_value

with st.container():
    # Stremalit columns initialize for the buttons
    col1, col2, col3, col4, col5, col6, col7, col8 = st.columns(
        [1, 1, 1, 1, 1, 1, 1, 1]
    )

    # Button 1
    col1.button(
        "Button 1",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(1,),
        use_container_width=True,
    )

    # Button 2
    col2.button(
        "Button 2",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(2,),
        use_container_width=True,
    )

    # Button 3
    col3.button(
        "Button 3",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(3,),
        use_container_width=True,
    )

    # Button 4
    col4.button(
        "Button 4",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(4,),
        use_container_width=True,
    )

    # Button 5
    col5.button(
        "Button 5",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(5,),
        use_container_width=True,
    )

    # Button 6
    col6.button(
        "Button 6",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(6,),
        use_container_width=True,
    )

    # Button 7
    col7.button(
        "Button 7",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(7,),
        use_container_width=True,
    )

    # Button 8
    col8.button(
        "Button 8",
        key=None,
        help="help info",
        on_click=btn_pressed_callback,
        args=(8,),
        use_container_width=True,
    )

    st.markdown(
        """
        <style>
           div[data-testid="stHorizontalBlock"] { position:fixed; bottom:126.5px; width: 89.3%; text-align: 
                center; padding-right: 9%; padding-left: 9%; z-index: 99; opacity: 1; background-color: #ece1ec;}
        </style>""",
        unsafe_allow_html=True,
    )

   
    
with st.container():
    # Initialize chat history
    if "messages" not in st.session_state:
        st.session_state.messages = []

    # Display chat messages from history on app rerun
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])

    # Accept user input
    if prompt := st.chat_input("What is up?"):
        # Add user message to chat history
        st.session_state.messages.append({"role": "user", "content": prompt})
        # Display user message in chat message container
        with st.chat_message("user"):
            st.markdown(prompt)

        # Display assistant response in chat message container
        with st.chat_message("assistant"):
            message_placeholder = st.empty()
            full_response = ""
            assistant_response = random.choice(
                [
                    "Hello there! How can I assist you today?",
                    "Hi, human! Is there anything I can help you with?",
                    "Do you need help?",
                ]
            )
            # Simulate stream of response with milliseconds delay
            for chunk in assistant_response.split():
                full_response += chunk + " "
                time.sleep(0.05)
                # Add a blinking cursor to simulate typing
                message_placeholder.markdown(full_response + "â–Ś")
            message_placeholder.markdown(full_response)
        # Add assistant response to chat history
        st.session_state.messages.append({"role": "assistant", "content": full_response})

    # Check button status and color
    ChkBtnStatusAndAssignColour()

Cheers

1 Like

@Shawn_Pereira, thank you very much!

@Shawn_Pereira, there is some scrolling issues when ChkBtnStatusAndAssignColour() is called. When multiple chat inputs are entered, there is always a region empty as in the attached image (rectangle in red color). It is because there exists some scrolling in the chat interface.

I don’t know why this behavior only happens when the ChkBtnStatusAndAssignColour() is called. Is there a way to fix this pseudo scrolling?

Each invocation of ChkBtnStatusAndAssignColour() eventually leads to the invocation of components.html, which inserts a blank line. This is why I moved ChkBtnStatusAndAssignColour() to the bottom of the script. Basis on what you need, there’s no way I know to get around this.

You could try using buttons from AntD ( streamlit-antd-components · Streamlit (nicedouble-streamlitantdcomponentsdemo-app-middmy.streamlit.app) instead, to see if it works for your use case.

Cheers

import streamlit.components.v1 as components

mystate = st.session_state
if "btn_prsd_status" not in mystate:
    mystate.btn_prsd_status = ["secondary"] * 2

unpressed_colour = "#E8EAF6"
pressed_colour = "#64B5F6"

def btn_pressed_callback(i):
    mystate.btn_prsd_status = ["secondary"] * 2
    mystate.btn_prsd_status[i-1] = "primary"

with st.container():
    col1, col2 = st.columns((1, 1))
    col1.button("General", key="b1", on_click=btn_pressed_callback, args=(1,), type= mystate.btn_prsd_status[0])
    col2.button("Whole Engine", key="b2", on_click=btn_pressed_callback, args=(2,), type= mystate.btn_prsd_status[1] )

you can also use type parameter to avoid JS