今回はTypescript, React, Tailwind CSS, Shadcnを用いてNavigation barにUser buttonを配置する方法を紹介します。User buttonを押すとDropdown menuが出現し、Logoutなどの処理を行える様にします。主にShadcnのDropdown menuとAvatarを使用します。またUser objectはFirebaseから取得しているため、適宜変更してください。
自分のアウトプット目的の記事ですが、もしも誰かの役に立てたなら幸いです。

参考動画:Build a Jira Clone With Nextjs, React, Tailwind, Hono.js | Part 1/2 (2024)
参考資料:Avatar – shadcn/ui, Dropdown Menu – shadcn/ui
- 使用技術
- React19, Tailwind CSS, Shadcn
 
 
Contents
まずUser buttonを配置するためのnavigation barを作成します。
//@/components/navbar
import { UserButton } from "@/components/user-button";
export const Navbar = () => {
  return (
    <nav className="pt-5 px-6 flex items-center justify-between">
      {/* ここにNavigationを入れてください */}
      <UserButton />
    </nav> 
  );
};
このnavigation barはlayout.tsxに配置してください。
//layout例
//@/app/layout
import { Navbar } from "@/components/navbar";
import { Sidebar } from "@/components/sidebar";
import React from "react";
const Layout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="min-h-screen">
      <div className="flex w-full h-full">
        <div className="fixed left-0 top-0 hidden lg:block lg:w-[264px] h-full overflow-y-auto">
          <Sidebar />
        </div>
        <div className="lg:pl-[264px] w-full">
          <div className="mx-auto max-x-screen-2xl h-full">
            <Navbar />{/* ここにNavbarを配置 */}
            <main className="h-full py-8 px-6 flex flex-col">{children}</main>
          </div>
        </div>
      </div>
    </div>
  );
};
export default Layout;
ちなみに余談ですが Tailwindで "hidden lg:block" とすることで、モバイルデバイスなど幅1024px未満の時はSidebarが表示されず、幅がそれ以上になるとCSS上で display:hidden が display:block で上書きされるため、Sidebarが表示されます。
Dropdown Menu – shadcn
まずはShadcnの公式documentでDropdown menuの使い方を確認しましょう。
Dropdown Menu – shadcn/ui

User buttonをDropdownMenuTriggerで囲えば良さそうです。
Avatar – shadcn
Avatarの方はもっと簡単ですね。
Avatar – shadcn/ui

Imageが存在するならAvatarImageが表示されて、imageの取得に失敗した時にAvatarFallbackが表示される様です。
Login中のuserはContext APIを使用して取得しています。こちらは別の記事で紹介しています。BaaSとしてFirebaseを使用しており、User objectの構造が異なる場合は適宜書き換えてください。
Userの取得
まずuserを取得します。Loading中はLucide reactのLoader circleを回転させて表示しています。もしuserが存在しない場合はUser buttonは表示されません。userのdisplayName, email, photoURLを取り出しています。
Loader circle – Lucide react
<LoaderCircle className="animate-spin" />は個人的によく使います。おすすめです。
const { user, isLoading } = useAuthContext();
const logoutMutation = useLogout();
if (isLoading) {
  return (
    <div className="size-10 rounded-full flex items-center justify-center bg-neutral-200 border border-neutral-300">
      <LoaderCircle className="size-4 animate-spin text-muted-foreground" />
    </div>
  );
}
if (!user) {
  return null;
}
const { displayName: name, email, photoURL } = user;
AvatarFallback
AvatarFallbackで表示される文字を設定します。userのdisplayNameの頭文字を取得し大文字にします。displayNameが存在しない場合はemailの頭文字を、それも存在しない場合は大文字のUを表示させる様にします。
const avatarFallback = name ? name.charAt(0).toUpperCase() : email?.charAt(0).toUpperCase() ?? "U";
TSXの実装
AvatarをDropdownのtriggerに設定します。Imageが存在する場合AvatarImageが表示され、Imageの取得に失敗した時にAvatarFallbackが表示されます。src={photoURL ?? undefined} とNull合体演算子で書いているのはAvatarImageのsrc属性がnullを受け付けないためです。
<DropdownMenu modal={false}>
  <DropdownMenuTrigger className="outline-none relative">
    <Avatar className="size-10 hover:opacity-75 transition border border-neutral-300">
      <AvatarImage src={photoURL ?? undefined} />
      <AvatarFallback className="bg-neutral-200 font-medium text-neutral-500 flex items-center justify-center">
        {avatarFallback}
      </AvatarFallback>
    </Avatar>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    {/* ここにDropdown menuの内容を記述 */}
  </DropdownMenuContent>
