Shell
A component with a collapsible sidebar, theme and language toggles, and optional OAuth2 authentication.
Minimal Header Variant
Use the variant="minimal" prop to render a horizontal header layout instead of the default sidebar.
Features
- Optional OAuth2 Authentication: Plug-and-play authorization code flow with PKCE
- Theme Toggle: Built-in light/dark mode support
- Language Toggle: Built-in language switcher for internationalization
- User Profile: Dropdown menu with user information and sign-out
Installation
npx shadcn@latest add @uipath/shellUsage
The shell works out of the box without authentication. Just provide layout props:
import { Home, Settings } from 'lucide-react';
import { ApolloShell } from '@/components/ui/shell';
const navItems = [
{ path: '/dashboard', label: 'dashboard', icon: Home },
{ path: '/settings', label: 'settings', icon: Settings },
];
function App() {
return (
<ApolloShell
companyName="Your Company"
productName="Your Product"
navItems={navItems}
>
<YourApp />
</ApolloShell>
);
}Adding Authentication
To enable built-in OAuth2 authentication with PKCE, wrap the shell with ShellAuthProvider.
Both ShellAuthProvider and ApolloShell must be inside a QueryClientProvider from @tanstack/react-query.
When a ShellAuthProvider is present, the shell automatically shows a login screen until the user is authenticated.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Home, Settings } from 'lucide-react';
import { ApolloShell } from '@/components/ui/shell';
import { ShellAuthProvider } from '@/components/ui/shell-auth-provider';
const queryClient = new QueryClient();
const navItems = [
{ path: '/dashboard', label: 'dashboard', icon: Home },
{ path: '/settings', label: 'settings', icon: Settings },
];
function App() {
return (
<QueryClientProvider client={queryClient}>
<ShellAuthProvider
clientId="your-client-id"
scope="openid profile email offline_access"
baseUrl={window.location.origin}
>
<ApolloShell
companyName="Your Company"
productName="Your Product"
navItems={navItems}
>
<YourApp />
</ApolloShell>
</ShellAuthProvider>
</QueryClientProvider>
);
}Gating access by group membership
Use GroupMembershipGuard to restrict the application to members of one or more
UiPath Identity groups. The guard reads the live groups and groupMembers
collections sourced by SolutionProvider from @uipath/vs-core, compares the
signed-in user’s email against the combined member list, and renders either a
loading indicator or an access-denied screen with a sign-out action. When the
user is a member of any of the listed groups, the children render
unchanged. An empty groupIds array denies access.
The toggle below switches between the loading, denied, and granted visual
states using stubbed UI — the live data path is exercised in real apps via
SolutionProvider.
Place GroupMembershipGuard inside both SolutionProvider (so it can read
identity collections) and ShellAuthProvider (so it can read the authenticated
user from useAuth):
import { SolutionProvider } from '@uipath/vs-core';
import { GroupMembershipGuard } from '@/components/ui/shell/group-membership-guard';
function App() {
return (
<SolutionProvider
orgName="your-org"
tenantName="your-tenant"
orgId="your-org-id"
env="production"
secret={solutionSecret}
configuration={{ entities }}
>
<ShellAuthProvider
clientId="your-client-id"
scope="openid profile email offline_access"
baseUrl={window.location.origin}
>
<GroupMembershipGuard
groupIds={['admins-group-id', 'editors-group-id']}
>
<ApolloShell
companyName="Your Company"
productName="Your Product"
navItems={navItems}
>
<YourApp />
</ApolloShell>
</GroupMembershipGuard>
</ShellAuthProvider>
</SolutionProvider>
);
}Resolving group ids by role
GroupMembershipGuard and useIsGroupMember need group ids, but applications
typically reason in terms of roles whose backing group names are known ahead of
time. useGroupId looks up a group id by name from the groups collection
exposed by SolutionProvider and re-renders reactively as the collection
updates. It returns { groupId, isLoading }; groupId is null while the
collection is loading or when no group with the configured name exists.
A default Role enum ships alongside the hook with two members — ADMIN and
BUSINESS_USER. Replace it with your own string enum if you need different
roles; the hook is generic over Role extends string and accepts any
Record<Role, string> mapping role to group name.
import { GroupMembershipGuard } from '@/components/ui/shell/group-membership-guard';
import { Role } from '@/components/ui/shell/shell-roles';
import { useGroupId } from '@/components/ui/shell/use-group-id';
const ROLE_GROUP_MAP: Record<Role, string> = {
[Role.ADMIN]: 'apollo-admins',
[Role.BUSINESS_USER]: 'apollo-business-users',
};
function AdminArea({ children }: { children: React.ReactNode }) {
const { groupId, isLoading } = useGroupId({
role: Role.ADMIN,
roleGroupMap: ROLE_GROUP_MAP,
});
if (isLoading || !groupId) {
return <div>Loading…</div>;
}
return (
<GroupMembershipGuard groupIds={[groupId]}>{children}</GroupMembershipGuard>
);
}