Skeleton

Component

Interactive examples and API documentation

Basic
Simplest skeleton usage with default title and paragraph rows.

Code
<%= render(Hakumi::Skeleton::Component.new) %>
Complex composition
Combine avatar, title width, and multiple paragraph rows.

Code
<%= render(Hakumi::Skeleton::Component.new(
  avatar: { size: :large, shape: :square },
  title: { width: "60%" },
  paragraph: { rows: 4, widths: ["100%", "95%", "80%", "60%"] }
)) %>
Active animation
Display the loading shimmer by enabling <code>active</code>.

Code
<%= render(Hakumi::Skeleton::Component.new(active: true)) %>
Button, avatar, input, image, node
Use the dedicated skeleton element subcomponents.
Code
<%= render(Hakumi::Space::Component.new(size: :large, wrap: true)) do |space| %>
  <% space.with_item do %>
    <%= render(Hakumi::Skeleton::Button::Component.new) %>
  <% end %>
  <% space.with_item do %>
    <%= render(Hakumi::Skeleton::Button::Component.new(shape: :round, size: :large)) %>
  <% end %>
  <% space.with_item do %>
    <%= render(Hakumi::Skeleton::Avatar::Component.new(size: :large)) %>
  <% end %>
  <% space.with_item do %>
    <%= render(Hakumi::Skeleton::Input::Component.new(size: :large, block: true)) %>
  <% end %>
  <% space.with_item do %>
    <%= render(Hakumi::Skeleton::Image::Component.new(width: 120, height: 80)) %>
  <% end %>
  <% space.with_item do %>
    <%= render(Hakumi::Skeleton::Node::Component.new) do %>
      <%= render(Hakumi::Icon::Component.new(name: :loading, spin: true, size: 18)) %>
    <% end %>
  <% end %>
<% end %>
Contains sub component
Skeleton can wrap subcomponents and render children when loading is disabled.
Code
<%= render(Hakumi::Skeleton::Component.new(loading: false)) do %>
  <%= render(Hakumi::Space::Component.new(size: :middle)) do |space| %>
    <% space.with_item do %>
      <%= render(Hakumi::Skeleton::Button::Component.new(active: true, shape: :round)) %>
    <% end %>
    <% space.with_item do %>
      <%= render(Hakumi::Skeleton::Input::Component.new(active: true, size: :large)) %>
    <% end %>
  <% end %>
<% end %>
List
Render multiple skeleton rows in a list-like layout.

List loading state

Toggle manually or click “Simulate request” to mimic a short network delay.
Loading

Code
<% placeholder_markup = capture do %>
  <%= render(
        Hakumi::Space::Component.new(direction: :vertical, size: :large, block: true)
      ) do |space| %>
    <% 3.times do %>
      <% space.with_item do %>
        <%= render(Hakumi::Skeleton::Component.new(
          active: true,
          avatar: true,
          title: { width: "50%" },
          paragraph: { rows: 2, widths: ["100%", "70%"] }
        )) %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

<% content_markup = capture do %>
  <%= render(
        Hakumi::Space::Component.new(direction: :vertical, size: :middle, block: true)
      ) do |list| %>
    <% [
      { initials: "JD", name: "Jane Doe", note: "Prepping the onboarding notes for the operations pod." },
      { initials: "RM", name: "Robin M.", note: "Closed the incident from last week’s sprint." },
      { initials: "LS", name: "Liv S.", note: "Shared the refreshed visual guidelines with the guild." }
    ].each do |user| %>
      <% list.with_item do %>
        <%= render(Hakumi::Space::Component.new(align: :center, size: :middle)) do |row| %>
          <% row.with_item do %>
            <%= render(Hakumi::Avatar::Component.new(size: :large, shape: :circle)) { user[:initials] } %>
          <% end %>
          <% row.with_item do %>
            <%= render(Hakumi::Typography::Text::Component.new(strong: true)) { user[:name] } %>
            <%= render(Hakumi::Typography::Paragraph::Component.new) { user[:note] } %>
          <% end %>
        <% end %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

