Basic
Affix an element to the viewport after it reaches the offset.
Code
<div class="playground-affix-basic-container">
<div class="playground-affix-spacer-sm"></div>
<%= render Hakumi::Affix::Component.new(offset_top: 0) do %>
<%= render(Hakumi::Button::Component.new(type: :primary)) { "Pinned controls" } %>
<% end %>
<div class="playground-affix-content-lg">
Scroll to see the button stick to the top of the viewport.
</div>
</div>
Affixed state callback
Listen for <code>hakumi--affix:change</code> when the state toggles.
Code
<div class="playground-affix-basic-container">
<div style="margin-bottom: 12px;">
<%= render Hakumi::Typography::Text::Component.new(id: "affix-callback-status", strong: true) { "Not affixed" } %>
</div>
<div class="playground-affix-spacer-sm"></div>
<%= render Hakumi::Affix::Component.new(id: "affix-callback", offset_top: 0) do %>
<%= render(Hakumi::Button::Component.new(type: :default)) { "Watch affix state" } %>
<% end %>
<div class="playground-affix-content-lg">
Scroll this view to trigger <code>hakumi--affix:change</code> events.
</div>
</div>
<script>
(() => {
var wire = () => {
var affix = document.getElementById("affix-callback")
var status = document.getElementById("affix-callback-status")
if (!affix || !status) return false
var update = (event) => {
var affixed = event?.detail?.affixed
status.textContent = affixed ? "Affixed" : "Not affixed"
}
affix.addEventListener("hakumi--affix:change", update)
if (affix.hakumiAffix?.isAffixed) {
update({ detail: { affixed: affix.hakumiAffix.isAffixed() } })
}
return true
}
if (wire()) return
var onReady = () => {
if (wire()) {
document.removeEventListener("turbo:load", onReady)
window.removeEventListener("load", onReady)
}
}
document.addEventListener("turbo:load", onReady)
window.addEventListener("load", onReady)
})()
</script>
Scroll container
Affix within a custom scrollable container using <code>target_selector</code>.
Code
<div class="playground-affix-container">
<div id="affix-scroll-target" class="playground-affix-scroll" style="height: 240px;">
<div style="height: 120px; margin-bottom: 16px;"></div>
<%= render Hakumi::Affix::Component.new(offset_top: 0, target_selector: "#affix-scroll-target") do %>
<%= render(Hakumi::Button::Component.new(type: :primary)) { "Pinned in container" } %>
<% end %>
<div class="playground-affix-content">
Scroll the container to keep the button affixed to its top edge.
</div>
</div>
</div>
JavaScript API
Control the affix via <code>element.hakumiAffix</code>.
Code
<div class="playground-affix-container">
<%= render Hakumi::Typography::Text::Component.new(id: "affix-api-status", strong: true) { "Not affixed" } %>
<div id="affix-scroll" class="playground-affix-scroll" style="margin-top: 12px;">
<div class="playground-affix-spacer"></div>
<%= render Hakumi::Affix::Component.new(id: "affix-api", offset_top: 0, target_selector: "#affix-scroll") do %>
<%= render(Hakumi::Button::Component.new(type: :default)) { "API controlled" } %>
<% end %>
<div class="playground-affix-content">
Scroll to toggle the affix state, or call the JavaScript API.
</div>
</div>
</div>
<script>
(() => {
var wire = () => {
var affix = document.getElementById("affix-api")
var status = document.getElementById("affix-api-status")
if (!affix || !status) return false
var update = (event) => {
var affixed = event?.detail?.affixed
status.textContent = affixed ? "Affixed" : "Not affixed"
}
affix.addEventListener("hakumi--affix:change", update)
if (affix.hakumiAffix?.isAffixed) {
update({ detail: { affixed: affix.hakumiAffix.isAffixed() } })
}
return true
}
if (wire()) return
var onReady = () => {
if (wire()) {
document.removeEventListener("turbo:load", onReady)
window.removeEventListener("load", onReady)
}
}
document.addEventListener("turbo:load", onReady)
window.addEventListener("load", onReady)
})()
</script>
Affix API
| Prop | Type | Default | Description |
|---|---|---|---|
body |
String |
- |
Fallback content used when no block is provided (used by renderComponent). |
offset_top |
Number |
0 |
Distance from the top of the viewport/container to trigger affix. |
offset_bottom |
Number |
- |
Distance from the bottom of the viewport/container to trigger affix (exclusive with offset_top). |
target_selector |
String |
window |
CSS selector for the scroll container; defaults to window. |
z_index |
Integer |
- |
Optional z-index applied to the affixed content. |
on_change |
String |
- |
Stimulus action invoked on state change (e.g., <code>playground--affix#update</code>). |
content slot |
Slot |
- |
Affixed content rendered inside the component. |
**html_options |
Keyword args |
- |
Additional HTML attributes merged into the root element. |
JavaScript API (element.hakumiAffix)
| Prop | Type | Default | Description |
|---|---|---|---|
check() |
Function |
- |
Recalculate affix position and state. |
update() |
Function |
- |
Alias for check(). |
isAffixed() |
Function |
- |
Return whether the element is currently affixed. |
getState() |
Function |
- |
Return current offsets and affixed state. |
setOffsetTop(value) |
Function |
- |
Update the top offset and remove the bottom offset. |
setOffsetBottom(value) |
Function |
- |
Update the bottom offset and remove the top offset. |