Developer Docs - Mobile Docs - Swaps and Animations

Hx.Swap decides how a response is placed once it arrives: whether the fragment replaces the target's content or the target itself, is added alongside what is already there, feeds a list, or is discarded, and whether the change animates.

Which route a request calls and which element receives the response are covered in Requests and Targeting. This page picks up from there, once the target is known.

Swap strategies

An incompatible target fails visibly. You get an error notification next to the target, never a silent no-op.

Border targets

Border cannot be an inner target. This one catches almost everyone, because Border is the container the mobile styling docs steer you toward. inner needs a ContentView or a Layout, and MAUI's Border is neither, even though it has a Content property.



    



    
        

The same applies to Rock:NotificationBox and Rock:Validator, which are both Border-derived. Your other option is to target the Border itself with outer, which cares about the parent rather than the target.

Using "after"

after constrains the parent, not the target. Every other strategy puts the fragment inside the target. after puts it beside the target, so the element that has to accept a new child is the target's parent:

          
    

That works because in a stack layout child order is visual order. Inserting at the next index really does mean "appears below."

A Grid does not work that way. It positions children by Grid.Row and Grid.Column, and child order means nothing visually. A new child with no row set lands in the first cell, on top of whatever is already there. AbsoluteLayout is the same story with LayoutBounds. So after refuses both, and shows the incompatible-target error rather than silently overlapping your content.

outer gets away with a Grid parent because it replaces the target, so it can copy the target's exact row and column and land where the old element was. after would need the next cell, and in a grid there is no such thing.

One consequence to remember: with no Hx.Target, the strategy applies to the element that made the request. append and prepend need that element to be a Layout. after inserts beside it, so its parent has to be a flow layout, and a Button sitting directly in a Grid fails for exactly the reason above. Give these strategies an explicit Hx.Target whenever the element that asks is not the element that should change.

Using "outer"

outer only preserves Grid positioning. outer removes the target and puts the fragment where it was, which only works if the replacement inherits how the target was positioned. The only attached properties copied over are the grid ones: Row, Column, RowSpan, ColumnSpan.



    



    



    

FlexLayout is a milder version of the same problem. The fragment keeps its place in the flow, because flex follows child order, but any Basis, Grow, Shrink, AlignSelf, or Order set on the target is lost and the replacement sizes itself with the defaults.

This one fails silently. You get no error notification, because the swap did succeed; the fragment is just in the wrong place. If the element you want to replace is positioned absolutely or flex-tuned, wrap it: put a container in that slot and inner-swap its contents, so the positioned element itself is never replaced.

After an outer swap, re-declare what has to survive. outer replaces the target element itself, so the element carrying its Hx.Id and its inheritable attributes is gone. Inheritance keeps this cheap: declare them once on the replacement's own root and the triggers inside it inherit again. inner preserves the target, so it needs no re-declaration.



    

Load More carries only its verb. It inherits Hx.Target="this" from the container, and this means the element that declared it, so the next tap replaces this whole batch with the following one.

Load More buttons

after does not give you infinite scroll on its own. That pattern needs two pieces: a revealed trigger that fires when an element scrolls into view, and an after swap that inserts the next batch beside it. Only the second one exists today. For a list that pages as the person scrolls, feed a CollectionView with items-append instead, as described in Data Lists and Paging.

Substituting a tap for the missing trigger does not work:


after leaves the target in place, so the button survives its own request and ends up sitting above the rows it just loaded, stranded in the middle of the list. And because the route is server-rendered text, it still says page=2, so tapping again reloads the batch you already have.

Keep Load More on outer targeting this. The button replaces itself with the next batch plus a fresh button carrying the next page:



    

The one cost is that each batch nests one layout deeper than the last, because every batch replaces a button that lived inside the previous batch. That is fine for a short Load More list. For a long one, use an items swap, which keeps every row in a single native list.

Animating a swap

A swap is instant unless you ask for motion. Everything after the strategy is optional, and the order of the modifiers does not matter:

 [animate:] [swap:

animate: takes none, fade, slide-up, slide-down, slide-left, slide-right, or scale. Slide directions name the direction of travel, so slide-up arrives from below and leaves upward.

easing: takes linear, ease-in, ease-out, ease-in-out, spring, or bounce. Left alone, content accelerates as it leaves and decelerates as it arrives, which is what you usually want; setting easing: explicitly overrides both halves with the one curve.

swap: and settle: keep the web's words but not quite its meaning. On the web they are phase holds that give CSS time to transition. Here they are the animation durations themselves, because MAUI has no CSS transition engine and the app runs the motion itself. swap: is the exit, settle: is the enter. Times accept 150ms, 0.2s, or bare milliseconds.

An unknown modifier is an error. Unlike Hx.Trigger, which ignores what it does not recognize, a swap declaration fails loudly. That is deliberate: a silently ignored animate:fadde is indistinguishable from "no animation configured," which you cannot debug by looking at the screen. The modifiers and values above are the ones recognized.

Picking a motion. A reasonable default: fade when content is replaced in place, a slide-* when content is added alongside what is already there, and scale for a single element appearing on its own. Reach for the timing and easing modifiers only when the default 150/200 pair feels wrong on a real device.


Overriding animations

Hx.Swap inherits as one string, not as separate settings. A child cannot add or replace a single modifier; whatever it declares replaces the entire declaration, strategy included.



    
    

This is the one place the whole-string design bites. It is also why declaring motion low, on the elements that need it, ages better than declaring it high and fighting it in the children.