How do I move the default scroll of 'st.text_area' to the bottom?

I am implementing st.text_area that can let you know the announcement.

If you type text and press the upload button, the new text will be written down, and you need to scroll down to see it.

Is there a way to specify that ‘st.text_area’ scrolls to the bottom?

Code snippet:

with st.form("notice",clear_on_submit=True):
    t = st.text_area('chat',value= '\n'.join(notice_list), height=200)
    t2 = st.text_input('input')
    submitted = st.form_submit_button('upload',use_container_width=True,type='primary')

2

1

Thanks.

Yes, you can use the scrollToEnd() JavaScript method to scroll the text_area to the bottom after new text is added.

Here is an example implementation:

import streamlit as st

# Define a unique ID for the text_area element
text_area_id = "my_text_area"

# Define a custom HTML template with a script to scroll the text area
scroll_script = f"""
<script>
  var textArea = document.getElementById("{text_area_id}");
  textArea.scrollTop = textArea.scrollHeight;
</script>
"""

# Render the form with the custom script included
with st.form("notice", clear_on_submit=True):
    # Get the current contents of the text area
    notice_list = []

    # Define the text_area with a unique ID
    t = st.text_area("chat", value="\n".join(notice_list), height=200, key=text_area_id)

    # Get user input from text_input
    t2 = st.text_input("input")

    # Add submitted input to the text area
    if st.form_submit_button("upload", use_container_width=True, type="primary"):
        notice_list.append(t2)
        t.value = "\n".join(notice_list)
        # Include the script to scroll the text area to the bottom
        st.markdown(scroll_script, unsafe_allow_html=True)
1 Like

If you want to scroll to the bottom by default for all text areas on the page, it is a little simpler. If you are trying to selectively choose a particular text area to scroll to the bottom, it is a little trickier. Unfortunately, I’m not aware of a Streamlit version which passes the widget keys forward as an HTML ID that can be used on the front end, so the selection process ends up being a little more convoluted than selecting by ID.

The first thing you’ll need to know is that you need to use the components module to execute JavaScript. Furthermore, when you use the components module, the embedded text will be contained in an iframe so your selector will need to “break out” of that.

The next thing is that javascript will only render the first time it appears on the screen, so if you have a function you want to rerun with each user interaction, you have to do something to make it “look new” to the front end.

Finally, I recommend disabling the text_area element since it appears to be used for display rather than input.

If you don’t have to worry about picking a particular text area, you can do this. This example doesn’t use a form, but it should be easy to add that if you want:

import streamlit as st

if 'chat' not in st.session_state:
    st.session_state.chat = "A: Hello"

def submit():
    st.session_state.chat += f'\nB: {st.session_state.B}'
    st.session_state.chat += '\nA: Some response.'
    # Clear the text input widget for convenience
    st.session_state.B = ''

st.text_area('Chat Log', key='chat', disabled=True)

st.text_input('B\'s Response', key='B', on_change=submit)

# Define the scroll operation as a function and pass in something unique for each
# page load that it needs to re-evaluate where "bottom" is
js = f"""
<script>
    function scroll(dummy_var_to_force_repeat_execution){{
        var textAreas = parent.document.querySelectorAll('.stTextArea textarea');
        for (let index = 0; index < textAreas.length; index++) {{
            textAreas[index].style.color = 'red'
            textAreas[index].scrollTop = textAreas[index].scrollHeight;
        }}
    }}
    scroll({len(st.session_state.chat)})
</script>
"""

st.components.v1.html(js)

If you want to pick and choose a text area to format/affect, you can get it via the label. For “Chat Log” as I’ve used in the above example, that would look like:

parent.document.querySelectorAll('.stTextArea [aria-label="Chat Log"]')
3 Likes

HI @mathcatsand how can I do it in the context of streamlit_chat. Here is the code:

import openai
import streamlit as st
from streamlit_chat import message


openai.api_key='API_KEY'

def generate_response(prompt):
    completion=openai.Completion.create(
        engine='text-davinci-003',
        prompt=prompt,
        max_tokens=1024,
        n=1,
        stop=None,
        temperature=0.6,
    )
    message=completion.choices[0].text
    return message

st.title("ChatGPT3.5 BOT")

if 'generated' not in st.session_state:
    st.session_state['generated'] = []
if 'past' not in st.session_state:
    st.session_state['past'] = []

def inputchange():


    inp={
        "inputs": {
            "past_user_inputs": st.session_state.past,
            "generated_responses": st.session_state.generated,
            "text": st.session_state.input,
        },
    }
    output=generate_response(inp['inputs']['text'])

    # append user_input and output to state
    st.session_state['past'].append(st.session_state.input)
    st.session_state['generated'].append(output)


# If responses have been generated by the model
if st.session_state['generated']:
    # Reverse iteration through the list
    for i in range(len(st.session_state['generated']) - 1, -1, -1):
        # message from streamlit_chat
        message(st.session_state['past'][::-1][i], is_user=True, key=str(i) + '_user', )
        message(st.session_state['generated'][::-1][i], key=str(i),avatar_style="adventurer",seed=123,)

user_input = st.text_input("Input Message: ", "", key="input", on_change=inputchange)
2 Likes

@piam were you able to achieve this?

1 Like

Try with

var textAreas = parent.document.querySelectorAll('section.main');

I use that in my conversation app to keep the user focus on the last response. Nice on mobile if you add some CSS like

*, html {scroll-behavior: smooth !important;}

Thank you very much. Do you have a gist or some code that we can look at? Your conversation app looks awesome. My question is where should we add the

*, html {scroll-behavior: smooth !important;}

Thank you.

Hi, I do not have any code examples online right now. I keep the styles early in the app code.

  1. imports of everything
  2. st.set_page_config…
  3. what you see below, though you can probably drop my other style entries and just use *, html {scroll-behavior: smooth !important;}
style_stuff = """
        <style>
        body {box-sizing: border-box; margin: 0;}
        *, html {scroll-behavior: smooth !important;}
        @media print {
        [data-testid="stSidebar"] {display: none !important;}
        div#root div.css-1meupwp {display: block !important;}
        }
        </style>
        """
st.markdown(style_stuff, unsafe_allow_html=True)
1 Like

Thank you very much. I tested it and it works for me now :blush:

styl = """
<style>
    .stTextInput {
        position: fixed;
        bottom: 2rem;
        background-color: white;
        right:700  
        left:500;
        border-radius: 36px; 
        z-index:4;
    }
    .stButton{
        position: fixed;
        bottom: 2rem;
        left:500; 
        right:500;
        z-index:4;
    }

    @media screen and (max-width: 1000px) {
        .stTextInput {
            left:2%; 
            width: 100%;
            bottom: 2.1rem;  
            z-index:2; 
        }                        
        .stButton {            
            left:2%;  
            width: 100%;       
            bottom:0rem;
            z-index:3; 
        }          
    } 

</style>

"""

st.markdown(styl, unsafe_allow_html=True)

This is the code I wrote to solve the problem of placing the text_input and button at the bottom of the page. Just place this code at the beginning of the main code. (Tip: This change in the input box and button operation affects all buttons and input boxes on the page. I tried to manipulate individual buttons and input boxes, but failed.)

2 Likes

Thank you!

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.