Flickering when restoring offset between renders

Hey everyone. I am currently working on the following problem:There are multiple collapsibles below each other on a page. When I expand the second collapsible, the first one collapses.The problem is that when this happens, the second collapsible does not stay where it is supposed to be - it flows further up and gives the user unexpected results.

I have worked around this with a 2-step-update using Browser.Dom. But now I do have a flickering because the process is: get offset > render > get offset > adjust.

during the second get offset stage the user sees the newly rendered view with the old scroll position.

Quite ugly.

Has anyone come across this before? Any reliefs?


  1. in the first screenshot you can see the first collapsible - it shows all items - it has an uneven count of elements so the last row is not fill if I see all items
  2. second screenshot - I clicked ‘show all’ on the second collapsible - the first C gets collapsed and the second on is expanded. Because the page reflows I am now in the middle of the second (now expanded) collapsible
  3. position is adjusted

I want to eliminate the single frame rendered at 2

    GotOffsetBeforeChange cat (Ok offset) ->
        let
            _ = Debug.log "before" (diff)
            diff = offset.element.y - offset.viewport.y
            getOffset =
                Browser.Dom.getElement
                    (Resource.categoryToId cat)
            isAlreadyOpen = model.openSection == Just cat
            toggled =
                if isAlreadyOpen
                    then Nothing
                    else Just cat
        in
            (
            { model
            | openSection = toggled
            }
            , Task.attempt (GotOffsetAfterChange diff) getOffset
            )
    GotOffsetBeforeChange _ (Err _) -> (model, Cmd.none)
    GotOffsetAfterChange diffBefore (Ok el) ->
        let
            _ = Debug.log "beforeafter" (diffBefore, diffAfter)
            diffAfter = el.element.y - el.viewport.y
            diffBoth = diffAfter - diffBefore
            y = el.viewport.y + diffBoth
            scrollToOpenSection = Task.perform (always NoOp) (Browser.Dom.setViewport 0 y)
        in
            ( model
            , scrollToOpenSection
            )
    GotOffsetAfterChange _ (Err _) -> (model, Cmd.none)

First suggestion, I see that when the content grows, it pushes up. The browser automatically keeps scroll position if you add content to the bottom. Maybe you can rethink the CSS in a different way.

Then, since changing the CSS is a workaround, going back to the question: you want to change the dom, trigger a reflow, get the new size and the apply the scroll position all within the same render phase. I don’t think it is possible in Elm. You can start by sending a scroll position at the same time you change the view, to see if the CMD gets executed before or after adding the new cards. if you still see the scrolling happening after the redering then I would assume that it is not possible, and open a bug report If the flicker is not visible, then probably you can do it in a port, or you can try to have deterministic of cards in order to have scroll position deterministic.

@francescortiz ended up modeling the collapsible as a set, which means that only one element is modified at the time. This means no modifications outside of the user’s view of the page - no (possibly unexpected) side effects.

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