Hello, Streamlit Community! ![]()
I’ve been building a quiz app, but I’m running into a UI issue that I’m hoping you can help with.
My Goal: I’m trying to create a stable user interface where the layout of the app remains consistent as the user navigates between different questions.
The Problem: The app’s layout “jumps” up and down. This happens because the height of the Option Box changes depending on the number of options in a given question (e.g., a True/False question has a shorter options list than a multiple-choice question). This dynamic height pushes the Button Box and Answer Box up or down, which is disruptive to the user experience.
I’ve tried using CSS min-height on the options container, but it doesn’t seem to be reliably enforced, and the container still resizes.
Could you please take a look at the minimal, reproducible code below? I’d appreciate any guidance on how to fix the height of the “Option Box” so that the “Button Box” and “Answer Box” remain at a fixed position relative to the page.
Thank you in advance!
import streamlit as st
import pandas as pd # Used for structure, though not for loading from disk in this minimal example— Streamlit Page Configuration —
st.set_page_config(page_title=“Minimal Quiz App - Height Issue Demo”, page_icon=“
”, layout=“centered”)
st.title(“Quiz App: Height Issue Demo”)
— Session State Initialization —
if “current_q_index” not in st.session_state:
st.session_state.current_q_index = 0
st.session_state.responses = {}
st.session_state.revealed = {} # To control when answers are shown— Hardcoded Questions (for demonstration) —
questions_data = [
{
“qid”: “Q001”,
“question”: “Is 2 + 2 equal to 4?”,
“type”: “true/false”,
“option_A”: “True”,
“option_B”: “False”,
“correct_options”: “A”,
“explanation”: “Simple arithmetic: 2 + 2 indeed equals 4.”
},
{
“qid”: “Q002”,
“question”: “What is 2 + 2?”,
“type”: “single”,
“option_A”: “1”,
“option_B”: “2”,
“option_C”: “3”,
“option_D”: “4”,
“correct_options”: “D”,
“explanation”: “2 + 2 = 4. This is a basic addition problem.”
},
{
“qid”: “Q003”,
“question”: “Which of these statements are true about 2 + 2?”,
“type”: “multiple”,
“option_A”: “2 + 2 is less than 1”,
“option_B”: “2 + 2 is less than 2”,
“option_C”: “2 + 2 is less than 3”,
“option_D”: “2 + 2 is less than 5”,
“correct_options”: “D”, # Only D is correct here based on the options
“explanation”: “2 + 2 = 4. Only ‘2 + 2 is less than 5’ is a true statement among the options.”
}
]Convert to DataFrame for easier handling (mimics your original app)
quiz_df = pd.DataFrame(questions_data)
total_questions = len(quiz_df)— CSS for Styling —
st.markdown(“”"
:root { --lh: 1.2em; } /* Styling for the box titles */ .box-title-padding { padding-top: 25px; /* Adds space for the box title */ } /* Question title styling */ .qtitle { border-radius: 8px; padding: 10px 12px; font-weight: 600; font-style: italic; background: #ffffff; color: #111111; min-height: calc(5 * var(--lh)); display: flex; align-items: flex-start; margin-bottom: 8px; } /* Marker for the options container */ .marker-optwrap + div[data-testid="stVerticalBlock"] { background: #f2f2f2; /* Grey background for the outer options wrapper */ padding: 8px; border-radius: 12px; margin-bottom: 10px; overflow: hidden; position: relative; /* Set parent to relative for absolute positioning of title */ } /* Options box (inner white container) - THIS IS THE ONE WE WANT TO FIX HEIGHT */ .marker-optwrap + div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlock"] { background-color: #ffffff; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; min-height: 250px !important; /* <--- PROBLEM AREA: This min-height is not consistently applied */ padding-top: 2px; } /* Consistent gap for options (inside the white box) */ .marker-optwrap + div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlock"] { display: flex; flex-direction: column; gap: 1.5rem; padding-left: 1rem; padding-right: 1rem; } /* Button box container */ .button-box-container { position: relative; /* Set parent to relative for absolute positioning of title */ } /* Answer/Explanation Box styling */ .answer-box { border-radius: 12px; border: 1px solid rgba(0,128,0,0.25); box-shadow: 0 2px 8px rgba(0,128,0,0.08); margin-top: 8px; margin-bottom: 20px; padding: 12px 14px; min-height: 100px; /* Placeholder height for empty answer box */ position: relative; /* Set parent to relative for absolute positioning of title */ } @media (prefers-color-scheme: dark) { .qtitle { background: #1a1a1a; color: #f0f2f6; } .marker-optwrap + div[data-testid="stVerticalBlock"] { background: #2a2a2a; } .marker-optwrap + div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlock"] { background-color: #1a1a1a; border-color: #444444; min-height: 250px !important; /* Dark mode fixed height */ } .marker-optwrap + div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlock"] { gap: 1.5rem; } .answer-box { background: rgba(0,128,0,0.1); border-color: rgba(0,128,0,0.5); } .box-title { color: #8c9096; } }“”", unsafe_allow_html=True)
— Question Renderer Function —
def render_question_content(row, idx, total_count):
qid = row[“qid”]
qtype = row[“type”]
opts = {k: v for k, v in row.items() if k.startswith(“option_”) and pd.notna(v)}
correct_letters = [x.strip() for x in str(row.get(“correct_options”,“”)).split(“,”) if str(x).strip()]is_revealed = st.session_state.revealed.get(qid, False) # 1. Question Title Box with st.container(border=True): st.markdown('<div class="box-title">Question Box</div>', unsafe_allow_html=True) st.markdown(f"""<div class="qtitle box-title-padding">Q{idx+1}. {row['question']}</div>""", unsafe_allow_html=True) # 2. Options Box (with intended fixed height) st.markdown('<div class="marker-optwrap"></div>', unsafe_allow_html=True) # Marker for CSS with st.container(): # Outer grey container with st.container(border=True): # Inner white container (target for fixed height) st.markdown('<div class="box-title">Option Box</div>', unsafe_allow_html=True) st.markdown('<div class="box-title-padding"></div>', unsafe_allow_html=True) if qtype in ["true/false", "single"]: st.markdown("**Select One**", unsafe_allow_html=True) choice = st.radio( "Select one", list(opts.keys()), format_func=lambda l: f"{l}. {opts.get(l,'')}", disabled=is_revealed, key=f"radio_{qid}", label_visibility="collapsed" ) st.session_state.responses[qid] = [choice] if choice else [] elif qtype == "multiple": st.markdown("**Select As Many As Apply**", unsafe_allow_html=True) current_responses = st.session_state.responses.get(qid, []) selected_options = [] for letter, text in opts.items(): checked = st.checkbox( f"{letter}. {text}", value=(letter in current_responses), disabled=is_revealed, key=f"chk_{qid}_{letter}" ) if checked: selected_options.append(letter) st.session_state.responses[qid] = selected_options else: st.warning("Unknown question type.") st.session_state.responses[qid] = [] # 3. Buttons Container (will jump if options box height changes) st.markdown('<div class="button-box-container">', unsafe_allow_html=True) with st.container(border=True): st.markdown('<div class="box-title">Button Box</div>', unsafe_allow_html=True) st.markdown('<div class="box-title-padding"></div>', unsafe_allow_html=True) col1, col2, col3 = st.columns(3) with col1: if st.button("Previous", use_container_width=True, key=f"prev_{qid}", disabled=(idx == 0)): st.session_state.current_q_index = max(0, idx - 1) st.rerun() with col2: if st.button("Submit", use_container_width=True, key=f"submit_{qid}", disabled=is_revealed): st.session_state.revealed[qid] = True # For this demo, submit also reveals st.rerun() with col3: if st.button("Next", use_container_width=True, key=f"next_{qid}", disabled=(idx == total_questions - 1)): st.session_state.current_q_index = min(total_questions - 1, idx + 1) st.rerun() st.markdown('</div>', unsafe_allow_html=True) # 4. Answer/Explanation Box with st.container(): ans_text = "" expl_html = "" if is_revealed: user_ans = st.session_state.responses.get(qid, []) ok = (set(user_ans) == set(correct_letters)) if qtype == "multiple" else (user_ans == correct_letters) user_display = ", ".join(user_ans) if user_ans else "—" ans_text = (f"✅ Correct. Answer: {', '.join(correct_letters)}" if ok else f"❌ Incorrect. Correct answer: {', '.join(correct_letters)} • Your answer: {user_display}") expl_content = row.get("explanation","") expl_html = f"<div style='margin-top:6px; opacity:0.9'>{expl_content}</div>" if isinstance(expl_content, str) and expl_content.strip() else "" st.markdown(f""" <div class="answer-box"> <div class="box-title">Answer Box</div> <div class="box-title-padding" style="font-weight:600">{ans_text}</div> {expl_html} </div> """, unsafe_allow_html=True)— Main App Logic —
Display the current question
current_q_row = quiz_df.iloc[st.session_state.current_q_index]
render_question_content(current_q_row, st.session_state.current_q_index, total_questions)



