The side navigation is positioned along the left side of the screen that provides quick access to different sections or functionalities of the application.
- Figma - Side Navigation
- Product Concept Note - Connected Navigation (Internal)
- Stackblitz - One Nav Levels POC
- Stackblitz - One Nav Router POC
import { NavLink } from 'react-router-dom';
// Component
<SideNav banner={<CustomActivationCard />}>
<SideNavBody>
{/* L1 Items */}
<SideNavLink as={NavLink} title="Home" icon={HomeIcon} href="/" />
<SideNavLink
as={NavLink}
title="Create Payouts"
trailing={<Button icon={PlusIcon} variant="tertiary" />}
icon={HomeIcon}
href="/create-payouts"
/>
<SideNavLink
as={NavLink}
title="Accounts"
icon={AccountsIcon}
// sets the submenu as active
isActive={true}
href="/accounts"
>
{/* L2 */}
<SideNavLevel title="Accounts">
<SideNavLink as={NavLink} title="Profile" icon={UserIcon} href="/accounts/profile" />
<SideNavLink
as={NavLink}
title="Settings"
icon={UserIcon}
// sets the link as active
isActive={true}
href="/accounts/settings"
/>
<SideNavLink as={NavLink} title="Edit" icon={UserIcon} href="/accounts/settings">
{/* L3 */}
<SideNavLevel>
<SideNavLink as={NavLink} title="Password" icon={PassIcon} href="/accounts/edit/pass" />
<SideNavLink as={NavLink} title="Email" icon={EmailIcon} href="/accounts/edit/email" />
</SideNavLevel>
</SideNavLink>
</SideNavLevel>
</SideNavLink>
{/* Section Heading */}
<SideNavSection title="Products" maxVisibleItems={3}>
<SideNavLink as={NavLink} href="/payment-gateway" title="Payment Gateway" />
<SideNavLink as={NavLink} href="/payment-pages" title="Payment Pages" />
<SideNavLink as={NavLink} href="/payment-links" title="Payment Links" />
<SideNavLink as={NavLink} href="/qr-codes" title="QR Codes" />
<SideNavLink as={NavLink} href="/subscriptions" title="Subscriptions" />
</SideNavSection>
</SideNavBody>
{/* Footer */}
<SideNavFooter>
<Box display="flex" paddingY="spacing.4" paddingX="spacing.3" justifyContent="spacing-between">
<Box display="flex" gap="spacing.3">
<Indicator color="positive" />
<Text>Test Mode</Text>
</Box>
<Switch />
</Box>
<SideNavLink as={NavLink} href="/settings" title="Settings" />
</SideNavFooter>
</SideNav>;Config-driven API
Example of Config-Driven API
<SideNav
routerLink={NavLink}
items={[
{
title: 'Home',
href: '/',
icon: HomeIcon,
},
{
level: 2,
headerTitle: 'Accounts',
icon: AccountsIcon,
href: '/accounts/',
items: [
{
title: 'User Profile',
icon: UserIcon,
href: '/accounts/profile',
},
{
title: 'Business Profile',
icon: SuitcaseIcon,
href: '/accounts/business-profile',
},
],
},
]}
/>- Config-Driven API requires you to have a low-level compound API anyways since compound APIs give higher flexibility into adding leading, trailing, descriptions, and other items when needed.
- Component from Blade is better to kept independent of backend API schema since that schema can change, update, etc and shouldn't be blocked on blade.
- Not binding it to merchant dashboard schema also allows us to use it outside of one dashboard products such admin dashboard, bank portals, products before migration to merchant dashboard
Alternate Levels API
Instead of nesting, we can have a trigger and L2 container separate. In trigger we can pass ref of its L2 container.
const accountsL2Ref = React.useRef(null);
<SideNav>
<SideNavL1>
<SideNavLink title="Home" icon={HomeIcon} href="/" />
<SideNavLink l2Ref={accountsL2Ref} title="Accounts" icon={UserIcon} href="/accounts" />
</SideNavL1>
<SideNavL2 ref={accountsL2Ref}>
<SideNavLink title="Profile" icon={ProfileIcon} href="/accounts/profile" />
<SideNavLink title="Business Profile" icon={BusinessIcon} href="/accounts/business">
<SideNavL3>
<SideNavLink title="Business Info" icon={ProfileIcon} href="/accounts/profile" />
<SideNavLink title="Business Details" icon={ProfileIcon} href="/accounts/profile" />
</SideNavL3>
</SideNavLink>
</SideNavL2>
</SideNav>;- While its the easiest to implement considering how close it is to final DOM structure 🙈, it is complex in understanding and the backend schema we have has nested L1, L2 JSON. So its more complex to loop through a data like that and render this structure
- When you go from L1 -> L2, SideNav slides from one nav level to other where this API makes sense. But when you go from L2 -> L3, L3 becomes a collapsed menu where nested API makes sense.
| Props | Description | Type | Default Value |
|---|---|---|---|
| children | children slot of SideNav, accepts SideNavBody, SideNavFooter | JSX | |
| banner | Slot at the top of SideNav for rendering critical UI info like Activation Pending Card. Meant for critical information only, if you see it being used for promotional banners, dial 100 | JSX |
<SideNav>{/* children */}</SideNav>
<SideNav
banner={
<Card href="/activate">
{/* Activation Pending Styles */}
</Card>
}
>
{/* children */}
</SideNav> |
|
| Props | Description | Type | Default Value |
|---|---|---|---|
| children | children slot of SideNavBody, accepts SideNavLink, SideNavSection, SideNavLevel | JSX |
<SideNav>
<SideNavBody>{/* children */}</SideNavBody>
<SideNavFooter>{/* children */}</SideNavFooter>
</SideNav>| Props | Description | Type | Default Value |
|---|---|---|---|
| title | title of SideNavLink | string | |
| as | as prop for passing React Router's NavLink | NavLinkComponentType | |
| href | URL to navigate to. Internally links to to attribute of router |
string | |
| isActive | Sets the link as selected / active | boolean | undefined |
| target | anchor tag target attribute target - MDN Documentation | AnchorTargetType | _self |
| rel | anchor tag rel attribute rel - MDN Documentation | AnchorRelType | target === ' _blank ' ? ' noreferrer noopener ' : undefined |
| onClick | Click handler on item | (e: React.MouseEvent) => void | |
| icon | Blade's Icon Component | IconComponent | |
| trailing | Trailing Slot of Item. It is visible on hover only. Can be used for adding Quick Shortcut Button, Trailing Text | JSX | |
| titleSuffix | Slot after the title to add Badge, Counter | JSX | |
| tooltip | Object with props that are forwarded to tooltip | TooltipProps | undefined |
| children | SideNavLink children slot. Items inside children turn into next level item with parent as a trigger | JSX | undefined |
Nested SideNavLevel components create new levels. This can be used to create L1 - L2 - L3 levels in your navbar
| Props | Description | Type | Default Value |
|---|---|---|---|
| children | children slot. Accepts SideNavLink as children | JSX |
| Props | Description | Type | Default Value |
|---|---|---|---|
| title | title of the section | string | |
| maxVisibleItems | Number of items visible (rest go inside +x more collapsible) | number | undefined |
| onToggleVisibleItems | Callback when collapsed items are expanded or collapsed back | (isExpanded: boolean) => void | undefined |
| children | Children slot. For SideNavLink children items | JSX |
A generic non-link item that gives you leading, trailing slots to render the components as per your usecase.
Note
SideNavItem cannot be active, cannot be link, and cannot be button. It can be static item with something interaction in trailing slot
| Props | Description | Type | Default Value |
|---|---|---|---|
| title | title of SideNavItem | string | |
| as | as prop for passing render element. Use label if trailing has some input component |
div | |
| leading | leading slot. Render indicator / icon here | ||
| trailing | Trailing Slot of Item. Render Switch, or any other blade component here | JSX | |
| tooltip | Object with props that are forwarded to tooltip | TooltipProps | undefined |
For Example of SideNavItem, check SideNavFooter example below.
| Props | Description | Type | Default Value |
|---|---|---|---|
| children | Children slot | JSX |
- All items should be accessible by
TAB. Including going between levels L1, L2, L3 - We should use Blade's
SkipNavutility to provide option of skipping nav and going to content - React Router automatically handles
aria-current="page". Verify that it is working as expected.
- RazorpayX Navigation Bar (Internal)
- Razorpay Merchant Dashboard Navigation Bar (Internal)
- SideNavigation - Atlassian DS
- SideNav - Primer / GitHub
- SideNav - Carbon DS
- SideNav - BaseWeb DS / Uber
- sidenav - Spectrum / Adobe
-
SideNav
-
SideNavLink
-
- We pass react router on parent SideNav component
- Example
<SideNav routerLink={NavLink}> <SideNavLink /> <SideNavLink /> </SideNav>
- Pros: Router Link defined at one place on top so less likely to be missed / changed between multiple items
- Cons: Uncommon pattern
- We pass it from as prop on SideNavLink component
- Example
<SideNav> <SideNavLink as={NavLink} /> <SideNavLink as={NavLink} /> </SideNav>
- Pros: Common pattern
- Cons: Requires defining NavLink at each item (for projects not part of One Dashboard like Admin Dashboard, Bank Portals, etc)
We are going with
as={NavLink} -
Earlier we started the API by saying that marking of the link as active based on route will be handled internally in blade component. There are some implemetation constraints when we go with this approach of handling active link internally
- We don't have access to hooks like
useLocationor utilities likematchPathinside Blade since blade is library indepedent of the routing logic - Although React Router's NavLink automatically adds
aria-current="page"to active link which can be used to change colors in CSS, it doesn't give us access to handling state internally for our L1 -> L2 navigations Here's the related proposal I created in React Router's Repo. - Overall there's no stable way to manage the
isActivestate internally in React x React Router setup currently.
- We don't have access to hooks like
- Should L2 trigger also change the route?
- Conclusion: Yes
-
-
Problem and Ideal state is documented in Doc - URL Patterns Based on Navigation (Internal)
-
Conclusion:
- While we discussed this internally (in engineering), it becomes out of scope for Blade since we can't handle active state internally anyways
- Although its a problem that we'll have to solve on product level hence documented this as we had good insights on what are pros of having nested routing
-










