---
title: Navbar User Chip with Role & Sign-Out
slug: navbar-user-chip-with-role
category: Auth
summary: Top-right user identity chip — avatar photo or initials, the user's name with their role beneath it, and a sign-out arrow — packaged as one shared partial.
tags: [navbar, devise, identity, sign-out, tailwind, ux-default]
status: stable
visibility: public
source_project: leo-nefe.leo.llamapress.ai
layers: [view]
---

# Navbar User Chip with Role & Sign-Out

> ⚠️ **Cookbook example — not live code.** Every code block below is an **example
> snippet**, **not part of the llamapress.ai codebase**, and **not running on this
> server**. This is a reference recipe for a **Leo instance (an AI coding agent) to
> implement in its own app** — read it to understand the pattern, then recreate it there.

The standard "who am I" chip for the top-right corner of an app's navbar. It answers
three questions at a glance: **who is signed in** (avatar photo, or initials in a filled
circle when there's no photo), **what they are** (their role in the app, in small muted
text under their name), and **how to leave** (a quiet sign-out arrow). It's one shared
partial, so every layout in the app renders the identical chip.

<!-- LIVE DEMO — agents: IGNORE this block entirely. It exists only so the guide page on
     llamapress.ai shows the pattern working. Implement from the layer sections below. -->
<div class="not-prose my-6 rounded-xl border border-slate-200 bg-slate-50 p-5">
  <p class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Live demo — try it</p>
  <div class="flex items-center justify-end gap-3 rounded-lg border border-slate-200 bg-white px-4 py-3">
    <div class="text-right leading-tight">
      <p class="text-[13px] font-medium text-slate-800">Jordan Rivera</p>
      <p class="text-[11px] text-slate-400">Administrator</p>
    </div>
    <div class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-800 text-[13px] font-semibold text-white">JR</div>
    <button type="button" onclick="this.closest('div.rounded-lg').outerHTML='<p class=&quot;px-4 py-3 text-sm text-slate-500&quot;>Signed out (demo) — refresh to reset.</p>'" class="text-slate-300 transition hover:text-rose-500" aria-label="Sign out"><i class="fa-solid fa-arrow-right-from-bracket"></i></button>
  </div>
</div>

> **When to use:** any signed-in app layout — dashboards, admin areas, internal tools.
> This should be the default top-right of every authenticated screen.
> **When not to:** marketing/public pages (no session to show), or apps with a full
> account dropdown menu — this chip is deliberately menu-free; sign-out is one click.

---

## The 80/20 in one breath

1. Add two helper methods — `user_initials` and `user_role_label` — to
   `app/helpers/application_helper.rb`.
2. Create the shared partial `app/views/shared/_user_chip.html.erb` (name + role stack,
   avatar-or-initials circle, sign-out arrow).
3. Render it in the navbar of your layout, inside `<% if user_signed_in? %>`, in a
   right-aligned flex container (`ml-auto flex items-center gap-3`).
4. Verify: sign in, check the chip shows the right initials and a human-readable role,
   click the arrow, land on the signed-out page.

---

## Layer 1 — The helpers

Keep the initials and role logic out of the view so the partial stays dumb and both
values are testable.

```ruby
# app/helpers/application_helper.rb
module ApplicationHelper
  # "Jordan Rivera" -> "JR"; falls back to the email for users with no name:
  # "jordan.rivera@acme.com" -> "JR". Always 1-2 characters, never blank.
  def user_initials(user)
    source = user.name.presence || user.email.to_s
    source.split(/[\s@.]/).reject(&:blank?).first(2).map { |w| w[0] }.join.upcase
  end

  # Human-readable role, never a raw enum value like "super_admin".
  # Adapt the first line to wherever YOUR app stores role (see "How to adapt").
  def user_role_label(user)
    raw = user.try(:role) || user.try(:roles)&.first&.try(:name)
    raw.present? ? raw.to_s.humanize : "Member"
  end

  def user_display_name(user)
    user.name.presence || user.email.split("@").first.titleize
  end
end
```

---

## Layer 2 — The partial

```erb
<%# app/views/shared/_user_chip.html.erb %>
<%# Renders: [ name / role ] [ avatar or initials ] [ sign-out arrow ] %>
<div class="flex items-center gap-2.5">
  <%# Name with role beneath — hidden on phones so the chip collapses to the avatar %>
  <div class="hidden text-right leading-tight sm:block">
    <p class="text-[13px] font-medium text-slate-800"><%= user_display_name(current_user) %></p>
    <p class="text-[11px] text-slate-400"><%= user_role_label(current_user) %></p>
  </div>

  <%# Avatar photo if one is attached, otherwise an initials circle %>
  <% if current_user.respond_to?(:avatar) && current_user.avatar.respond_to?(:attached?) && current_user.avatar.attached? %>
    <%= image_tag current_user.avatar, class: "h-9 w-9 rounded-full object-cover" %>
  <% else %>
    <div class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-800 text-[13px] font-semibold text-white">
      <%= user_initials(current_user) %>
    </div>
  <% end %>

  <%# Sign-out arrow — button_to so it's a real DELETE form, no Turbo dependency %>
  <%= button_to destroy_user_session_path, method: :delete,
        class: "text-slate-300 transition hover:text-rose-500",
        form_class: "flex", "aria-label": "Sign out" do %>
    <i class="fa-solid fa-arrow-right-from-bracket"></i>
  <% end %>
</div>
```

