AppShell
The AppShell component provides a complete application frame.
It offers slots for brand/logo, navigation, user profile, sidebar, footer, and the main content area, with built-in responsive handling.
This makes it ideal for dashboards, admin panels, and single-page apps.
- Preview
- Code
My App
U
User Name
Welcome to the Dashboard
© 2025 My App
import AppShell from '@ignix-ui/appshell';
import Logo from './components/Logo';
import UserProfile from './components/UserProfile';
import NavItems from './components/NavItems';
<AppShell
navigation="side"
responsive={true}
sidebarWidth="normal"
theme="modern"
brand={<Logo />}
menu={<NavItems />}
user={<UserProfile />}
>
<div className="p-6">
<h2 className="text-2xl font-semibold">Welcome to the Dashboard</h2>
<p className="mt-2 text-gray-600">This is the main content area of your application.</p>
</div>
</AppShell>
- CLI
- Manual
ignix add component appshell
import React, { useState, useEffect, ReactNode } from "react";
import clsx from "clsx";
type SidebarItem = {
label: string;
href?: string;
icon?: ReactNode;
children?: SidebarItem[];
};
export type AppShellProps = {
navigation?: "top" | "side" | "bottom" | "drawer";
brand?: ReactNode;
menu?: SidebarItem[] | ReactNode;
user?: ReactNode;
footer?: ReactNode;
responsive?: boolean;
sidebarWidth?: "narrow" | "normal" | "wide";
sidebar?: ReactNode;
theme?:
| "light"
| "dark"
| "corporate"
| "custom"
| "glass"
| "modern"
| "ocean"
| "forest"
| "solarized";
className?: string;
children: ReactNode;
};
const widthMap = {
narrow: "w-16",
normal: "w-64",
wide: "w-80",
};
const AppShell: React.FC<AppShellProps> = ({
navigation = "top",
brand,
menu,
user,
footer,
responsive = true,
sidebarWidth = "normal",
sidebar,
theme = "modern",
className,
children,
}) => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsDrawerOpen(false);
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, []);
const getSidebarWidthClass = () => widthMap[sidebarWidth] || widthMap.normal;
const renderMenu = (items: SidebarItem[]) => (
<ul className="space-y-2 pl-1!">
{items.map((item, i) => (
<li key={i}>
<a
href={item.href || "#"}
className={clsx(
"flex items-center px-3 py-2 rounded-md transition-colors duration-150",
{
"hover:bg-gray-200 text-gray-800!": theme === "light",
"hover:bg-gray-300 text-gray-700!": theme === "corporate",
"hover:bg-gray-700 text-gray-200!": theme === "dark",
"hover:bg-white/60 text-gray-700!": theme === "glass",
"hover:bg-gray-700/80 text-gray-200!": theme === "modern",
"hover:bg-teal-600/80 text-gray-200!": theme === "ocean",
"hover:bg-green-700/80 text-gray-200!": theme === "forest",
"hover:bg-[#e0dab5] text-gray-700!": theme === "solarized",
}
)}
>
{item.icon && <span>{item.icon}</span>}
<span className="hover:no-underline!">{item.label}</span>
</a>
{item.children && (
<ul className="ml-4 mt-1 space-y-1">{renderMenu(item.children)}</ul>
)}
</li>
))}
</ul>
);
return (
<div
className={clsx(
"flex flex-col h-full",
{
"bg-white text-gray-900": theme === "light",
"bg-gray-900 text-white": theme === "dark",
"bg-gray-50 text-gray-800 border border-gray-200":
theme === "corporate",
"backdrop-blur-md bg-white/70 text-gray-900": theme === "glass",
"bg-gradient-to-r from-gray-900 via-gray-800 to-black text-white":
theme === "modern",
"bg-gradient-to-br from-blue-500 via-cyan-500 to-teal-500 text-white":
theme === "ocean",
"bg-gradient-to-br from-green-700 via-emerald-600 to-lime-500 text-white":
theme === "forest",
"bg-[#fdf6e3] text-[#657b83]": theme === "solarized",
},
className
)}
>
{/* Header */}
<header className="flex items-center justify-between p-4 border-b shadow-sm">
{responsive && (navigation === "side" || navigation === "drawer") && (
<button
className="md:hidden p-2"
onClick={() => setIsDrawerOpen(!isDrawerOpen)}
aria-label="Toggle menu"
>
☰
</button>
)}
<div className="flex items-center gap-4">
{brand}
{navigation === "top" && (
<nav className="hidden md:flex gap-4">
{Array.isArray(menu) ? renderMenu(menu) : menu}
</nav>
)}
</div>
{user}
</header>
{/* Layout Wrapper */}
<div className="flex flex-1 overflow-hidden">
{/* Sidebar (desktop) */}
{navigation === "side" && (
<aside
className={clsx(
"hidden md:flex flex-col p-4",
getSidebarWidthClass(),
{
"bg-gray-100 text-gray-900": theme === "light",
"bg-gray-800 text-gray-100": theme === "dark",
"bg-gray-100 border-r border-gray-200 text-gray-800":
theme === "corporate",
"backdrop-blur-md bg-white/40 text-gray-900": theme === "glass",
"bg-gradient-to-b from-gray-800 to-gray-900 text-white":
theme === "modern",
"bg-gradient-to-b from-cyan-600 to-teal-700 text-white":
theme === "ocean",
"bg-gradient-to-b from-emerald-700 to-green-800 text-white":
theme === "forest",
"bg-[#eee8d5] text-[#586e75]": theme === "solarized",
}
)}
>
{sidebar
? sidebar
: Array.isArray(menu)
? renderMenu(menu)
: menu}
</aside>
)}
{/* Drawer (mobile) */}
{responsive && (navigation === "side" || navigation === "drawer") && (
<div
className={clsx(
"fixed inset-0 z-[200] bg-[rgb(0,0,0,0.4)] md:hidden",
isDrawerOpen ? "block" : "hidden"
)}
onClick={() => setIsDrawerOpen(false)}
>
<div
className={clsx(
"h-full p-4 transition-transform transform",
getSidebarWidthClass(),
{
"bg-gray-100 text-gray-900": theme === "light",
"bg-gray-800 text-gray-100": theme === "dark",
"bg-gray-100 border-r border-gray-200 text-gray-800":
theme === "corporate",
"backdrop-blur-md bg-white/40 text-gray-900": theme === "glass",
"bg-gradient-to-b from-gray-800 to-gray-900 text-white":
theme === "modern",
"bg-gradient-to-b from-cyan-600 to-teal-700 text-white":
theme === "ocean",
"bg-gradient-to-b from-emerald-700 to-green-800 text-white":
theme === "forest",
"bg-[#eee8d5] text-[#586e75]": theme === "solarized",
},
isDrawerOpen ? "translate-x-0" : "-translate-x-full"
)}
onClick={(e) => e.stopPropagation()}
>
{sidebar
? sidebar
: Array.isArray(menu)
? renderMenu(menu)
: menu}
</div>
</div>
)}
{/* Main Content */}
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
{/* Bottom Navigation */}
{navigation === "bottom" && (
<footer className="flex justify-around border-t p-2 bg-inherit">
{Array.isArray(menu) ? renderMenu(menu) : menu}
</footer>
)}
{footer && <div className="border-t p-4">{footer}</div>}
</div>
);
};
export default AppShell;
Usage
import { AppShell } from "@ignix-ui/appshell"
Side Navigation with Footer
<AppShell
navigation="side"
footer={<Footer />}
brand={<Logo />}
menu={menu}
user={user}
>
{children}
</AppShell>
Top Navigation
<AppShell
navigation="top"
brand={<Logo />}
menu={menu}
user={user}
>
{children}
</AppShell>
Drawer (Mobile Friendly)
<AppShell
navigation="drawer"
responsive
brand={<Logo />}
menu={menu}
>
{children}
</AppShell>
Theming
Switch layout styles using the theme prop:
<AppShell theme="light" ... />
<AppShell theme="dark" ... />
<AppShell theme="corporate" ... />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
navigation | "top" | "side" | "bottom" | "drawer" | "top" | Navigation placement |
brand | ReactNode | null | Brand/logo slot |
menu | Array<{ label: string, href?: string, icon?: ReactNode, children?: any[] }> | ReactNode | [] | Menu items (JSON or custom JSX) |
user | ReactNode | null | User profile/avatar |
footer | ReactNode | null | Footer slot |
responsive | boolean | true | Enables mobile drawer on small screens |
sidebarWidth | "narrow" | "normal" | "wide" | "normal" | Sidebar width |
theme | "light" | "dark" | "modern" | "custom" | "dark" | Built-in theme support |
className | string | "" | Custom classes |
children | ReactNode | — | Main content |