Components
Sidebar
The azure-sidebar rail — a white collapsible Sider (228px expanded, 42px collapsed) with a branded 42px header, 34px azure-selected menu rows, module-scoped brand colour, and a mobile drawer variant.
Live example
The rail is an AntD Sider (theme="light", width={228}, collapsedWidth={42}) carrying azure-sidebar azure-sidebar--<module>, a .sidebar-header with the module logo, and a Menu with className="raffles-sidebar-menu". The collapse trigger swaps DoubleLeftOutlined / DoubleRightOutlined.
// React (rate-fe MainLayout.tsx) — desktop rail
<Sider
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
theme="light"
width={228}
collapsedWidth={42}
trigger={collapsed ? <DoubleRightOutlined /> : <DoubleLeftOutlined />}
className="azure-sidebar azure-sidebar--rate"
>
<div className="sidebar-header">
<div className="logo-section">
<div className="logo-icon">
<EditOutlined />
</div>
{!collapsed && (
<div className="logo-text">
<h3>Rate System</h3>
<div className="subtitle">Dashboard & Configuration</div>
</div>
)}
</div>
</div>
<Menu
mode="inline"
selectedKeys={selectedKeys}
openKeys={openKeys}
onOpenChange={setOpenKeys}
onClick={handleMenuClick}
className="raffles-sidebar-menu"
items={menuItems}
style={{ fontSize: 13 }}
/>
</Sider>Surface & header
White surface with a 1px #e1e5e9 right border and a soft rightward shadow. The header is a 42px min-height #f8f9fa band holding .logo-section — a 16px .logo-icon plus .logo-text (14px semibold title, 12px subtitle) at a 12px gap. The built-in .ant-layout-sider-trigger gets the same #f8f9fa treatment with a top border.
.azure-sidebar {
background: #ffffff !important;
border-right: 1px solid #e1e5e9;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
.ant-layout-sider-trigger {
background: #f8f9fa;
border-top: 1px solid #e1e5e9;
color: #6c757d;
&:hover {
background: #e9ecef;
color: #495057;
}
}
}
.sidebar-header {
display: flex;
align-items: center;
box-sizing: border-box;
min-height: 42px;
padding: 6px 16px;
border-bottom: 1px solid #e1e5e9;
background: #f8f9fa;
.logo-section {
display: flex;
align-items: center;
gap: 12px;
.logo-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
.azure-sidebar--rate & {
color: #38b2ac;
}
}
.logo-text {
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #323130;
line-height: 1.2;
}
.subtitle {
font-size: 12px;
color: #605e5c;
line-height: 1.2;
}
}
}
}Module brand colour
Each module tints only its .logo-icon: rate #38b2ac, racar #ff7e14, report #000080. The rule is written against the module modifier — .azure-sidebar--rate & — never against bare .logo-icon.
.logo-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
// Brand color scoped to THIS module's sidebar. These class names are global and identical
// across every MFE; in the shared appshell <head> their `.logo-icon` color rules collide and
// the last-injected one wins, so the icon flipped color when switching MFEs. The module
// modifier makes this rule match only this module's own sidebar.
.azure-sidebar--rate & {
color: #38b2ac;
}
}.sidebar-header, .logo-section and .logo-icon are the same global class names in every MFE, and all module stylesheets land in the one shared appshell <head>. A bare .logo-icon { color: … } therefore matches every module’s sidebar, and whichever module injected its CSS last wins — the icon visibly flips colour when the user switches MFEs. Scoping the colour under .azure-sidebar--<module> makes the rule match only that module’s own rail, whatever the injection order.Icon alignment
On the expanded desktop rail the menu icons must sit in the same left column as the header’s 16px logo icon. Standalone, AntD indents items ~24px; embedded, the appshell’s leaked global 9px wins instead — either way the icons drift out of the header’s column. The override pins item padding to 12px, matches the header’s 12px icon→label gap, and standardizes menu icons at 16px, so one uniform 16px icon column runs from the header down through the menu. It is scoped to the module modifier and to :not(.ant-layout-sider-collapsed) at min-width: 768px — the collapsed rail and the mobile drawer keep their compact 9px inset.
@media (min-width: 768px) {
.azure-sidebar--rate:not(.ant-layout-sider-collapsed) {
.raffles-sidebar-menu > .ant-menu-item,
.raffles-sidebar-menu > .ant-menu-submenu > .ant-menu-submenu-title {
padding-left: 12px !important;
padding-inline-start: 12px !important;
}
// Gap: antd applies the icon→label gap as `margin-inline-start` on the label span
// (`.anticon + span`, default 10px), NOT as the icon's end margin.
.raffles-sidebar-menu > .ant-menu-item > .anticon + span,
.raffles-sidebar-menu > .ant-menu-submenu > .ant-menu-submenu-title > .anticon + span {
margin-inline-start: 12px;
}
// Size: standardize the menu icons to 16px so they match the sidebar-header logo icon —
// one uniform 16px icon column from the header down through the menu.
.raffles-sidebar-menu > .ant-menu-item > .anticon,
.raffles-sidebar-menu > .ant-menu-submenu > .ant-menu-submenu-title > .anticon {
font-size: 16px;
}
}
}Collapsed rail
Collapsing the Sider to collapsedWidth={42} adds .ant-layout-sider-collapsed. The header drops its side padding and centres the logo icon; menu rows compact to 26px with the icon inset 9px from the left edge. Only height and left padding are overridden — nothing else.
.azure-sidebar.ant-layout-sider-collapsed {
.sidebar-header {
padding-left: 0;
padding-right: 0;
justify-content: center;
.logo-section {
justify-content: center;
gap: 0;
}
}
// Collapsed rail: compact rows with the icon inset 9px from the left edge.
.ant-menu-item,
.ant-menu-submenu-title {
height: 26px !important;
padding-left: 9px !important;
}
}display: flex + justify-content: center to collapsed menu items to “centre the icons”. The hidden label span still has width, so flex centring pushes the icon off the 42px rail’s left edge, where overflow: hidden clips it — the icons disappear entirely when the menu is minimized. Keep the plain height + padding-left override.Mobile drawer
Below 768px the persistent Sider is not rendered at all — the same menuElement moves into an overlay Drawer (placement="left", width={228}, closable={false}) that dims the content and auto-closes after a menu click. Rows compact to 26px at 12px type, and nested items restore a 36px indent that the parent padding override flattened.
// rate-fe MainLayout.tsx — the same menuElement, as a temporary overlay
{isMobile && (
<Drawer
placement="left"
width={228}
open={mobileOpen}
onClose={() => setMobileOpen(false)}
closable={false}
styles={{ body: { padding: 0 } }}
className="azure-sidebar-drawer"
title={<span className="mobile-drawer-title">Rate System</span>}
>
{menuElement}
</Drawer>
)}Options
| Name | Type | Default | Description |
|---|---|---|---|
className | string | 'azure-sidebar azure-sidebar--{module}' | Root class plus the module modifier that scopes the brand colour and alignment overrides. |
width | number | 228 | Desktop rail width when expanded. The mobile drawer reuses the same 228px. |
collapsedWidth | number | 42 | Icon-only rail width when collapsed. |
collapsed | boolean | false | Adds .ant-layout-sider-collapsed — centred header icon, 26px compact rows. |
theme | 'light' | 'light' | AntD light theme: white surface, dark text. |
trigger | ReactNode | — | Collapse toggle — DoubleRightOutlined when collapsed, DoubleLeftOutlined when expanded. |
selectedKeys | string[] | — | AntD Menu selected item key, synced from the current route. |
openKeys | string[] | — | AntD Menu open submenu keys. |
items | MenuProps['items'] | — | AntD Menu items array (key, icon, label, children). |
isMobile | boolean | — | Below 768px: skip the Sider and render the overlay Drawer instead. |
Notes
Layout must be sized to the visible viewport via JS — el.getBoundingClientRect().top + window.innerHeight on mount/resize — not a hard 100vh. That keeps .layout-content scrolling flush to the real bottom, so the sticky AppFormFooter stays pinned both standalone and when embedded under the appshell header.<body> and do not inherit the in-rail styles. Set popupClassName: 'raffles-sidebar-popup' on submenu items so the flyout gets the matching white card with icon-aligned rows..sidebar-header under their module modifier; rate-fe and administrator-fe scope only the .logo-icon colour. Menu row metrics also vary slightly per module (report-fe 40px vs rate/administrator 48px in the legacy .sidebar-menu block) — not yet harmonized.