DEPRECATED - Drop-in router with transitions for animated, single page apps in Elm.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

Elm Transit Router

elm package install etaque/elm-transit-router

Drop-in router with animated route transitions for single page apps in Elm, extracted from (and used in) Tacks.


  • Takes care of the history interaction and actions plumbing
  • Enables animated transition on route changes, thanks to elm-transit
  • Provides a simple Signal.Address String for navigation

There is a projet example with a minimal usage of the router right here.


To be able to provide animated route transitions, TransitRouter (and Transit underneath) works by action delegation: it will be able to emit (Action, Effects Action) by knowing how to wrap his own TransitRouter.Action type into your app's Action type. This is why the actionWrapper is needed in config below.


Say you have an Action type wrapping TransitRouter.Action:

type Action = Foo | ... | RouterAction (TransitRouter.Action Route) | NoOp

Also a Route type to describe all routes in your apps:

type Route = Home | ... | NotFound | EmptyRoute

You must then extend your model with WithRoute on Route type:

type alias Model = TransitRouter.WithRoute Route 
  { foo: String }

Your Model is now enabled to work with TransitRouter. Initialize it with the EmptyRoute that should render nothing in your view, to avoid content flashing on app init.

initialModel : Model
initialModel =
  { transitRouter = TransitRouter.empty EmptyRoute
  , foo = ""


A config should be prepared:

routerConfig : TransitRouter.Config Route Action Model

-- which expands to:
routerConfig :
  { mountRoute : Route -> Route -> Model -> (Model, Effects Action)
  , getDurations : Route -> Route -> Model -> (Float, Float)
  , actionWrapper : TransitRouter.Action Route -> Action
  , routeDecoder : String -> Route
  • In mountRoute, you'll provide what should be done in your update when a new route is mounted. The Route params are previous and new routes.

  • In getDurations, you'll return the transition durations, given previous/new route and current model. Write \_ _ _ -> (50, 200) if you always want an exit of 50ms then an enter of 200ms. You mountRoute will happend at the end of exit.

  • actionWrapper will be used to transform internal TransitAction.Action to your own Action.

  • routeDecoder takes the current path as input and should return the associated route. See my elm-route-parser package for help on this.

It's now time to wire init and update functions:

init : String -> (Model, Effects Action)
init path =
  TransitRouter.init routerConfig path initialModel

This will parse and mount initial route on app init. You can get initial path value by setting up a port in main and provide current path from JS side.

In update, the lib will take care of routes updates and transition control.

update : Action -> Model -> (Model, Effects Action)
update action model =
  case action of

    NoOp ->
      (model, Effects.none)

    RouterAction routeAction ->
      TransitRouter.update routerConfig routeAction model

    Foo ->
      (doFoo model, Effects.none)


This is where visible part of routing happens. You just have to match current route to render the current route's view, using getRoute:

contentView : Address Action -> Model -> Html
contentView address model =
  case (TransitRouter.getRoute model) of

    Home ->
      homeView address model

    --- and so on

Now for animations, there is getTransition, to be used with elm-transit-style (or directly with Transit.getStatus and Transit.getValue from elm-transit).

view : Address Action -> Model -> Html
view address model =
    [ style (TransitStyle.fadeSlideLeft 100 (getTransition model)) ]
    [ contentView address model ]

Links: use pushPathAddress for clink handling, for instance within that kind of helper:

clickTo : String -> List Attribute
clickTo path =
  [ href path
  , onWithOptions
      { stopPropagation = True, preventDefault = True }
      (\_ -> message TransitRouter.pushPathAddress path)

Routing with Effects

Suppose you are in the module of one of the screens and you want to switch routes after handling an action (for instance after handling the result from a task). You can do this by using the redirect effect:

redirect : Routes.Route -> Effects ()
redirect route =
  Routes.toPath route
    |> Signal.send TransitRouter.pushPathAddress
    |> Effects.task

In the update function of the screen module you will not have access to the RouteAction action, since it is defined in the main app module. To be able to make the effect work in your update function, map it to a null operation:

update : Action -> Model -> (Model, Effects Action)
update action model =
  case action of

    NoOp ->
      (model, Effects.none)

    TaskCompleted ->
      (model, (\_ -> NoOp) (redirect Home))

Subrouting transitions

If you app contains submenus, you might want to adapt the scope of transition animation, ie you only want to animate the submenu content when you switch from a submenu item to another, not the whole content of your page.

A good way to do that is to create a type to indicate the current route switch happening, and to store it in your model, so you will be able to adapt the animation in your views. Let's say you have an admin submenu:

type RouteSwitch = Global | InAdmin | NoSwitch

type alias Model = TransitRouter.WithRoute Route 
  { foo: String
  , routeSwitch : RouteSwitch

Types are in place, but we still need to set routeSwitch value, and Config.mountRoute is the right place for that as it provides previous and new route, so you can compare them and decide what is the current switch:

mountRoute : Route -> Route -> Model -> (Model, Effects Action)
mountRoute prevRoute newRoute model =
    routeSwitch = case (prevRoute, newRoute) of
      (Admin _, Admin _) ->
      _ ->
    newModel = { model | routeSwitch = routeSwitch }
    case newRoute of

Then you have everything in your hands in order to show animation in the right view, be it global content or admin content only.