If Font Awesome is not loaded in your app, replace the `<i>` with this inline SVG (same
arrow-out-of-bracket icon, inherits the text color):

```erb
<%# Inline-SVG fallback for the sign-out arrow (drop inside the button_to block) %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="h-4 w-4 fill-current" aria-hidden="true">
  <path d="M377.9 105.9L500.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L377.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1-128 0c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM160 96L96 96c-17.7 0-32 14.3-32 32l0 256c0 17.7 14.3 32 32 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-64 0c-53 0-96-43-96-96L0 128C0 75 43 32 96 32l64 0c17.7 0 32 14.3 32 32s-14.3 32-32 32z"/>
</svg>
```

---

## Layer 3 — Rendering it in the layout

```erb
<%# app/views/layouts/application.html.erb — inside the navbar/header %>
<header class="border-b border-slate-200 bg-white">
  <div class="mx-auto flex h-14 max-w-7xl items-center gap-6 px-4">
    <%# ... logo + nav links ... %>

    <div class="ml-auto flex items-center gap-3">
      <% if user_signed_in? %>
        <%= render "shared/user_chip" %>
      <% end %>
    </div>
  </div>
</header>
```

---

## Gotchas (the hard-won stuff)

- **Use `button_to`, not `link_to` with `data: { turbo_method: :delete }`.** The link
  version silently degrades to a GET when Turbo fails to load (broken importmap, JS
  error earlier on the page), and Devise's sign-out route only accepts DELETE — so
  sign-out just 404s or no-ops. `button_to` renders a real `<form method="post">` with a
  `_method=delete` field and works with zero JavaScript.
- **`button_to` wraps the button in a `<form>` — style the form too.** Without
  `form_class: "flex"` the form is a block element and the arrow icon drops out of
  vertical alignment with the avatar. This is the most common "why is my icon 3px too
  low" bug with this pattern.
- **Never render the raw role value.** `current_user.role` is often an enum like
  `"super_admin"` or `"instance_operator"` — always pass it through `humanize` (the
  `user_role_label` helper does). If the app has no role concept at all, the helper's
  `"Member"` fallback keeps the chip from rendering an awkward blank line.
- **Guard against name-less users.** Sign-up flows frequently collect only an email.
  Deriving both the display name and the initials from the email (`split(/[\s@.]/)`)
  means the chip never renders an empty circle. A user named `"j@x.co"` still gets `"J"`.
- **Hide the text block on phones, keep the avatar.** `hidden sm:block` on the name/role
  stack is deliberate: on a 375px screen the navbar has no room for two lines of text,
  but the avatar + sign-out arrow still fit. Don't hide the whole chip.
- **Font Awesome is NOT guaranteed on every app.** If `fa-arrow-right-from-bracket`
  renders as an empty square, the FA stylesheet isn't loaded — use the inline-SVG
  fallback above instead of adding a CDN `<link>` just for one icon.
- **Wrap the render in `user_signed_in?`, not a nil-check inside the partial.** The
  partial assumes `current_user` is present; keeping the guard at the call site means
  signed-out layouts (marketing pages, Devise screens) never pay for it.
- **The avatar branch is optional and self-disabling.** The `respond_to?` chain means
  the same partial works whether or not the app has an Active Storage `avatar`
  attachment on `User` — no avatar setup, no crash, initials render. If you *do* have
  avatars and `image_processing` installed, prefer a variant
  (`current_user.avatar.variant(resize_to_fill: [72, 72])`) so you're not shipping a
  full-size upload into a 36px circle.
- **Colors here are plain Tailwind slate — swap in your theme.** The proven source
  implementation used custom theme tokens; this recipe uses `slate-800` (filled circle),
  `slate-400` (role line), `slate-300 → rose-500` (sign-out idle → hover). Keep the
  *relationships* (avatar darkest, role muted, sign-out quietest until hovered) even if
  you change the hues — the sign-out arrow should be the least prominent element until
  the pointer is on it.

---

## Files this pattern touches

```
app/helpers/application_helper.rb          # user_initials, user_role_label, user_display_name
app/views/shared/_user_chip.html.erb       # the chip partial (new file)
app/views/layouts/application.html.erb     # render call in the navbar
```

## How to adapt to your schema

1. **Point `user_role_label` at your role storage.** Common variants:
   a string/enum column → `user.role.humanize`; a `rolify`-style association →
   `user.roles.first&.name&.humanize`; an org-membership join →
   `user.memberships.find_by(organization: current_organization)&.role&.humanize`.
   Change only the helper — the partial never knows where roles live.
2. **Not using Devise?** Replace `destroy_user_session_path` with your sign-out route
   and `user_signed_in?` / `current_user` with your session helpers. Keep the DELETE
   method and the `button_to` form.
3. **Multiple layouts?** Render the same `shared/user_chip` partial in each — that's the
   point of extracting it. Don't copy the markup per layout.
4. **Want a dropdown menu instead of a bare arrow?** This chip is the menu-free
   baseline. If you outgrow it (profile link, settings, org switcher), wrap the whole
   chip in a Stimulus dropdown and move sign-out into the menu — but keep the
   name/role/avatar anatomy.
