Skip to main content

Overview

The DashedBox component creates the classic Windows XP drag-to-select rectangle that appears when clicking and dragging on the desktop. It dynamically calculates position and size based on mouse movements. Location: src/components/DashedBox/index.jsx

Visual Behavior

When a user clicks and drags on the desktop:
  1. Mouse Down: Records starting position
  2. Mouse Move: DashedBox expands/contracts to follow cursor
  3. Mouse Up: Selection completes, DashedBox disappears

Props API

mouse
object
required
Mouse position object from react-use/lib/useMouse hook
{
  docX: number,  // Horizontal position in document
  docY: number   // Vertical position in document
}
startPos
object | null
required
Starting position when drag began, or null if not dragging
{
  x: number,
  y: number
} | null

Usage Example

Basic Implementation

import { useRef, useState } from 'react';
import useMouse from 'react-use/lib/useMouse';
import { DashedBox } from 'components';

function Desktop() {
  const ref = useRef(null);
  const mouse = useMouse(ref);
  const [selecting, setSelecting] = useState(null);

  function onMouseDownDesktop(e) {
    if (e.target === e.currentTarget) {
      setSelecting({ x: mouse.docX, y: mouse.docY });
    }
  }

  function onMouseUpDesktop() {
    if (selecting) {
      setSelecting(null);
    }
  }

  return (
    <div 
      ref={ref}
      onMouseDown={onMouseDownDesktop}
      onMouseUp={onMouseUpDesktop}
    >
      <DashedBox startPos={selecting} mouse={mouse} />
      {/* Desktop icons, etc. */}
    </div>
  );
}

Real-World Example: WinXP Desktop

Source: src/WinXP/index.jsx:26, 192-204, 264
import React, { useRef, useCallback, useState } from 'react';
import useMouse from 'react-use/lib/useMouse';
import { DashedBox } from 'components';
import { START_SELECT, END_SELECT } from './constants/actions';

function WinXP() {
  const ref = useRef(null);
  const mouse = useMouse(ref);
  const [state, dispatch] = useReducer(reducer, initState);

  function onMouseDownDesktop(e) {
    if (e.target === e.currentTarget) {
      dispatch({ type: FOCUS_DESKTOP });
      dispatch({
        type: START_SELECT,
        payload: { x: mouse.docX, y: mouse.docY },
      });
    }
  }

  function onMouseUpDesktop() {
    if (state.selecting) {
      dispatch({ type: END_SELECT });
    }
  }

  return (
    <Container
      ref={ref}
      onMouseUp={onMouseUpDesktop}
      onMouseDown={onMouseDownDesktop}
    >
      <Icons
        mouse={mouse}
        selecting={state.selecting}
        setSelectedIcons={onIconsSelected}
      />
      <DashedBox startPos={state.selecting} mouse={mouse} />
    </Container>
  );
}

Component Implementation

Rectangle Calculation

Source: src/components/DashedBox/index.jsx:4-11
function DashedBox({ mouse, startPos }) {
  function getRect() {
    return {
      x: Math.min(startPos.x, mouse.docX),
      y: Math.min(startPos.y, mouse.docY),
      w: Math.abs(startPos.x - mouse.docX),
      h: Math.abs(startPos.y - mouse.docY),
    };
  }
}
Algorithm:
  • x: Leftmost point (minimum of start and current)
  • y: Topmost point (minimum of start and current)
  • w: Width (absolute difference in X)
  • h: Height (absolute difference in Y)
This ensures the box always expands in the correct direction regardless of drag direction.

Conditional Rendering

Source: src/components/DashedBox/index.jsx:12-27
if (startPos) {
  const { x, y, w, h } = getRect();
  return (
    <div
      style={{
        transform: `translate(${x}px,${y}px)`,
        width: w,
        height: h,
        position: 'absolute',
        border: `1px dotted gray`,
      }}
    />
  );
}
return null;
Key Points:
  • Only renders when startPos is truthy (drag in progress)
  • Uses transform: translate() for positioning (GPU-accelerated)
  • Returns null when not selecting

