Skip to content

Scroll.Task

This is everything Scroll.Task offers:

Function Type
scroll (ScrollBuilder -> ScrollBuilder) -> Task ScrollError (List ScrollOk)
scrollEach (ScrollBuilder -> ScrollBuilder) -> Task Never (List (Result ScrollError ScrollOk))
delay Int -> ScrollBuilder -> ScrollBuilder
duration Int -> ScrollBuilder -> ScrollBuilder
speed Float -> ScrollBuilder -> ScrollBuilder
easing Easing -> ScrollBuilder -> ScrollBuilder

Two triggers, plus timing and easing. Same fire-and-forget mental model as Cmd, but the result is typed - so you can:

  • find out whether the scroll succeeded or failed,
  • chain a scroll with other Tasks (e.g. fetch data, then scroll to it),
  • decide what to do when one scroll in a sequence fails.

If you don't need any of those, Cmd is simpler. If you need pause / resume / mid-flight redirect / per-frame progress, you want Sub.

Example

View Example

View Source Code
module Scroll.Task.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.Task as Task exposing (ScrollBuilder)
import Task



-- 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
    | Failed String


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



-- UPDATE


type Msg
    = ScrollTo String
    | ScrollResult (Result Task.ScrollError (List Task.ScrollOk))


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

        ScrollResult (Ok _) ->
            ( { model | status = Arrived }, Cmd.none )

        ScrollResult (Err (Task.ScrollError err)) ->
            let
                containerLabel =
                    case err.container of
                        Task.Document ->
                            "document"

                        Task.Container id ->
                            id
            in
            ( { model | status = Failed ("Scroll failed for container: " ++ containerLabel) }
            , 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" )

                Failed err ->
                    ( "#ef4444", "✗ " ++ err )
    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

Task.scroll returns a Task ScrollError (List ScrollOk). Turn it into a Cmd with Task.attempt:

View Source Code
import Scroll.Engine.Task as Task
import Task as TaskCore


type Msg
    = ScrollTo String
    | GotScrollResult (Result Task.ScrollError (List Task.ScrollOk))


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ScrollTo targetId ->
            ( model
            , scrollToSection targetId
                |> Task.scroll
                |> TaskCore.attempt GotScrollResult
            )

        GotScrollResult (Ok _) ->
            ( { model | status = "Arrived" }, Cmd.none )

        GotScrollResult (Err _) ->
            ( { model | status = "Scroll failed" }, Cmd.none )

Handling Results

ScrollOk

ScrollOk is a type alias for { container : Container, targetElementId : Maybe String }.

It is returned for every scroll that completed successfully, and tells you which container finished and, if you targeted a specific element, which one.

Field Type Description
container Container The container that was scrolled (Document or Container "id")
targetElementId Maybe String The element ID, if toElement was used
View Source Code
GotScrollResult (Ok results) ->
    let
        arrived =
            results
                |> List.filterMap .targetElementId
                |> String.join ", "
    in
    ( { model | status = "Arrived at: " ++ arrived }
    , Cmd.none
    )

ScrollError

Returned when a scroll fails. Tells you what was being scrolled, which target was involved, and the underlying DOM error - usually because the container or target element wasn't in the DOM.

Field Type Description
container Container The container that was being scrolled
targetElementId Maybe String The element ID, if one was specified
domError Dom.Error The underlying browser DOM error
View Source Code
GotScrollResult (Err (Task.ScrollError err)) ->
    let
        target =
            err.targetElementId
                |> Maybe.withDefault "(no element)"
    in
    ( { model | status = "Scroll failed for: " ++ target }
    , Cmd.none
    )

scroll vs scrollEach

Task.scroll is fail-fast: the first scroll to fail ends the task immediately, and later scrolls in the same builder are skipped. You only get Ok if every scroll completed - and at that point the payload lists them all, in order. The moment one fails you get Err with no record of the ones that succeeded before it.

If you'd rather get a result for every target - failures included - use Task.scrollEach:

View Source Code
import Scroll.Task as Task
import Task

type Msg
    = ScrollAttempts (List (Result ScrollError ScrollOk))


ScrollToSequence ->
    ( model
    , scrollSequence
        |> Task.scrollEach
        |> Task.perform ScrollAttempts
    )

scrollEach always completes and returns one Result per target.

Task Composition

Because Task.scroll is a regular Task, you can compose it with anything else that returns a Task. Example - fetch data, then scroll to wherever the response points:

View Source Code
import Scroll.Task as Task
import Task

fetchArticle "article-123"
    |> Task.andThen
        (Task.scroll << scrollToSection << .anchorId)
    |> Task.attempt GotResult

Caveats

Task suffers the two pre-calculation trade-offs that Cmd does:

  • Timing drift on busy pages or high-refresh-rate displays.
  • Re-triggering doesn't cancel - parallel scrolls compete for the container, longest wins.

Both are fixed by Sub if they matter to you.

Next Steps

Need state, mid-flight redirects, pause / resume, or per-frame progress?

Sub Engine →