Skip to content

Conversation

@evnchn
Copy link
Collaborator

@evnchn evnchn commented Jan 18, 2026

Motivation

In #4918 we see that Quasar expansion "jumps" when opening and closing.

Analysis

Previous analysis by @platinops points out how height: 80px is being set in the intermediate transition process.

  • Question: Why 80px in particular?
  • Answer: actually the value changes with the height of the children.

Here comes the eureka moment: during expansion opening and closing, Quasar evaluates the height of the children, and animates appropriately.

Implementation

The following has been found to be harmful to the heigh evaluation process:

  • Vertical padding: hence why we have padding: 0 var(--nicegui-default-padding);
  • content: "" in ::before and ::after (new in this PR): hence why it is removed.

But we still need the padding, so what do we do?

  • Apply via margin: margin: var(--nicegui-default-padding) 0;

The plain ui.expansion case still breaks:

  • display: block; is found to work

But we want to minimize the intervention:

  • Apply it only when height is explcitly set by Quasar: [style*="height"]

Progress

  • I chose a meaningful title that completes the sentence: "If applied, this PR will..."
  • The implementation is complete.
  • If this PR addresses a security issue, it has been coordinated via the security advisory process.
  • We generally do not pytest layouts.
  • Documentation is not necessary for a bugfix.

Test script shortcuts

Simplified version: refer to #4918 (comment)

with ui.expansion('Expansion'):
    with ui.item():
        with ui.item_section():
            ui.item_label('Line 1')
            ui.item_label('Line 2')

Comprehensive version: refer to #4918

from nicegui import ui

with ui.element("div").classes("q-pa-md").style("width: 350px"):
    with ui.list().style("background-color:#f1948a"):
        with ui.expansion("Expansion (NiceGUI)", caption="Caption"):
            with ui.item():
                with ui.item_section():
                    ui.item_label("Item")
                    ui.item_label("Caption").props("caption")

    with ui.list().style("background-color:#f8c471"):
        with ui.expansion("Expansion (add ui.column)", caption="Caption"):
            with ui.item():
                with ui.column():
                    with ui.item_section():
                        ui.item_label("Item")
                        ui.item_label("Caption").props("caption")

    with ui.list().style("background-color:#85c1e9"):
        with ui.expansion(
            "Expansion (remove .nicegui-expansion)", caption="Caption"
        ).classes(remove="nicegui-expansion"):
            with ui.item():
                with ui.item_section():
                    ui.item_label("Item")
                    ui.item_label("Caption").props("caption")

    with ui.element("q-list").style("background-color:#7dcea0"):
        with ui.element("q-expansion-item").props(
            "label='Expansion (Quasar)' caption=Caption"
        ):
            with ui.element("q-item"):
                with ui.element("q-item-section"):
                    with ui.element("q-item-label"):
                        ui.label("Item")
                    with ui.element("q-item-label").props("caption"):
                        ui.label("Caption")

ui.run(show=False)

Both work flawlessly with this PR

Final notes

CSS changes are easy to make mistake. I have make mistakes before, and I would like more eyes on this to ensure we did not regress anywhere.

@evnchn evnchn added the bug Type/scope: Incorrect behavior in existing functionality label Jan 18, 2026
@evnchn evnchn linked an issue Jan 18, 2026 that may be closed by this pull request
3 tasks
@falkoschindler falkoschindler self-requested a review January 18, 2026 11:29
@falkoschindler falkoschindler added this to the 3.7 milestone Jan 18, 2026
@falkoschindler falkoschindler added the review Status: PR is open and needs review label Jan 18, 2026
Copy link
Contributor

@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great approach, @evnchn! It seems to work flawlessly in many cases.
But setting display: block changes the layout when the content relies on flex layout:

with ui.expansion('Expansion'):
    with ui.row().classes('border'):
        ui.label('Hello')
        ui.label('world')

The row resizes during animation, which it didn't before this PR. Maybe we can explicitly set gap: 0 instead of changing display?

@evnchn
Copy link
Collaborator Author

evnchn commented Jan 23, 2026

explicitly set gap: 0 instead of changing display

Fixes your example but breaks the main one in the PR description above:

with ui.expansion('Expansion'):
    with ui.item():
        with ui.item_section():
            ui.item_label('Line 1')
            ui.item_label('Line 2')

I think it's just trial and error and see what works...

@falkoschindler
Copy link
Contributor

Opus 4.5 suggests to add width: max-content on children - which seems to work nicely.


During animation, display: block is necessary because Quasar's QSlideTransition uses scrollHeight to measure content height, and this measurement is unreliable with flex containers.

However, switching to block layout changes how children are sized:

  • Flex with align-items: flex-start: children size to their content (don't stretch)
  • Block: children default to 100% width (stretch to fill parent)

This causes elements like ui.row() to momentarily expand to full width during animation, then snap back after.

Adding width: max-content to children makes them size to their content even in block layout, mimicking the flex align-items: flex-start behavior and eliminating the width glitch.

@falkoschindler
Copy link
Contributor

But width: max-content causes long text to be merged into a single line during animation:

with ui.expansion('Expansion').classes('w-100'):
    ui.label('Hello ' * 100).classes('border')

😕

@falkoschindler
Copy link
Contributor

I found a solution that seems to work:

Introduce an inner wrapper div that hosts the flex layout, while leaving the outer q-expansion-item__content as block (Quasar's default):

New file: nicegui/elements/expansion.js

export default {
  template: `
    <q-expansion-item ref="qRef">
      <div class="nicegui-expansion-content">
        <slot></slot>
      </div>
    </q-expansion-item>
  `,
};

nicegui/elements/expansion.py – use custom component:

class Expansion(..., component='expansion.js', ...):

nicegui/static/nicegui.css – move flex styling to inner wrapper:

  • .nicegui-expansion .q-expansion-item__content.nicegui-expansion-content
  • Remove the [style*="height"] CSS hack entirely

Benefits

  • Fixes the stutter at the root cause
  • No CSS hacks or display: block overrides during animation
  • Children maintain their layout throughout the animation
  • Clean separation: Quasar handles animation on block element, NiceGUI handles flex layout inside

falkoschindler
falkoschindler previously approved these changes Jan 23, 2026
@falkoschindler
Copy link
Contributor

Thank god we have pytests! This PR breaks the ability to define custom header slots:

with ui.expansion() as expansion:
    with expansion.add_slot('header'):
        ui.label('Header')  # not displayed
    ui.label('Content')

Copy link
Contributor

@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have a final solution. 🤞🏻
@evnchn Do you agree?

@falkoschindler falkoschindler added this pull request to the merge queue Jan 23, 2026
@falkoschindler falkoschindler removed this pull request from the merge queue due to a manual request Jan 23, 2026
@evnchn
Copy link
Collaborator Author

evnchn commented Jan 23, 2026

Tried everything above and it seems to work. I don't really have a systematic way of discovering further edge cases though. At least now that we have bdb28f0 out of the way, I believe that every content should show up even if the stuttering persists, so the impact is manageable.

@falkoschindler falkoschindler added this pull request to the merge queue Jan 24, 2026
Merged via the queue into zauberzeug:main with commit 4e3af34 Jan 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Type/scope: Incorrect behavior in existing functionality review Status: PR is open and needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui.expansion flex styling makes item caption jump

2 participants