Styling Details

Inline Styles

The component uses inline styles for dynamic values:
style={{
  transform: `translate(${x}px,${y}px)`,  // Dynamic position
  width: w,                                 // Dynamic width
  height: h,                                // Dynamic height
  position: 'absolute',                     // Fixed positioning
  border: `1px dotted gray`,                // Dashed border
}}

Why Transform Instead of Top/Left?

// Good: GPU-accelerated, better performance
transform: `translate(${x}px, ${y}px)`

// Avoid: Triggers layout recalculation on every mouse move
top: `${y}px`
left: `${x}px`
Using transform provides smoother animations during rapid mouse movements.

Mouse Hook Integration

react-use/lib/useMouse

Source: src/WinXP/index.jsx:3-4, 56-57
import useMouse from 'react-use/lib/useMouse';

function WinXP() {
  const ref = useRef(null);
  const mouse = useMouse(ref);
  
  // mouse = {
  //   docX: 450,
  //   docY: 300,
  //   posX: 450,
  //   posY: 300,
  //   elX: 100,
  //   elY: 50,
  //   elH: 1080,
  //   elW: 1920
  // }
}
Properties Used:
  • docX: X coordinate relative to document
  • docY: Y coordinate relative to document

State Management Pattern

Redux-Style Reducer

Reducer Actions:
// reducer.js
case START_SELECT:
  return {
    ...state,
    selecting: action.payload,  // { x: number, y: number }
    focusing: FOCUSING.DESKTOP
  };

case END_SELECT:
  return {
    ...state,
    selecting: null  // Clears the DashedBox
  };

Component Integration

import { START_SELECT, END_SELECT } from './constants/actions';

function onMouseDownDesktop(e) {
  if (e.target === e.currentTarget) {
    dispatch({
      type: START_SELECT,
      payload: { x: mouse.docX, y: mouse.docY }
    });
  }
}

function onMouseUpDesktop() {
  if (state.selecting) {
    dispatch({ type: END_SELECT });
  }
}

Icon Selection Logic

The DashedBox is typically paired with icon selection logic: Source: src/WinXP/Icons/index.jsx (conceptual)
useEffect(() => {
  if (!selecting) return;
  
  const selectedIds = icons
    .filter(icon => {
      const iconRect = {
        x: icon.position.x,
        y: icon.position.y,
        w: ICON_WIDTH,
        h: ICON_HEIGHT
      };
      
      const selectionRect = {
        x: Math.min(selecting.x, mouse.docX),
        y: Math.min(selecting.y, mouse.docY),
        w: Math.abs(selecting.x - mouse.docX),
        h: Math.abs(selecting.y - mouse.docY)
      };
      
      // Check if rectangles intersect
      return (
        iconRect.x < selectionRect.x + selectionRect.w &&
        iconRect.x + iconRect.w > selectionRect.x &&
        iconRect.y < selectionRect.y + selectionRect.h &&
        iconRect.y + iconRect.h > selectionRect.y
      );
    })
    .map(icon => icon.id);
  
  setSelectedIcons(selectedIds);
}, [selecting, mouse.docX, mouse.docY]);

Event Flow

Complete Selection Cycle

1. User clicks desktop (mousedown)
   → dispatch(START_SELECT, { x: 100, y: 200 })
   → state.selecting = { x: 100, y: 200 }
   → DashedBox renders

2. User drags mouse (mousemove)
   → mouse.docX and mouse.docY update
   → DashedBox recalculates position/size
   → Icons check for intersection

3. User releases mouse (mouseup)
   → dispatch(END_SELECT)
   → state.selecting = null
   → DashedBox disappears
   → Selected icons remain highlighted

Performance Considerations

Optimizations

  1. Transform over Top/Left: GPU-accelerated positioning
  2. Conditional Rendering: Only renders when actively selecting
  3. Simple Calculation: Minimal math per mouse move
  4. No Styled-Components: Inline styles avoid CSS-in-JS overhead

Mouse Move Frequency

