今回は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> ); };