</DropdownMenu>
DropdownMenuContent
Dropdownの出現場所などを設定します。
alignとsideで出現位置を設定します。sideOffsetは主軸からどれだけ離すかを指定します。今回の例では下方向に10px移動します。
<DropdownMenuContent
  align="end"
  side="bottom"
  className="w-60"
  sideOffset={10}
>
align="center" // 中央揃え <- default align="start" // Start揃え、sideが上下なら左端揃え align="end" // End揃え、sideが上下なら右端揃え
side="bottom" // 下側に表示(デフォルト) side="top" // 上側に表示 side="left" // 左側に表示 side="right" // 右側に表示
sideOffset={0}   // ピッタリくっつける
sideOffset={10}  // 10px離す
Dropdownの中身はAvatar, User名, Email, Logout buttonを表示しています。ちなみにLogoutのための関数はTanstack queryのuseMutationで作成しています。こちらも適宜書き換えてください。
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { useAuthContext } from "@/context/auth-context";
import { useLogout } from "@/features/auth/custom-hooks/use-logout";
import { LoaderCircle, LogOut } from "lucide-react";
export const UserButton = () => {
  const { user, loading } = useAuthContext();
  const logoutMutation = useLogout(); //Tanstack queryのuseMutationを使用しています。
  if (loading) {
    return (
      <div className="size-10 rounded-full flex items-center justify-center bg-neutral-200 border border-neutral-300">
        <LoaderCircle className="size-4 animate-spin text-muted-foreground" />
      </div>
    );
  }
  if (!user) {
    return null;
  }
  const { displayName: name, email, photoURL } = user;
  const avatarFallback = name
    ? name.charAt(0).toUpperCase()
    : email?.charAt(0).toUpperCase() ?? "U";
  return (
    <DropdownMenu modal={false}>
      <DropdownMenuTrigger className="outline-none relative">
        <Avatar className="size-10 hover:opacity-75 transition border border-neutral-300">
          <AvatarImage src={photoURL ?? undefined} />
          <AvatarFallback className="bg-neutral-200 font-medium text-neutral-500 flex items-center justify-center">
            {avatarFallback}
          </AvatarFallback>
        </Avatar>
      </DropdownMenuTrigger>
      <DropdownMenuContent
        align="end"
        side="bottom"
        className="w-60"
        sideOffset={10}
      >
        {/* Dropdownの内容は適宜書き換えてください。 */}
        <div className="flex flex-col items-center justify-center gap-2 px-2.5 py-4">
          <Avatar className="size-[52px] transition border border-neutral-300">
            <AvatarImage src={photoURL ? photoURL : undefined} />
            <AvatarFallback className="bg-neutral-200 font-medium text-neutral-500 text-xl flex items-center justify-center">
              {avatarFallback}
            </AvatarFallback>
          </Avatar>
          <div className="flex flex-col items-center justify-center">
            <p className="text-sm font-medium text-neutral-900">
              {name || "User"}
            </p>
            <p className="text-xs  text-neutral-500">{email}</p>
          </div>
          <Separator />
          <DropdownMenuItem
            className="h-10 flex items-center justify-center text-amber-700 font-medium cursor-pointer"
            onClick={() => logoutMutation.mutate()}
          >
            <LogOut className="size-4 mr-2" />
            Log out
          </DropdownMenuItem>
        </div>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};