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 HakumiComponents::Affix::Component.new(offset_top: 0) do %>
<%= render(HakumiComponents::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 hakumi--affix:change event when the state toggles.
Code
<div class="playground-affix-basic-container">
<div style="margin-bottom: 12px;">
<%= render HakumiComponents::Typography::Text::Component.new(id: "affix-callback-status", strong: true) { "Not affixed" } %>
</div>
<div class="playground-affix-spacer-sm"></div>
<%= render HakumiComponents::Affix::Component.new(id: "affix-callback", offset_top: 0) do %>
<%= render(HakumiComponents::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.hakumiComponent.api?.isAffixed) {
update({ detail: { affixed: affix.hakumiComponent.api.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 target_selector.
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 HakumiComponents::Affix::Component.new(offset_top: 0, target_selector: "#affix-scroll-target") do %>
<%= render(HakumiComponents::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 programmatically via element.hakumiComponent.api.
Code
<div class="playground-affix-container">
<%= render HakumiComponents::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 HakumiComponents::Affix::Component.new(id: "affix-api", offset_top: 0, target_selector: "#affix-scroll") do %>
<%= render(HakumiComponents::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.hakumiComponent.api?.isAffixed) {
update({ detail: { affixed: affix.hakumiComponent.api.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>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
body |
String |
nil |
Fallback content used when no block is provided |
offset_top |
Number |
nil |
Distance in pixels from the top of the viewport/container to trigger affix (defaults to 0 if both offsets are nil) |
offset_bottom |
Number |
nil |
Distance in pixels from the bottom of the viewport/container to trigger affix (mutually exclusive with offset_top) |
target_selector |
String |
nil |
CSS selector for the scroll container (defaults to window if not specified) |
z_index |
Integer |
nil |
Z-index value applied to the affixed content |
on_change |
String |
nil |
Stimulus action string invoked on affix state change |
Slots
| Prop | Type | Default | Description |
|---|---|---|---|
content |
Slot |
nil |
Content to be affixed |
HTML Options
| Prop | Type | Default | Description |
|---|---|---|---|
**html_options |
Keyword args |
{} |
Additional HTML attributes merged into the root element |
JavaScript API
Access via element.hakumiComponent.api or element.hakumiAffix (legacy)
| Prop | Type | Default | Description |
|---|---|---|---|
check() |
Method |
- |
Recalculate affix position and state |
update() |
Method |
- |
Alias for check() |
isAffixed() |
Method |
- |
Check if the element is currently affixed |
getState() |
Method |
- |
Get current affix state including offsets |
setOffsetTop(value) |
Method |
- |
Update the top offset and clear bottom offset |
setOffsetBottom(value) |
Method |
- |
Update the bottom offset and clear top offset |