Developer Docs - Mobile Docs - Requests and Targeting
Three questions decide what a Helix interaction does: what it calls, where the answer goes, and how it gets placed. This page covers all three.
Verbs
Set one on any element and it becomes interactive.
Verbs never inherit. A verb belongs to the element it is written on, always.
What gets wired
Use Hx.Trigger when you want something other than the natural interaction.
Attribute order in your markup does not matter. The wiring is recalculated whenever a verb or Hx.Trigger changes, so a trigger written before or after its verb behaves the same.
Route rules worth knowing
Exactly one verb per element. Two verbs on one element does not pick a winner. It renders an error notification and sends nothing.
The short ^/endpoint-slug form needs an ambient application. It resolves against the nearest enclosing Lava Application Content block that has an Application configured. Outside one, the route fails on the device with a message telling you to use the long form, and no request is sent. Inside a cover sheet, the ambient application is inherited from whatever opened the sheet.
A route must start with ^/ or /. A bare ^endpoint with no slash, or a relative api/..., is rejected before anything leaves the device.
Query strings stay in the URL for every verb, including POST. Only included form values move to the body. See Forms and Values.
A verb is not limited to ^ Lava Application routes. Any absolute server path is sent as-is, carrying the signed-in person's credentials, so a fragment containing <ContentView Hx.Post="/api/v2/..." Hx.Trigger="load" /> fires an authenticated request as that person the moment the screen renders. Read Security before you widen who can edit Applications.
Hx.Target
Ids are page-global
Ids are looked up across the whole page, so one block can target an element inside another block. This is the capability Callbacks never had; a Callback could only ever replace its own block.
Bindings keep working wherever a fragment lands. The standard client commands (PopPage, ShowToast, OpenBrowser, and so on) are available in every block, so Command="{Binding PopPage}" in your fragment works no matter which block hosts it.
"this" means the element that carries the attribute
Not the element that fired. Think of the English word: whoever says "this" is pointing from where they stand.
Written directly on your trigger, the two are the same element:
Inherited from an ancestor, they are not:
The TextBox fires the request, but it declares no Hx.Target of its own, so it inherits the container's. this resolves to the FieldContainer, and the response replaces that whole container: label, input, and all.
That is exactly what inline validation needs. Your endpoint returns a fresh FieldContainer holding the field plus an error message. If this meant "the element that fired," the swap would replace only the TextBox and there would be nowhere to put the message. See Forms and Values.
Other targeting details
Id matching is case-sensitive. Hx.Target="grouplist" will not find Hx.Id="GroupList". The reserved words this and coversheet are case-insensitive, as are the swap strategy names.
x:Name works as a fallback. Helix looks for a matching Hx.Id first, then for a matching x:Name. So an explicit Hx.Id always wins if both exist.
An unresolvable target is an error, not a no-op. You get an error notification and no request.
A cover sheet is a separate scope. A fragment in a sheet cannot target the presenting page, and the page cannot target into the sheet. See Cover Sheets.
Hx.Id
Hx.Id fills the role of the DOM id attribute. It does two jobs: it makes an element targetable, and it gives a form control the name its value is submitted under.
Details:
Hx.Swap
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 shell runs the motion itself. swap: is the exit, settle: is the enter. Times accept 150ms, 0.2s, or bare milliseconds.
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 an inherited animation means restating the strategy
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.
Other animation details
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.
"after" constrains the parent
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: a block's parent is a Zone, which is a grid. Since the default target is the enclosing block, Hx.Swap="after" with no Hx.Target always fails. Point after at a target that sits inside a stack layout.
Other swap details
An incompatible target fails visibly. You get an error notification next to the target, never a silent no-op.
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.
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. See Animating a swap for the modifiers that are recognized.
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.
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 Load More list, and it is exactly what revealed plus after will avoid once revealed exists.