Skip to main content

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

data
array
required
Array of menu item objects (see Item Structure below)
style
object
Inline styles for the menu container
onClick
function
required
Click handler called with the item’s text when clicked
(itemText: string) => void
className
string
CSS class name (automatically set by styled-components)
left
string
default:"100%"
CSS left position for submenu positioning
bottom
string
default:"-1px"
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:
{
  type: 'separator'
}

Type: “menu”

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' }
];

Nested Submenu with Custom Positioning

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

Nested Submenu Display

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:
  1. User clicks “Paint” in Accessories > Graphics > Paint
  2. onClick('Paint') is called
  3. 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

FeatureSubMenuWindowDropDowns
UsageStart menu, hierarchical navigationApplication menu bars
LayoutFlexbox (vertical)CSS Grid (precise alignment)
StylingBlue theme (#1b65cc)Blue theme (#1660e8)
HotkeysNot supportedSupported (e.g., “Ctrl+S”)
IconsAlways shownOptional symbols
Left borderBlue accent barNone
OpeningHover onlyClick-to-open, hover-to-switch

Applications Using SubMenu

  • 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

  1. Icon Consistency: Use 16x16 or 32x32 icons consistently within a menu
  2. Nesting Limit: Avoid more than 3 levels of nesting
  3. Separator Usage: Group related items with separators
  4. Text Length: Keep menu text concise (under 30 characters)
  5. 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

Build docs developers (and LLMs) love