HTML Markup & Attributes
Heimdall is HTML-first. Every interaction helper eventually becomes ordinary markup that the browser runtime reads from the DOM.
Use this page when you want the raw attributes, the C# helper that emits them, and the minimum complete markup needed to make an interaction work.
1. The One Rule
If Heimdall behavior is confusing, inspect the rendered HTML. The runtime does not need a component protocol or JSON view model. It needs attributes on DOM elements.
C# helper
-> rendered HTML attribute
-> browser runtime reads the attribute
-> server action returns HTML
-> runtime swaps that HTML into the DOM2. Raw Markup Is Always Valid
You can write Heimdall attributes directly in Razor, MVC partials, static HTML, templates, or any server-rendered markup. The fluent helpers are convenience APIs, not a second runtime.
<button
type="button"
heimdall-content-click="cart.add"
heimdall-payload-from="self"
data-product-id="42"
heimdall-content-target="#cart"
heimdall-content-swap="outer">
Add to cart
</button>3. The Same Markup From C#
The fluent API keeps server-rendered markup strongly typed where useful, but the output is the same HTML contract.
button.Type("button");
button.Data("product-id", "42");
button.Heimdall()
.Click("cart.add")
.PayloadFromSelf()
.Target("#cart")
.SwapOuter();Terminology Note
A content action is a server method registered with [ContentInvocation]. An <invocation> element is a response directive for out-of-band DOM updates.
The directive does not call a server action. It is consumed from HTML already returned by one.
4. Native HTML Attribute Helpers
Every element builder supports generic attributes, boolean attributes, classes, data attributes, and ARIA attributes. Use the named helpers when they exist and Attr or Bool for the rest of the HTML platform.
// Named helpers
input.Id("email");
input.Type(Html.InputType.email);
input.Name("Email");
input.Value(model.Email ?? "");
input.Placeholder("you@example.com");
input.AutoComplete("email");
input.Required();
input.MinLength(5);
input.MaxLength(100);
input.Pattern(".+@.+\\..+");
// Generic escape hatches
input.Attr("inputmode", "email");
input.Attr("aria-describedby", "email-help email-error");
input.Data("tracking-field", "email");
input.Bool("disabled", model.IsSaving);Attribute values are encoded when rendered. Boolean attributes render by presence only when the supplied condition is true.
5. HTML Helper Cheat Sheet
These are the native HTML helper names exposed by the current server rendering API. When you need an attribute not listed here, use Attr or Bool.
Generic:
Attr(name, value)
Bool(name, on)
Class(classes...)
Id(value)
Style(css)
Role(value)
TitleAttr(value)
Data(key, value)
Aria(key, value)
Links and media:
Href(value)
Src(value)
Alt(value)
Rel(value)
Target(value)
Forms and inputs:
Type(value)
Type(Html.InputType.email)
Name(value)
Value(value)
Placeholder(value)
AutoComplete(value)
Min(value)
Max(value)
Step(value)
Pattern(value)
MaxLength(value)
MinLength(value)
Rows(value)
Cols(value)
Action(value)
Method(value)
EncType(value)
For(value)
Boolean:
Disabled()
Checked()
Selected()
ReadOnly()
Required()
Multiple()
AutoFocus()6. Content Triggers
A trigger attribute names the server action to invoke. The value is the resolved ContentInvocation id.
heimdall-content-click="orders.refresh"
heimdall-content-submit="orders.save"
heimdall-content-input="search.query"
heimdall-content-change="filters.change"
heimdall-content-keydown="search.accept"
heimdall-content-blur="profile.validate-name"
heimdall-content-hover="products.preview"
heimdall-content-visible="feed.load-more"
heimdall-content-scroll="activity.load-earlier"
heimdall-content-load="dashboard.summary"7. Trigger Helpers
The static helpers and fluent Heimdall wrapper emit the same trigger attributes. Use whichever shape is clearer for the file you are working in.
// Static attributes
HeimdallHtml.OnClick("orders.refresh")
HeimdallHtml.OnSubmit("orders.save")
HeimdallHtml.OnInput("search.query")
HeimdallHtml.OnChange("filters.change")
HeimdallHtml.OnKeyDown("search.accept")
HeimdallHtml.OnBlur("profile.validate-name")
HeimdallHtml.OnHover("products.preview")
HeimdallHtml.OnVisible("feed.load-more")
HeimdallHtml.OnScroll("activity.load-earlier")
HeimdallHtml.OnLoad("dashboard.summary")
// Fluent
element.Heimdall().Click("orders.refresh");
element.Heimdall().Submit("orders.save");
element.Heimdall().Input("search.query");
element.Heimdall().Change("filters.change");
element.Heimdall().KeyDown("search.accept");
element.Heimdall().Blur("profile.validate-name");
element.Heimdall().Hover("products.preview");
element.Heimdall().Visible("feed.load-more");
element.Heimdall().Scroll("activity.load-earlier");
element.Heimdall().Load("dashboard.summary");8. Target and Swap
Target chooses the DOM element that receives returned HTML. Swap chooses how that HTML is applied. If target is omitted, the triggering element is the fallback target. If swap is omitted, inner is used.
<button
heimdall-content-click="profile.refresh"
heimdall-content-target="#profile-card"
heimdall-content-swap="outer">
Refresh
</button>
<section id="profile-card">
...
</section>Supported swap values:
inner -> replace target children
outer -> replace the target element itself
beforeend -> append inside the target
afterbegin -> prepend inside the target
none -> do not apply the main response directly9. Target and Swap Helpers
The helper names map directly to the target and swap attributes.
element.Heimdall()
.Target("#profile-card")
.Swap(HeimdallHtml.Swap.Outer);
element.Heimdall().SwapInner();
element.Heimdall().SwapOuter();
element.Heimdall().SwapBeforeEnd();
element.Heimdall().SwapAfterBegin();
element.Heimdall().SwapNone();
// Static helpers
HeimdallHtml.Target("#profile-card")
HeimdallHtml.SwapMode(HeimdallHtml.Swap.Outer)10. Payload Attributes
Payload attributes tell the runtime what JSON body to send with the action request. Inline payload wins first, then payload-ref, then payload-from.
<!-- 1. Inline JSON object -->
<button
heimdall-content-click="reports.load"
heimdall-payload='{"page":2,"sort":"desc"}'>
Load
</button>
<!-- 2. Global object path on window -->
<button
heimdall-content-click="reports.load"
heimdall-payload-ref="App.Reports.Filters">
Refresh
</button>
<!-- 3. Source directive -->
<button
heimdall-content-click="reports.load"
heimdall-payload-from="closest-form">
Search
</button>11. Payload Source Values
Use the source that already owns the interaction data. Submit triggers also fall back to their form payload when no payload source is supplied.
closest-form
-> nearest containing form as FormData
self
-> the triggering element's dataset
closest-state
-> nearest data-heimdall-state JSON boundary
closest-state:weather
-> nearest data-heimdall-state-weather JSON boundary
#search-form
-> explicit selector that resolves to a form
ref:App.Reports.Filters
-> global object path on window12. Payload Helpers
The helpers serialize objects when needed and emit the same payload attributes shown above.
element.Heimdall().Payload(new { page = 2, sort = "desc" });
element.Heimdall().PayloadEmptyObject();
element.Heimdall().PayloadFromClosestForm();
element.Heimdall().PayloadFromSelf();
element.Heimdall().PayloadFromClosestState();
element.Heimdall().PayloadFromClosestState("weather");
element.Heimdall().PayloadRef("App.Reports.Filters");
element.Heimdall().PayloadFromRef("App.Reports.Filters");
// Static helpers
HeimdallHtml.Payload(new { page = 2 })
HeimdallHtml.PayloadFromDirective("closest-form")
HeimdallHtml.PayloadRef("App.Reports.Filters")
HeimdallHtml.PayloadFromClosestState("weather")13. State Attributes
State is just JSON stored on the DOM boundary that owns the current interaction context.
<div
id="counter-host"
data-heimdall-state='{"count":1}'>
...
</div>
<div
id="weather-host"
data-heimdall-state-weather='{"page":2,"zip":"48104"}'>
...
</div>State helper equivalents:
host.Heimdall().State(new CounterState { Count = 1 });
host.Heimdall().State("weather", new WeatherState { Page = 2 });
host.Heimdall().StateJson("{\"count\":1}");
host.Heimdall().StateJson("weather", "{\"page\":2}");
// Static helpers
HeimdallHtml.State(new CounterState { Count = 1 })
HeimdallHtml.State("weather", new WeatherState { Page = 2 })14. Modifier Attributes
Modifiers refine trigger behavior. They do not create a new action. They shape timing, filtering, browser defaults, in-flight behavior, and delegated trigger resolution.
heimdall-debounce="300"
-> delay input or change invocation until events settle
heimdall-key="Enter"
-> keydown only fires for Enter
heimdall-hover-delay="200"
-> hover only fires after pointer dwell
heimdall-visible-once="false"
-> visible may fire more than once
heimdall-scroll-threshold="160"
-> scroll fires near the end of the scroll region
heimdall-poll="5000"
-> repeat the load action every 5 seconds
heimdall-prevent-default="true"
-> prevent native browser behavior before invoking
heimdall-content-disable="true"
-> set disabled and aria-busy while request is in flight
heimdall-ignore="click"
-> stop delegated click resolution across this boundary
heimdall-scope="self"
-> trigger only when the event target is the trigger element15. Modifier Helpers
These helper names emit the modifier attributes. Boolean Heimdall attributes accept presence, true, 1, or yes as true; false, 0, or no as false.
element.Heimdall().DebounceMs(300);
element.Heimdall().Key("Enter");
element.Heimdall().HoverDelayMs(200);
element.Heimdall().VisibleOnce(false);
element.Heimdall().ScrollThresholdPx(160);
element.Heimdall().PollMs(5000);
element.Heimdall().PreventDefault();
element.Heimdall().Disable();
element.Heimdall().Ignore(HeimdallHtml.Trigger.Click);
element.Heimdall().IgnoreAll();
element.Heimdall().ScopeSelf();
element.Heimdall().ScopeClosest();
// Static helpers
HeimdallHtml.DebounceMs(300)
HeimdallHtml.Key("Enter")
HeimdallHtml.HoverDelayMs(200)
HeimdallHtml.VisibleOnce()
HeimdallHtml.ScrollThresholdPx(160)
HeimdallHtml.PollMs(5000)
HeimdallHtml.PreventDefault()
HeimdallHtml.Disable()
HeimdallHtml.Ignore(HeimdallHtml.Trigger.Click)
HeimdallHtml.Scope(HeimdallHtml.EventScope.Self)16. Runtime Defaults Worth Knowing
A few browser behaviors are intentionally useful without extra markup. Add the attributes when you want to make the behavior explicit or override it.
Default target:
triggering element
Default swap:
inner
Input debounce:
250ms unless heimdall-debounce is supplied
Change debounce:
0ms unless heimdall-debounce is supplied
Hover delay:
150ms unless heimdall-hover-delay is supplied
Visible once:
true unless heimdall-visible-once is false
Scroll threshold:
120px unless heimdall-scroll-threshold is supplied
Click and submit:
disable during request by default
Anchor click and form submit:
prevent default browser behavior by default
disabled or aria-disabled="true":
action does not run17. SSE Attributes
SSE attributes subscribe an element to a Bifrost topic. The topic chooses the stream. The event chooses the message type. Target and swap choose how received HTML is applied.
<div
id="orders-stream"
heimdall-sse="orders"
heimdall-sse-event="order.updated"
heimdall-sse-target="#orders"
heimdall-sse-swap="beforeend">
</div>
<!-- Alias for heimdall-sse -->
<div heimdall-sse-topic="orders"></div>SSE defaults: event is heimdall, target is the subscriber element, swap is none, and disable is false.
element.Heimdall()
.SseTopic("orders")
.SseEvent("order.updated")
.SseTarget("#orders")
.SseSwap(HeimdallHtml.Swap.BeforeEnd);
element.Heimdall().SseDisable();
// Static helpers
HeimdallHtml.SseTopic("orders")
HeimdallHtml.SseTopicAlias("orders")
HeimdallHtml.SseEvent("order.updated")
HeimdallHtml.SseTarget("#orders")
HeimdallHtml.SseSwapMode(HeimdallHtml.Swap.BeforeEnd)
HeimdallHtml.SseDisable()18. Response Directive Markup
Response directives are HTML elements returned by an action. The runtime consumes them as instructions and removes them from the final DOM.
<!-- Update another DOM region -->
<invocation
heimdall-content-target="#toast-manager"
heimdall-content-swap="afterbegin">
<div class="toast">Saved</div>
</invocation>
<!-- Wrap parser-sensitive payloads such as table rows -->
<invocation
heimdall-content-target="#orders-body"
heimdall-content-swap="beforeend">
<template>
<tr><td>Order 1001</td></tr>
</template>
</invocation>
<!-- Cancel the main swap but keep invocations -->
<abort reason="validation-failed"></abort>
<!-- Call an existing browser function -->
<javascript
function="window.App.toast.success"
args='["Saved"]'
timing="after">
</javascript>
<!-- Navigate the browser -->
<redirect url="/login"></redirect>19. Response Directive Helpers
Use the helpers from content actions so directive markup stays valid and easy to refactor.
return Html.Fragment(
RenderForm(model),
HeimdallHtml.Invocation(
targetSelector: "#toast-manager",
swap: HeimdallHtml.Swap.AfterBegin,
payload: ToastManager.Create(toast)),
HeimdallHtml.Abort("validation-failed")
);
return HeimdallHtml.JsInvokeVoid(
"window.App.toast.success",
"Saved");
return HeimdallHtml.Redirect("/login");20. Complete Form Example
This is a complete native HTML form boundary. It uses normal browser validation attributes plus Heimdall submit and payload attributes.
Rendered HTML
<form
id="contact-form"
heimdall-content-submit="contact.submit"
heimdall-payload-from="closest-form"
heimdall-content-target="#contact-form"
heimdall-content-swap="outer">
<label for="contact-email">Email</label>
<input
id="contact-email"
name="Email"
type="email"
autocomplete="email"
required
maxlength="100"
aria-describedby="contact-email-help" />
<small id="contact-email-help">
Use an address we can reply to.
</small>
<label for="contact-message">Message</label>
<textarea
id="contact-message"
name="Message"
required
minlength="10"
maxlength="1000"
rows="5"></textarea>
<button type="submit">Send</button>
</form>C# Builder
form.Id("contact-form");
form.Heimdall()
.Submit("contact.submit")
.PayloadFromClosestForm()
.Target("#contact-form")
.SwapOuter();
form.Label(label =>
{
label.For("contact-email");
label.Text("Email");
});
form.Input(Html.InputType.email, input =>
{
input.Id("contact-email");
input.Name("Email");
input.AutoComplete("email");
input.Required();
input.MaxLength(100);
input.Aria("describedby", "contact-email-help");
});
form.TextArea(textarea =>
{
textarea.Id("contact-message");
textarea.Name("Message");
textarea.Required();
textarea.MinLength(10);
textarea.MaxLength(1000);
textarea.Rows(5);
});21. Complete Stateful Button Example
This is the smallest stateful component pattern: the host carries state, the button sends closest-state, and the response replaces the host.
<div
id="counter-host"
data-heimdall-state='{"count":1}'>
<p>Count: 1</p>
<button
type="button"
heimdall-content-click="counter.increment"
heimdall-payload-from="closest-state"
heimdall-content-target="#counter-host"
heimdall-content-swap="outer">
Increment
</button>
</div>22. Razor or MVC Partial Example
In MVC or Razor partials, write the attributes directly. The Heimdall runtime does not care whether the element came from Razor, a fluent builder, or a template.
@model OrderFilter
<form
id="order-filter"
heimdall-content-submit="orders.filter"
heimdall-payload-from="closest-form"
heimdall-content-target="#orders-table"
heimdall-content-swap="inner">
<input
name="Search"
type="search"
value="@Model.Search"
autocomplete="off"
heimdall-content-input="orders.filter"
heimdall-payload-from="closest-form"
heimdall-debounce="300"
heimdall-content-target="#orders-table"
heimdall-content-swap="inner" />
<button type="submit">Apply</button>
</form>
<div id="orders-table">
@await Html.PartialAsync("_OrderTable", Model)
</div>23. Implementation Checklist
When an interaction does not work, walk the markup in this order. Almost every issue becomes visible in the rendered HTML.
1. Is the runtime script loaded?
/_content/HeimdallFramework.Web/heimdall-bundle.min.js
2. Is there exactly one trigger attribute with an action id?
heimdall-content-click="orders.refresh"
3. Does the action id match the resolved ContentInvocation id?
[ContentInvocationPrefix("orders")]
[ContentInvocation("refresh")]
-> orders.refresh
4. Does the payload source point to real data?
closest-form, closest-state, self, selector, or ref:path
5. Does the target selector match an element?
heimdall-content-target="#orders"
6. Is the swap mode appropriate?
inner, outer, beforeend, afterbegin, or none
7. Is the element disabled?
disabled or aria-disabled="true" prevents invocationNext Steps
After the attribute surface is clear, the focused concept pages explain how each group behaves in real workflows.