Overview
The SubMenu component creates Windows XP-style nested menus with icons, separators, and hover effects. It’s used primarily in the Start menu for hierarchical navigation (e.g., “All Programs” with subfolders).
Location: src/components/SubMenu/index.jsx
Props API
Array of menu item objects (see Item Structure below)
Inline styles for the menu container
Click handler called with the item’s text when clicked(itemText: string) => void
CSS class name (automatically set by styled-components)
CSS left position for submenu positioning
CSS bottom position for submenu positioning
Item Structure
Menu items can be one of three types:
Type: “item”
Clickable menu item with icon:
{
type: 'item',
icon: iconImageSrc, // Required: Image source
text: 'Notepad' // Required: Display text
}
Type: “separator”
Horizontal divider:
Nested submenu with arrow indicator:
{
type: 'menu',
icon: iconImageSrc,
text: 'Accessories',
items: [...], // Required: Array of sub-items
bottom: '0px' // Optional: Position adjustment
}
Usage Example
Basic Implementation
import SubMenu from 'components/SubMenu';
import calculatorIcon from 'assets/calculator.png';
import notepadIcon from 'assets/notepad.png';
import folderIcon from 'assets/folder.png';
const menuData = [
{ type: 'item', icon: calculatorIcon, text: 'Calculator' },
{ type: 'item', icon: notepadIcon, text: 'Notepad' },
{ type: 'separator' },
{
type: 'menu',
icon: folderIcon,
text: 'Accessories',
items: [
{ type: 'item', icon: notepadIcon, text: 'Paint' },
{ type: 'item', icon: calculatorIcon, text: 'Calculator' }
]
}
];
function MyMenu() {
const handleClick = (itemText) => {
console.log('Clicked:', itemText);
};
return (
<SubMenu
data={menuData}
onClick={handleClick}
style={{ position: 'absolute', bottom: 0 }}
/>
);
}
Source: src/WinXP/Footer/FooterMenu.jsx:155-157
import SubMenu from 'components/SubMenu';
import { AllPrograms } from './FooterMenuData';
<Item text="All Programs" icon={empty}>
{hovering === 'All Programs' && (
<SubMenu data={AllPrograms} onClick={onClick} />
)}
</Item>
FooterMenuData.js structure:
export const AllPrograms = [
{
type: 'menu',
icon: allProgramsIcon,
text: 'Accessories',
items: [
{ type: 'item', icon: calculatorIcon, text: 'Calculator' },
{ type: 'item', icon: notepadIcon, text: 'Notepad' },
{ type: 'item', icon: paintIcon, text: 'Paint' },
{ type: 'separator' },
{
type: 'menu',
icon: gamesIcon,
text: 'Games',
items: [
{ type: 'item', icon: minesweeperIcon, text: 'Minesweeper' },
{ type: 'item', icon: pinballIcon, text: 'Pinball' }
]
}
]
},
{ type: 'separator' },
{ type: 'item', icon: ieIcon, text: 'Internet Explorer' }
];
Source: src/WinXP/Footer/FooterMenu.jsx:181-187
<SubMenu
left="153px" // Custom horizontal offset
data={MyRecentDocuments}
onClick={onClick}
/>
Component Implementation
Main Component
Source: src/components/SubMenu/index.jsx:4-21
function SubMenu({ className, data, style, onClick }) {
const [hoverIndex, setHoverIndex] = useState(-1);
return (
<div style={{ ...style }} className={className}>
{data.map((item, index) => (
<SubMenuItem
onClick={onClick}
onHover={setHoverIndex}
key={index}
hover={hoverIndex === index}
item={item}
index={index}
className={className}
/>
))}
</div>
);
}
Source: src/components/SubMenu/index.jsx:23-66
const SubMenuItem = ({ index, item, className, hover, onHover, onClick }) => {
function _onMouseOver() {
onHover(index);
}
function _onClick() {
onClick(item.text);
}
switch (item.type) {
case 'item':
return (
<div
onClick={_onClick}
onMouseEnter={_onMouseOver}
className={`${className}-item`}
>
<img className={`${className}-img`} src={item.icon} alt="" />
<div className={`${className}-text`}>{item.text}</div>
</div>
);
case 'separator':
return <div className={`${className}-separator`} />;
case 'menu':
return (
<div
onMouseEnter={_onMouseOver}
className={`${className}-item ${hover ? 'hover' : ''}`}
>
<img className={`${className}-img`} src={item.icon} alt="" />
<div className={`${className}-text`}>{item.text}</div>
<div className={`${className}-arrow`}>
{hover && (
<StyledSubMenu
data={item.items}
bottom={item.bottom}
onClick={onClick}
/>
)}
</div>
</div>
);
}
};
Styled-Components Implementation
Source: src/components/SubMenu/index.jsx:68-139
Container Styling
const StyledSubMenu = styled(SubMenu)`
position: absolute;
z-index: 1;
left: ${({ left }) => left || '100%'};
bottom: ${({ bottom }) => bottom || '-1px'};
background-color: white;
padding-left: 1px;
box-shadow: inset 0 0 0 1px #72ade9, 2px 3px 3px rgb(0, 0, 0, 0.5);
`;
Item Styling
&-item {
height: 25px;
display: flex;
align-items: center;
padding: 0 10px;
box-shadow: inset 3px 0 #4081ff;
position: relative;
padding-right: 22px;
color: black;
}
&-item.hover {
background-color: #1b65cc;
color: white;
}
&-item:hover {
background-color: #1b65cc;
color: white;
&-arrow:before {
border-left-color: #fff;
}
}
Separator Styling
&-separator {
padding: 0 5px;
height: 2px;
box-shadow: inset 3px 0 #4081ff;
background: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0) 100%
);
}
Arrow Indicator
&-arrow {
position: absolute;
right: 0;
height: 100%;
width: 10px;
&:before {
top: 9px;
right: 6px;
content: '';
display: block;
border: 4px solid transparent;
border-right: 0;
border-left-color: #000;
position: absolute;
}
}
&-item:hover &-arrow:before {
border-left-color: #fff;
}
Hover Behavior
State Management
Source: src/components/SubMenu/index.jsx:5
const [hoverIndex, setHoverIndex] = useState(-1);
- Only one item can be hovered at a time
- Hovering over a menu item shows its submenu immediately
- Moving to a different item hides the previous submenu and shows the new one
Source: src/components/SubMenu/index.jsx:53-59
<div className={`${className}-arrow`}>
{hover && (
<StyledSubMenu
data={item.items}
bottom={item.bottom}
onClick={onClick}
/>
)}
</div>
Submenus only render when their parent item is hovered.
Positioning
Default Positioning
left: 100%; /* Appears to the right of parent */
bottom: -1px; /* Aligns bottom edge with parent item */
Custom Positioning
Pass left or bottom props to adjust:
<SubMenu
data={items}
left="150px" // Fixed offset from parent
bottom="0px" // Align bottom edges exactly
onClick={handleClick}
/>
Click Propagation
Source: src/components/SubMenu/index.jsx:27-29
function _onClick() {
onClick(item.text);
}
Clicks propagate up through all nested levels:
- User clicks “Paint” in
Accessories > Graphics > Paint
onClick('Paint') is called
- Parent menu receives the click and can handle it
The parent component is responsible for:
- Closing the menu
- Taking action based on
itemText
Visual Features
Blue Left Border
box-shadow: inset 3px 0 #4081ff;
All items and separators have a blue accent on the left edge.
Gradient Separator
background: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0) 100%
);
Separators use a subtle gradient that fades at the edges.
Drop Shadow
box-shadow: inset 0 0 0 1px #72ade9, 2px 3px 3px rgb(0, 0, 0, 0.5);
Menus have both an inset border and outer shadow for depth.
Differences from WindowDropDowns
| Feature | SubMenu | WindowDropDowns |
|---|
| Usage | Start menu, hierarchical navigation | Application menu bars |
| Layout | Flexbox (vertical) | CSS Grid (precise alignment) |
| Styling | Blue theme (#1b65cc) | Blue theme (#1660e8) |
| Hotkeys | Not supported | Supported (e.g., “Ctrl+S”) |
| Icons | Always shown | Optional symbols |
| Left border | Blue accent bar | None |
| Opening | Hover only | Click-to-open, hover-to-switch |
- Footer Menu (
src/WinXP/Footer/FooterMenu.jsx:156) - “All Programs” submenu
- Recent Documents (
src/WinXP/Footer/FooterMenu.jsx:182) - Document list
- Connect To (
src/WinXP/Footer/FooterMenu.jsx:224) - Network connections
Best Practices
- Icon Consistency: Use 16x16 or 32x32 icons consistently within a menu
- Nesting Limit: Avoid more than 3 levels of nesting
- Separator Usage: Group related items with separators
- Text Length: Keep menu text concise (under 30 characters)
- Positioning: Test nested menus don’t overflow screen edges
Accessibility Considerations
Current implementation does not include:
- Keyboard navigation
- ARIA attributes
- Focus management
For production use, consider adding:
<div
role="menuitem"
tabIndex={0}
onKeyDown={handleKeyDown}
aria-haspopup={item.type === 'menu'}
>
See Also