Skip to content
Components

Application header

A glass header with navigation, an account menu and room for your brand.

Open preview ↗

Interact with the example above, or open it in a full window.

After setting up the library, copy the example files into your app and adapt the content. These are composition references, with no block API to install or import.

Load the copied stylesheet from your application layout. Place the controller in your Stimulus controller directory and register it using your app's usual loading convention.

Replace the sample brand, links and account actions with your application's content.

Keep navigation destinations as links and account actions in the menu. The header's layout and scroll behavior live in the example files.

Built with Button

The preview renders this ERB and loads these styles and interactions.

app/views/shared/_application_header.html.erb

<%# locals: (brand:, home: "/", home_label:, links: [], label: "Main", width: "max-w-5xl", navigation_class: "sm:flex") %>
<%# links: [[label, url, current], …]. The block's content renders the actions at the end of the bar. %>
<header class="header-example sticky top-0 z-sticky px-3 pt-3 pb-4 sm:px-6" data-controller="blocks-header" data-action="scroll@window->blocks-header#scroll">
  <div class="relative isolate mx-auto flex min-h-16 <%= width %> items-center gap-related px-3 py-2 sm:gap-content sm:px-5">
    <span class="header-example__glass <%= lc_glass_class(thick: true, extra: "pointer-events-none absolute inset-0 -z-10") %>" aria-hidden="true"></span>
    <a href="<%= home %>" class="flex min-h-11 shrink-0 items-center gap-related pe-2 font-semibold tracking-tight no-underline text-inherit" aria-label="<%= home_label %>"><%= brand %></a>
    <% if links.any? %>
      <nav class="hidden items-center gap-tight <%= navigation_class %>" aria-label="<%= label %>">
        <% links.each do |text, url, current| %>
          <%= lc_button_link_to text, url, variant: :ghost, size: :sm, class: "aria-[current=page]:bg-accent", aria: { current: ("page" if current) } %>
        <% end %>
      </nav>
    <% end %>
    <div class="ms-auto flex items-center gap-related"><%= yield %></div>
  </div>
</header>

app/assets/stylesheets/application_header.css

.header-example__glass {
  opacity: 0;
  transition: opacity 300ms ease;
}

.header-example[data-scrolled] .header-example__glass {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .header-example__glass {
    transition: none;
  }
}

app/javascript/controllers/blocks_header_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.scroll()
  }

  // An open modal pins the body, so scrollY reads 0 until it closes; keep the
  // state the page had rather than fading the glass out behind the dialog.
  scroll() {
    if (document.body.dataset.modalScrollLock === "true") return
    this.element.toggleAttribute("data-scrolled", window.scrollY > 8)
  }
}
← All blocks