<%= render(
      Hakumi::Space::Component.new(direction: :vertical, size: :large, block: true)
    ) do |demo| %>

  <% demo.with_item do %>
    <%= render(Hakumi::Typography::Title::Component.new(level: 4)) { "List loading state" } %>
  <% end %>

  <% demo.with_item do %>
    <%= render(Hakumi::Typography::Paragraph::Component.new) do %>
      Toggle manually or click “Simulate request” to mimic a short network delay.
    <% end %>
  <% end %>

  <% demo.with_item do %>
    <%= render(Hakumi::Space::Component.new(direction: :vertical, size: :middle, block: true)) do |controls| %>
      <% controls.with_item do %>
        <%= render(Hakumi::Checkbox::Component.new(
          id: "skeleton-list-toggle",
          checked: true,
          label: "Toggle loading"
        )) %>
      <% end %>
      <% controls.with_item do %>
        <%= render(Hakumi::Typography::Text::Component.new(id: "skeleton-list-caption")) { "Loading" } %>
      <% end %>
      <% controls.with_item do %>
        <%= render(Hakumi::Button::Component.new(
          id: "skeleton-list-trigger",
          type: :default
        )) { "Simulate request" } %>
      <% end %>
    <% end %>
  <% end %>

  <% demo.with_item do %>
    <div id="skeleton-list-placeholder" style="width: 100%; max-width: 560px;">
      <%= placeholder_markup %>
    </div>
  <% end %>

  <% demo.with_item do %>
    <div id="skeleton-list-content" hidden style="width: 100%; max-width: 560px;"></div>
  <% end %>
<% end %>

<template id="skeleton-list-placeholder-template">
  <%= placeholder_markup %>
</template>

<template id="skeleton-list-template">
  <%= content_markup %>
</template>

<script>
  (() => {
    const toggle = document.getElementById("skeleton-list-toggle")
    const placeholderPanel = document.getElementById("skeleton-list-placeholder")
    const contentPanel = document.getElementById("skeleton-list-content")
    const caption = document.getElementById("skeleton-list-caption")
    const trigger = document.getElementById("skeleton-list-trigger")

    if (!toggle || !placeholderPanel || !contentPanel || !caption || !trigger) return

    const placeholderTemplate = document.getElementById("skeleton-list-placeholder-template")
    const contentTemplate = document.getElementById("skeleton-list-template")
    let requestTimer = null
    let contentRendered = false

    const renderContent = () => {
      if (contentRendered) return
      contentPanel.innerHTML = contentTemplate?.innerHTML || ""
      contentRendered = true
    }

    const renderSkeleton = () => {
      placeholderPanel.innerHTML = placeholderTemplate?.innerHTML || ""
    }

    const updatePanels = () => {
      const showSkeleton = toggle.checked
      placeholderPanel.hidden = !showSkeleton
      contentPanel.hidden = showSkeleton
      caption.textContent = showSkeleton ? "Loading" : "Content ready"

      if (showSkeleton) {
        renderSkeleton()
      } else {
        renderContent()
      }
    }

    const finishRequest = () => {
      requestTimer = null
      toggle.checked = false
      trigger.disabled = false
      updatePanels()
    }

    toggle.addEventListener("change", () => {
      if (requestTimer) {
        clearTimeout(requestTimer)
        requestTimer = null
        trigger.disabled = false
      }
      updatePanels()
    })

    trigger.addEventListener("click", () => {
      if (requestTimer) return

      toggle.checked = true
      trigger.disabled = true
      updatePanels()

      requestTimer = setTimeout(finishRequest, 1500)
    })

    updatePanels()
  })()
