Skip to content

Scroll.Cmd

This is everything Scroll.Cmd offers:

Function Type
scroll msg -> (ScrollBuilder -> ScrollBuilder) -> Cmd msg
delay Int -> ScrollBuilder -> ScrollBuilder
duration Int -> ScrollBuilder -> ScrollBuilder
speed Float -> ScrollBuilder -> ScrollBuilder
easing Easing -> ScrollBuilder -> ScrollBuilder

One trigger, plus timing and easing. That's the whole engine.

And that's the point: if your scroll is "send the user there, I don't need to know anything more", you can wire it up in two lines of update and there is nothing else to learn. No state in the model, no subscription, no event union.

If you need anything beyond "go" - typed success/failure, mid-flight redirects, live progress, pause/resume - the other engines have it. But for plain navigation, this is the whole story.

Example

View Example

View Source Code
module Scroll.Cmd.FirstScroll.Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, id, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
import Scroll.Builder as Scroll
import Scroll.Engine.Cmd as Cmd exposing (ScrollBuilder)



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }



-- MODEL


type alias Model =
    { status : ScrollStatus }


type ScrollStatus
    = Idle
    | Scrolling
    | Arrived


init : ( Model, Cmd Msg )
init =
    ( { status = Idle }, Cmd.none )



-- UPDATE


type Msg
    = ScrollTo String
    | ScrollComplete


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ScrollTo targetId ->
            ( { model | status = Scrolling }
            , Cmd.scroll ScrollComplete <|
                scrollToElement targetId
            )

        ScrollComplete ->
            ( { model | status = Arrived }, Cmd.none )





scrollToElement : String -> ScrollBuilder -> ScrollBuilder
scrollToElement targetId =
    Scroll.forContainer "scroll-container"
        >> Scroll.toElement targetId
        >> Scroll.speed 250
        >> Scroll.easing BounceOut
        >> Scroll.build



-- VIEW


view : Model -> Html Msg
view model =
    div [ class "example-stage" ]
        [ div [ class "example-controls" ]
            [ styledButton (ScrollTo "top-element") "Scroll to Top"
            , styledButton (ScrollTo "middle-element") "Scroll to Middle"
            , styledButton (ScrollTo "bottom-element") "Scroll to Bottom"
            ]
        , statusBar model.status
          div
            [ id "scroll-container"
            , style "width" "100%"
            , style "flex" "1 1 auto"
            , style "min-height" "0"
            , style "box-sizing" "border-box"
            , style "overflow-y" "auto"
            , style "border" "2px solid #333"
            , style "border-radius" "8px"
            ]
            [ scrollContent ]

        ]


statusBar : ScrollStatus -> Html msg
statusBar status =
    let
        ( color, message ) =
            case status of
                Idle ->
                    ( "#94a3b8", "Click a button to scroll" )

                Scrolling ->
                    ( "#f59e0b", "Scrolling..." )

                Arrived ->
                    ( "#22c55e", "✓ Scroll complete" )
    in
    div
        [ style "padding" "6px 14px"
        , style "border-radius" "6px"
        , style "background-color" color
        , style "color" "white"
        , style "font-size" "clamp(11px, 1.8vmin, 14px)"
        , style "font-weight" "500"
        , style "flex" "0 0 auto"
        ]
        [ text message ]


styledButton : Msg -> String -> Html Msg
styledButton msg label =
    button
        [ onClick msg
        , class "ui-action-button"
        , style "background-color" "#6366f1"
        ]
        [ text label ]


scrollContent : Html Msg
scrollContent =
    div
        [ style "padding" "20px" ]
        [ targetElement "top-element" "Top Section" "#4CAF50"
        , spacer
        , targetElement "middle-element" "Middle Section" "#2196F3"
        , spacer
        , targetElement "bottom-element" "Bottom Section" "#9C27B0"
        ]


targetElement : String -> String -> String -> Html Msg
targetElement elementId label color =
    div
        [ id elementId
        , style "padding" "40px"
        , style "background-color" color
        , style "color" "white"
        , style "border-radius" "8px"
        , style "text-align" "center"
        , style "font-size" "24px"
        ]
        [ text label ]


spacer : Html Msg
spacer =
    div [ style "height" "400px" ] []

Trigger

Use scroll to trigger the scroll animation.

View Source Code
import Scroll.Engine.Cmd as Cmd


type Msg
    = ScrollTo String
    | ScrollComplete


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ScrollTo targetId ->
            ( model
            , Cmd.scroll ScrollComplete <|
                scrollToSection targetId
            )

        ScrollComplete ->
            ( model, Cmd.none )

ScrollComplete fires when the scroll finishes - successfully or not, you can't tell the difference, and that's deliberate. If the target element isn't in the DOM, the scroll fails silently and reports completion.

If you need a real result, use Task or Sub.

Multiple Targets in One Call

Chain build calls into a single pipeline to dispatch several scrolls at once. The completion message fires once per target:

View Source Code
scrollSidebarAndMain : ScrollBuilder -> ScrollBuilder
scrollSidebarAndMain =
    Scroll.forContainer "sidebar"
        >> Scroll.toElement "nav-item-3"
        >> Scroll.speed 300
        >> Scroll.build
        >> Scroll.forContainer "main-content"
        >> Scroll.toElement "section-3"
        >> Scroll.speed 400
        >> Scroll.build

Caveats

This Engine has two real trade-offs - both shared with Task, and both fixed by Sub if they matter to you.

Timing Drift

When you ask for a duration of 3000, you're saying "I want this scroll to take about three seconds". Cmd plans out all the in-between scroll positions up front and hands them to Elm as a chain of small tasks: set position, then set the next, then the next...

There's no clock between those steps. The runtime just runs them back-to-back as fast as it can, and the browser paints whatever it has ready on its next refresh. So the actual time you see depends on how busy the page is and how fast the display refreshes - the duration is a target, not a guarantee.

If you need a duration you can rely on to the millisecond, use Sub.

Re-Triggering Doesn't Cancel

Calling Cmd.scroll again while a scroll is in flight doesn't replace the running scroll - both run in parallel and compete for control of the container. The longer one usually wins, often finishing short of its real target.

If you have to stay on Cmd, prevent overlap in your own code: ignore new triggers while a scroll is active, debounce rapid input, or queue the latest target and only dispatch it after ScrollComplete.

Next Steps

Need to know whether the scroll succeeded, or compose a scroll with other tasks?

Task Engine →