SaaS workspace
Drawer panel for a SaaS workspace: logo and name with a close button, an Accent Colour onboarding item with a 5 Steps Badge, main items, a bottom group (Notifications with a count, Settings, Docs, Help) and a footer with a team switcher and the user's avatar.
Light
Dark
Your app is ready.
Inter body text, Inter headings.
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
npx nativecn-cli@latest add drawer-02Also installs: drawer, avatar, badge, button, icon, pressable, text, theme.
Packages: lucide-react-native@^1.51.0, react-native-svg.
What makes it different
A product workspace: an Accent-tinted onboarding Checklist on top, a second group pushed to the bottom, and a team switcher and account button in the footer instead of a profile header.
Block Variants
drawer-01– Profile header + grouped sections: The familiar Gmail/Slack drawer: who is signed in on top, items grouped under small headings, Log out at the bottom; the active item sits on a muted background.drawer-02(this page) – SaaS workspace: A product workspace: an Accent-tinted onboarding Checklist on top, a second group pushed to the bottom, and a team switcher and account button in the footer instead of a profile header.drawer-03– Cover header: A consumer or commerce app drawer: a dark profile cover that stays dark in both Schemes (with a light status bar over it while the drawer is open), and items that show the current route by Accent Colour text and icon, with no background.
Accessibility
- Each item is a DrawerItem: role link with aria-selected while its route is active, or role button for an action without href.
- An item's visible label is its accessible name.
- Items are 48 high in Vega and 40 in Nova, with the tap area extended to 48.
- The close button is an icon-only Button named "Close menu"; the logo icon is decorative.
- The footer buttons are named "Switch team, current team Acme Inc" and "Account menu", with their tap areas extended to 48.
Notes
In app/(drawer)/_layout.tsx, with Expo Router's drawer from expo-router/drawer (never @react-navigation/drawer or the npm package expo-drawer): <Drawer drawerContent={(props) => <Drawer02 {...props} />} />. Spread every drawerContent prop in, so items can close the drawer. Tapping an item navigates and closes the drawer; the backdrop and the system back gesture also close it. Names, avatars, counts, items and routes are placeholders: rename them and point each href at a route in your drawer Layout. Item heights follow the drawer.item Style Slot (Vega 48, Nova 40), and the panel keeps clear of the status bar and home indicator.
Source
The Vega Style's code, exactly as add copies it into your app.
{components}/drawer-02/index.tsx
// drawer-02: SaaS workspace. The panel of a drawer Layout, built from the drawer Components on
// Expo Router's own drawer (`expo-router/drawer`; never install `@react-navigation/drawer` or
// the unrelated npm package `expo-drawer`).
//
// In app/(drawer)/_layout.tsx:
// <Drawer
// drawerContent={(props) => (
// <Drawer02 {...props} onSwitchTeam={openTeams} onAccountPress={openAccount} />
// )}
// screenOptions={{ drawerStyle: { width: 290 } }}
// />
//
// The logo, app and team names, items, counts and routes are placeholders: rename them to your
// app's.
import {
Bell,
ChevronsUpDown,
CreditCard,
FileText,
Hexagon,
House,
LifeBuoy,
ListChecks,
Package,
Settings,
TicketPercent,
Users,
Wallet,
X,
} from "lucide-react-native";
import { View } from "react-native";
import { Avatar } from "@/components/avatar";
import { Badge } from "@/components/badge";
import { Button } from "@/components/button";
import {
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerItem,
DrawerSection,
type DrawerContentProps,
} from "@/components/drawer";
import { Icon } from "@/components/icon";
import { Pressable } from "@/components/primitives/pressable";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
export type Drawer02Props = DrawerContentProps & {
/** Runs when the team switcher in the footer is pressed. */
onSwitchTeam?: () => void;
/** Runs when the user's avatar in the footer is pressed. */
onAccountPress?: () => void;
};
export function Drawer02({ onSwitchTeam, onAccountPress, ...props }: Drawer02Props) {
const styles = useStyles();
return (
<DrawerContent {...props}>
<DrawerHeader style={styles.header}>
<View style={styles.brand}>
<Icon icon={Hexagon} size="lg" color="primary" />
<Text variant="h4" numberOfLines={1} style={styles.shrink}>
Acme
</Text>
</View>
<Button
variant="ghost"
size="sm"
icon={X}
aria-label="Close menu"
onPress={() => props.navigation?.closeDrawer()}
/>
</DrawerHeader>
<DrawerSection>
<DrawerItem
label="Checklist"
icon={ListChecks}
href="/checklist"
tone="accent"
badge={<Badge label="5 Steps" />}
/>
<DrawerItem label="Home" icon={House} href="/" />
<DrawerItem label="Products" icon={Package} href="/products" />
<DrawerItem label="Payment page" icon={CreditCard} href="/payment-page" />
<DrawerItem label="Promo codes" icon={TicketPercent} href="/promo-codes" />
<DrawerItem label="Customers" icon={Users} href="/customers" />
<DrawerItem label="Payments" icon={Wallet} href="/payments" />
</DrawerSection>
{/* Pushed to the bottom of the scrolling area, above the footer. */}
<DrawerSection style={styles.bottom}>
<DrawerItem
label="Notifications"
icon={Bell}
href="/notifications"
badge={<Badge label="+9" />}
/>
<DrawerItem label="Settings" icon={Settings} href="/settings" />
<DrawerItem label="Docs" icon={FileText} href="/docs" />
<DrawerItem label="Help" icon={LifeBuoy} href="/help" />
</DrawerSection>
<DrawerFooter style={styles.footer}>
<Pressable
role="button"
aria-label="Switch team, current team Acme Inc"
onPress={onSwitchTeam}
pressedStyle={styles.pressed}
style={[styles.control, styles.team]}
>
<View style={styles.teamLogo} />
<Text variant="label" numberOfLines={1} style={styles.shrink}>
Acme Inc
</Text>
<Icon icon={ChevronsUpDown} size="sm" color="mutedForeground" />
</Pressable>
<Pressable
role="button"
aria-label="Account menu"
onPress={onAccountPress}
pressedStyle={styles.pressed}
style={styles.control}
>
<Avatar fallback="JD" size="sm" />
<Icon icon={ChevronsUpDown} size="sm" color="mutedForeground" />
</Pressable>
</DrawerFooter>
</DrawerContent>
);
}
const useStyles = createStyles((t) => {
const item = {
height: t.scaleValue(48),
borderRadius: t.radius.md,
paddingHorizontal: t.spacing[3],
gap: t.spacing[3],
};
return {
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
borderBottomWidth: t.borderWidth.default,
borderBottomColor: t.colors.border,
},
brand: { flex: 1, flexDirection: "row", alignItems: "center", gap: t.spacing[2] },
shrink: { flexShrink: 1 },
bottom: { marginTop: "auto" },
footer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: t.spacing[2],
borderTopWidth: t.borderWidth.default,
borderTopColor: t.colors.border,
},
// The footer controls share a DrawerItem's height, radius and padding (drawer.item Slot).
control: {
...item,
flexDirection: "row",
alignItems: "center",
gap: t.spacing[2],
borderCurve: "continuous",
},
team: { flexShrink: 1 },
teamLogo: {
width: t.iconSize.lg,
height: t.iconSize.lg,
borderRadius: t.radius.sm,
backgroundColor: t.colors.primary,
},
pressed: { backgroundColor: t.colors.muted },
};
});{components}/drawer-02/index.tsx
// drawer-02: SaaS workspace. The panel of a drawer Layout, built from the drawer Components on
// Expo Router's own drawer (`expo-router/drawer`; never install `@react-navigation/drawer` or
// the unrelated npm package `expo-drawer`).
//
// In app/(drawer)/_layout.tsx:
// <Drawer
// drawerContent={(props) => (
// <Drawer02 {...props} onSwitchTeam={openTeams} onAccountPress={openAccount} />
// )}
// screenOptions={{ drawerStyle: { width: 290 } }}
// />
//
// The logo, app and team names, items, counts and routes are placeholders: rename them to your
// app's.
import {
Bell,
ChevronsUpDown,
CreditCard,
FileText,
Hexagon,
House,
LifeBuoy,
ListChecks,
Package,
Settings,
TicketPercent,
Users,
Wallet,
X,
} from "lucide-react-native";
import { View } from "react-native";
import { Avatar } from "@/components/avatar";
import { Badge } from "@/components/badge";
import { Button } from "@/components/button";
import {
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerItem,
DrawerSection,
type DrawerContentProps,
} from "@/components/drawer";
import { Icon } from "@/components/icon";
import { Pressable } from "@/components/primitives/pressable";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
export type Drawer02Props = DrawerContentProps & {
/** Runs when the team switcher in the footer is pressed. */
onSwitchTeam?: () => void;
/** Runs when the user's avatar in the footer is pressed. */
onAccountPress?: () => void;
};
export function Drawer02({ onSwitchTeam, onAccountPress, ...props }: Drawer02Props) {
const styles = useStyles();
return (
<DrawerContent {...props}>
<DrawerHeader style={styles.header}>
<View style={styles.brand}>
<Icon icon={Hexagon} size="lg" color="primary" />
<Text variant="h4" numberOfLines={1} style={styles.shrink}>
Acme
</Text>
</View>
<Button
variant="ghost"
size="sm"
icon={X}
aria-label="Close menu"
onPress={() => props.navigation?.closeDrawer()}
/>
</DrawerHeader>
<DrawerSection>
<DrawerItem
label="Checklist"
icon={ListChecks}
href="/checklist"
tone="accent"
badge={<Badge label="5 Steps" />}
/>
<DrawerItem label="Home" icon={House} href="/" />
<DrawerItem label="Products" icon={Package} href="/products" />
<DrawerItem label="Payment page" icon={CreditCard} href="/payment-page" />
<DrawerItem label="Promo codes" icon={TicketPercent} href="/promo-codes" />
<DrawerItem label="Customers" icon={Users} href="/customers" />
<DrawerItem label="Payments" icon={Wallet} href="/payments" />
</DrawerSection>
{/* Pushed to the bottom of the scrolling area, above the footer. */}
<DrawerSection style={styles.bottom}>
<DrawerItem
label="Notifications"
icon={Bell}
href="/notifications"
badge={<Badge label="+9" />}
/>
<DrawerItem label="Settings" icon={Settings} href="/settings" />
<DrawerItem label="Docs" icon={FileText} href="/docs" />
<DrawerItem label="Help" icon={LifeBuoy} href="/help" />
</DrawerSection>
<DrawerFooter style={styles.footer}>
<Pressable
role="button"
aria-label="Switch team, current team Acme Inc"
onPress={onSwitchTeam}
pressedStyle={styles.pressed}
style={[styles.control, styles.team]}
>
<View style={styles.teamLogo} />
<Text variant="label" numberOfLines={1} style={styles.shrink}>
Acme Inc
</Text>
<Icon icon={ChevronsUpDown} size="sm" color="mutedForeground" />
</Pressable>
<Pressable
role="button"
aria-label="Account menu"
onPress={onAccountPress}
pressedStyle={styles.pressed}
style={styles.control}
>
<Avatar fallback="JD" size="sm" />
<Icon icon={ChevronsUpDown} size="sm" color="mutedForeground" />
</Pressable>
</DrawerFooter>
</DrawerContent>
);
}
const useStyles = createStyles((t) => {
const item = {
height: t.scaleValue(40),
borderRadius: t.radius.sm,
paddingHorizontal: t.scaleValue(10),
gap: t.scaleValue(10),
};
return {
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
borderBottomWidth: t.borderWidth.default,
borderBottomColor: t.colors.border,
},
brand: { flex: 1, flexDirection: "row", alignItems: "center", gap: t.spacing[2] },
shrink: { flexShrink: 1 },
bottom: { marginTop: "auto" },
footer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: t.spacing[2],
borderTopWidth: t.borderWidth.default,
borderTopColor: t.colors.border,
},
// The footer controls share a DrawerItem's height, radius and padding (drawer.item Slot).
control: {
...item,
flexDirection: "row",
alignItems: "center",
gap: t.spacing[2],
borderCurve: "continuous",
},
team: { flexShrink: 1 },
teamLogo: {
width: t.iconSize.lg,
height: t.iconSize.lg,
borderRadius: t.radius.sm,
backgroundColor: t.colors.primary,
},
pressed: { backgroundColor: t.colors.muted },
};
});