</script>
Programmatic
Create skeletons dynamically via <code>HakumiComponents.renderComponent</code>.
Code
<%= render(Hakumi::Space::Component.new) do |space| %>
  <% space.with_item do %>
    <%= render(Hakumi::Button::Component.new(id: "skeleton-programmatic-create", type: :primary)) { "Render skeleton" } %>
  <% end %>
  <% space.with_item do %>
    <%= render(Hakumi::Button::Component.new(id: "skeleton-programmatic-clear")) { "Clear" } %>
  <% end %>
<% end %>

<%= render(Hakumi::Flex::Component.new(
  id: "skeleton-programmatic-target",
  vertical: true,
  gap: "16px",
  style: "margin-top: 16px;"
)) { "" } %>

<script>
  (() => {
    const wire = () => {
      if (!window.HakumiComponents?.renderComponent) return false

      const targetSelector = "#skeleton-programmatic-target"
      const createButton = document.getElementById("skeleton-programmatic-create")
      const clearButton = document.getElementById("skeleton-programmatic-clear")

      createButton?.addEventListener("click", () => {
        window.HakumiComponents.renderComponent("skeleton", {
          params: {
            active: true,
            avatar: true,
            title: { width: "55%" },
            paragraph: { rows: 2, widths: ["100%", "70%"] }
          },
          target: targetSelector,
          mode: "replace"
        })
      })

      clearButton?.addEventListener("click", () => {
        const target = document.querySelector(targetSelector)
        if (target) target.innerHTML = ""
      })

      return true
    }

    if (wire()) return

    const onReady = () => {
      if (wire()) {
        document.removeEventListener("turbo:load", onReady)
        window.removeEventListener("load", onReady)
      }
    }

    document.addEventListener("turbo:load", onReady)
    window.addEventListener("load", onReady)
  })()
</script>

Skeleton API

Prop Type Default Description
loading Boolean true Whether to show the skeleton or render children.
active Boolean false Enable the shimmer animation.
avatar Boolean or Hash false Display avatar placeholder. Hash supports :size and :shape.
title Boolean or Hash true Display title placeholder. Hash supports :width.
paragraph Boolean or Hash true Display paragraph placeholders. Hash supports :rows, :width, :widths.
round Boolean false Apply rounded borders to skeleton elements.
content slot Slot - Content rendered when loading is false.
**html_options Keyword args - Extra attributes merged into the wrapper div.

Skeleton::Avatar API

Prop Type Default Description
active Boolean false Enable shimmer animation.
size Symbol or Integer :default Size of avatar placeholder (:small, :default, :large, or px).
shape Symbol :circle Shape of avatar placeholder (:circle, :square).
**html_options Keyword args - Extra attributes merged into the wrapper span.

Skeleton::Button API

Prop Type Default Description
active Boolean false Enable shimmer animation.
size Symbol :default Button size (:small, :default, :large).
shape Symbol :default Button shape (:default, :circle, :round).
block Boolean false Stretch the placeholder to full width.
**html_options Keyword args - Extra attributes merged into the wrapper span.

Skeleton::Input API

Prop Type Default Description
active Boolean false Enable shimmer animation.
size Symbol :default Input size (:small, :default, :large).
block Boolean false Stretch the placeholder to full width.
**html_options Keyword args - Extra attributes merged into the wrapper span.

Skeleton::Image API

Prop Type Default Description
active Boolean false Enable shimmer animation.
width Integer or String 96 Width of the image placeholder.
height Integer or String 96 Height of the image placeholder.
**html_options Keyword args - Extra attributes merged into the wrapper span.

Skeleton::Node API

Prop Type Default Description
active Boolean false Enable shimmer animation.
size Integer or String 40 Square size of the node placeholder.
content slot Slot - Optional icon or custom content.
**html_options Keyword args - Extra attributes merged into the wrapper span.

JavaScript API (element.hakumiSkeleton)

Skeleton is a static component and does not expose a JavaScript API.
Prop Type Default Description
- - - No public methods.