Mouse move events fire at ~60fps. The component recalculates on every move:
60 moves/second × 4 calculations (x, y, w, h) = 240 operations/second
This is efficient because:
  • Pure JavaScript math (no DOM manipulation)
  • Single element update (transform is fast)
  • No layout recalculation

Edge Cases

Dragging Off-Screen

// Works correctly - uses document coordinates
mouse.docX = -50;  // Off left edge
mouse.docY = 2000; // Below viewport
The box extends beyond the viewport and scrolls naturally.

Zero-Size Box

// Starting position = current position
w = Math.abs(100 - 100) = 0;
h = Math.abs(200 - 200) = 0;
// Renders 0x0 box (invisible but present)

Negative Direction

// Drag from bottom-right to top-left
startPos = { x: 500, y: 400 };
mouse = { docX: 100, docY: 50 };

x = Math.min(500, 100) = 100;  // ✓ Correct
y = Math.min(400, 50) = 50;    // ✓ Correct
w = Math.abs(500 - 100) = 400; // ✓ Correct
h = Math.abs(400 - 50) = 350;  // ✓ Correct
Algorithm handles all drag directions correctly.

Styling Variations

Windows 10 Style

style={{
  transform: `translate(${x}px,${y}px)`,
  width: w,
  height: h,
  position: 'absolute',
  border: '1px solid #0078D4',
  backgroundColor: 'rgba(0, 120, 212, 0.1)',
}}

macOS Style

style={{
  transform: `translate(${x}px,${y}px)`,
  width: w,
  height: h,
  position: 'absolute',
  border: '1px solid #007AFF',
  backgroundColor: 'rgba(0, 122, 255, 0.2)',
  borderRadius: '4px',
}}

Animated Border

const AnimatedDashedBox = styled.div`
  position: absolute;
  border: 1px dashed gray;
  
  @keyframes march {
    0% { stroke-dashoffset: 0; }
    100% { stroke-dashoffset: 8; }
  }
  
  animation: march 0.5s linear infinite;
`;

Testing

Unit Test Example

import { render } from '@testing-library/react';
import DashedBox from './DashedBox';

test('renders with correct dimensions', () => {
  const mouse = { docX: 200, docY: 150 };
  const startPos = { x: 100, y: 100 };
  
  const { container } = render(
    <DashedBox mouse={mouse} startPos={startPos} />
  );
  
  const box = container.firstChild;
  expect(box).toHaveStyle({
    transform: 'translate(100px,100px)',
    width: '100px',
    height: '50px',
  });
});

test('does not render without startPos', () => {
  const mouse = { docX: 200, docY: 150 };
  
  const { container } = render(
    <DashedBox mouse={mouse} startPos={null} />
  );
  
  expect(container.firstChild).toBeNull();
});

Accessibility

The DashedBox is purely visual and does not require accessibility features:
  • Not keyboard-navigable (mouse-only interaction)
  • Not announced to screen readers
  • Decorative element (selection feedback)
Alternative keyboard selection should be provided separately:
// Shift+Arrow keys for keyboard selection
function onKeyDown(e) {
  if (e.shiftKey && e.key.startsWith('Arrow')) {
    // Select adjacent icon
  }
}

Common Issues

Box Doesn’t Appear

Problem: startPos is never set
// ❌ Wrong: Checking wrong target
onMouseDown={(e) => {
  setSelecting({ x: mouse.docX, y: mouse.docY });
}}

// ✓ Correct: Only trigger on desktop background
onMouseDown={(e) => {
  if (e.target === e.currentTarget) {
    setSelecting({ x: mouse.docX, y: mouse.docY });
  }
}}

Box Position Offset

Problem: Parent element has unexpected positioning
// Ensure parent is positioned
const Container = styled.div`
  position: relative;  /* DashedBox is absolute */
`;

Box Doesn’t Clear

Problem: startPos not reset on mouse up
function onMouseUpDesktop() {
  if (selecting) {
    setSelecting(null);  // Must clear state
  }
}

See Also

Build docs developers (and LLMs) love