Cover header
Drawer panel with a dark cover (large avatar, name, View profile, a bell with a count, an overflow button and a round + action), icon items active in Accent Colour text, a divider and text-only secondary items.
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-03Also installs: drawer, avatar, badge, button, pressable, separator, text, theme.
Packages: expo-router, lucide-react-native@^1.51.0, react-native-svg.
What makes it different
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.
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– 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(this page) – 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 bell is an icon-only Button named "Notifications, <count> unread"; its count Badge is hidden from screen readers.
- The overflow and + buttons are icon-only Buttons named "More" and "Create".
- View profile has role link; the initials Avatar is decorative.
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) => <Drawer03 {...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-03/index.tsx
// drawer-03: cover header. 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) => (
// <Drawer03 {...props} onNotificationsPress={openInbox} onCreatePress={create} />
// )}
// screenOptions={{ drawerStyle: { width: 330 } }}
// />
//
// The cover is dark in both Schemes: it renders the dark Scheme's Colour Roles through a nested
// ThemeProvider. The name, counts, items and routes are placeholders: rename them to your app's.
import { router } from "expo-router";
import { getDrawerStatusFromState } from "expo-router/drawer";
import {
Bell,
EllipsisVertical,
History,
House,
Plus,
ShoppingCart,
Store,
} from "lucide-react-native";
import { StatusBar, View } from "react-native";
import { Avatar } from "@/components/avatar";
import { Badge } from "@/components/badge";
import { Button } from "@/components/button";
import {
DrawerContent,
DrawerHeader,
DrawerItem,
DrawerSection,
type DrawerContentProps,
} from "@/components/drawer";
import { Pressable } from "@/components/primitives/pressable";
import { Separator } from "@/components/separator";
import { Text } from "@/components/text";
import { ThemeProvider, colors, createStyles } from "@/theme";
export type Drawer03Props = DrawerContentProps & {
/** Runs when the bell in the cover is pressed. */
onNotificationsPress?: () => void;
/** Runs when the overflow (⋮) button in the cover is pressed. */
onMorePress?: () => void;
/** Runs when the round + action in the cover is pressed. */
onCreatePress?: () => void;
};
/** Placeholder unread count shown on the bell. */
const UNREAD = 45;
export function Drawer03({
onNotificationsPress,
onMorePress,
onCreatePress,
...props
}: Drawer03Props) {
const styles = useStyles();
const open = props.state ? getDrawerStatusFromState(props.state) === "open" : false;
const viewProfile = () => {
router.navigate("/profile");
props.navigation?.closeDrawer();
};
return (
<DrawerContent {...props}>
{/* The cover is dark in both Schemes, so the status bar over it is light while it's open. */}
{open ? <StatusBar barStyle="light-content" /> : null}
<DrawerHeader style={styles.cover}>
<ThemeProvider scheme="dark">
<Cover
onViewProfile={viewProfile}
onNotificationsPress={onNotificationsPress}
onMorePress={onMorePress}
onCreatePress={onCreatePress}
/>
</ThemeProvider>
</DrawerHeader>
<DrawerSection>
<DrawerItem label="Home" icon={House} href="/" variant="text" />
<DrawerItem label="Shop" icon={Store} href="/shop" variant="text" />
<DrawerItem label="Shopping cart" icon={ShoppingCart} href="/cart" variant="text" />
<DrawerItem label="Same-day delivery" icon={History} href="/delivery" variant="text" />
</DrawerSection>
<Separator />
<DrawerSection>
<DrawerItem label="Invite friends" href="/invite" variant="text" />
<DrawerItem label="Find friends" href="/friends" variant="text" />
<DrawerItem label="Account" href="/account" variant="text" />
<DrawerItem label="Settings" href="/settings" variant="text" />
</DrawerSection>
</DrawerContent>
);
}
type CoverProps = {
onViewProfile: () => void;
onNotificationsPress?: () => void;
onMorePress?: () => void;
onCreatePress?: () => void;
};
/** The cover's content, rendered inside a dark ThemeProvider so every Colour Role is the dark one. */
function Cover({ onViewProfile, onNotificationsPress, onMorePress, onCreatePress }: CoverProps) {
const styles = useCoverStyles();
return (
<View style={styles.root}>
<View style={styles.top}>
<Avatar fallback="JD" size="lg" style={styles.avatar} />
<View style={styles.actions}>
<View>
<Button
variant="ghost"
icon={Bell}
aria-label={`Notifications, ${UNREAD} unread`}
onPress={onNotificationsPress}
/>
<Badge
label={String(UNREAD)}
aria-hidden
importantForAccessibility="no-hide-descendants"
style={styles.count}
/>
</View>
<Button variant="ghost" icon={EllipsisVertical} aria-label="More" onPress={onMorePress} />
</View>
</View>
<View style={styles.identity}>
<Text variant="h4" numberOfLines={1}>
Jane Doe
</Text>
<Pressable role="link" onPress={onViewProfile} style={styles.link}>
<Text variant="small" color="mutedForeground">
View profile
</Text>
</Pressable>
</View>
<Button
icon={Plus}
size="lg"
aria-label="Create"
onPress={onCreatePress}
style={styles.create}
/>
</View>
);
}
const useStyles = createStyles(() => ({
// The dark Scheme's surface in both Schemes, so the cover always reads as dark.
cover: { backgroundColor: colors.dark.muted },
}));
const useCoverStyles = createStyles((t) => ({
root: { gap: t.spacing[3] },
top: { flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" },
avatar: { backgroundColor: t.colors.background },
actions: { flexDirection: "row", alignItems: "center" },
count: { position: "absolute", top: 0, end: 0, pointerEvents: "none" },
identity: { gap: t.spacing[0.5], paddingEnd: t.spacing[16] },
link: { alignSelf: "flex-start" },
create: { position: "absolute", end: 0, bottom: 0, borderRadius: t.radius.full },
}));{components}/drawer-03/index.tsx
// drawer-03: cover header. 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) => (
// <Drawer03 {...props} onNotificationsPress={openInbox} onCreatePress={create} />
// )}
// screenOptions={{ drawerStyle: { width: 330 } }}
// />
//
// The cover is dark in both Schemes: it renders the dark Scheme's Colour Roles through a nested
// ThemeProvider. The name, counts, items and routes are placeholders: rename them to your app's.
import { router } from "expo-router";
import { getDrawerStatusFromState } from "expo-router/drawer";
import {
Bell,
EllipsisVertical,
History,
House,
Plus,
ShoppingCart,
Store,
} from "lucide-react-native";
import { StatusBar, View } from "react-native";
import { Avatar } from "@/components/avatar";
import { Badge } from "@/components/badge";
import { Button } from "@/components/button";
import {
DrawerContent,
DrawerHeader,
DrawerItem,
DrawerSection,
type DrawerContentProps,
} from "@/components/drawer";
import { Pressable } from "@/components/primitives/pressable";
import { Separator } from "@/components/separator";
import { Text } from "@/components/text";
import { ThemeProvider, colors, createStyles } from "@/theme";
export type Drawer03Props = DrawerContentProps & {
/** Runs when the bell in the cover is pressed. */
onNotificationsPress?: () => void;
/** Runs when the overflow (⋮) button in the cover is pressed. */
onMorePress?: () => void;
/** Runs when the round + action in the cover is pressed. */
onCreatePress?: () => void;
};
/** Placeholder unread count shown on the bell. */
const UNREAD = 45;
export function Drawer03({
onNotificationsPress,
onMorePress,
onCreatePress,
...props
}: Drawer03Props) {
const styles = useStyles();
const open = props.state ? getDrawerStatusFromState(props.state) === "open" : false;
const viewProfile = () => {
router.navigate("/profile");
props.navigation?.closeDrawer();
};
return (
<DrawerContent {...props}>
{/* The cover is dark in both Schemes, so the status bar over it is light while it's open. */}
{open ? <StatusBar barStyle="light-content" /> : null}
<DrawerHeader style={styles.cover}>
<ThemeProvider scheme="dark">
<Cover
onViewProfile={viewProfile}
onNotificationsPress={onNotificationsPress}
onMorePress={onMorePress}
onCreatePress={onCreatePress}
/>
</ThemeProvider>
</DrawerHeader>
<DrawerSection>
<DrawerItem label="Home" icon={House} href="/" variant="text" />
<DrawerItem label="Shop" icon={Store} href="/shop" variant="text" />
<DrawerItem label="Shopping cart" icon={ShoppingCart} href="/cart" variant="text" />
<DrawerItem label="Same-day delivery" icon={History} href="/delivery" variant="text" />
</DrawerSection>
<Separator />
<DrawerSection>
<DrawerItem label="Invite friends" href="/invite" variant="text" />
<DrawerItem label="Find friends" href="/friends" variant="text" />
<DrawerItem label="Account" href="/account" variant="text" />
<DrawerItem label="Settings" href="/settings" variant="text" />
</DrawerSection>
</DrawerContent>
);
}
type CoverProps = {
onViewProfile: () => void;
onNotificationsPress?: () => void;
onMorePress?: () => void;
onCreatePress?: () => void;
};
/** The cover's content, rendered inside a dark ThemeProvider so every Colour Role is the dark one. */
function Cover({ onViewProfile, onNotificationsPress, onMorePress, onCreatePress }: CoverProps) {
const styles = useCoverStyles();
return (
<View style={styles.root}>
<View style={styles.top}>
<Avatar fallback="JD" size="lg" style={styles.avatar} />
<View style={styles.actions}>
<View>
<Button
variant="ghost"
icon={Bell}
aria-label={`Notifications, ${UNREAD} unread`}
onPress={onNotificationsPress}
/>
<Badge
label={String(UNREAD)}
aria-hidden
importantForAccessibility="no-hide-descendants"
style={styles.count}
/>
</View>
<Button variant="ghost" icon={EllipsisVertical} aria-label="More" onPress={onMorePress} />
</View>
</View>
<View style={styles.identity}>
<Text variant="h4" numberOfLines={1}>
Jane Doe
</Text>
<Pressable role="link" onPress={onViewProfile} style={styles.link}>
<Text variant="small" color="mutedForeground">
View profile
</Text>
</Pressable>
</View>
<Button
icon={Plus}
size="lg"
aria-label="Create"
onPress={onCreatePress}
style={styles.create}
/>
</View>
);
}
const useStyles = createStyles(() => ({
// The dark Scheme's surface in both Schemes, so the cover always reads as dark.
cover: { backgroundColor: colors.dark.muted },
}));
const useCoverStyles = createStyles((t) => ({
root: { gap: t.spacing[3] },
top: { flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between" },
avatar: { backgroundColor: t.colors.background },
actions: { flexDirection: "row", alignItems: "center" },
count: { position: "absolute", top: 0, end: 0, pointerEvents: "none" },
identity: { gap: t.spacing[0.5], paddingEnd: t.spacing[16] },
link: { alignSelf: "flex-start" },
create: { position: "absolute", end: 0, bottom: 0, borderRadius: t.radius.full },
}));
