<div id="root"></div>
<div id="context-menu"></div>
html,
body {
    min-height: 100%;
    
}

body {
    position: relative;
    color: #666;
    background-color: #a18cd1;
    background-image: linear-gradient(to top, #a18cd1 0%, #fbc2eb 100%);
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

.portal-toggle {
    position: absolute;
    display: block;
    padding: 6px 10px;
    bottom: 16px;
    left: 50%;
    outline: none;
    border: none;
    border-radius: 4px;
    min-width: 140px;
    margin-left: -70px;
    cursor: pointer;
    color: #666;
    background-color: #eee;
    box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.2),
        0 4px 6px rgba(0, 0, 0, 0.1);
    &:hover {
        color: #fff;
        background-color: #42b6f4;
        outline: none;
    }
}

.window {
    position: fixed;
    top: 18%;
    left: 18%;
    right: 18%;
    bottom: 18%;
    background: #fff;
    overflow: hidden;
    border-radius: 5px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);

    .scroll {
        position: absolute;
        top: 32px;
        left: 0;
        right: 0;
        bottom: 0;
        overflow: scroll;
        cursor: crosshair;
    }

    header {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 32px;
        line-height: 32px;
        background-color: #d6d6d6;
        border-bottom: 1px solid #ddd;
        z-index: 1;
    }

    .controls {
        position: absolute;
        left: 10px;
        top: 0;
    }

    .red,
    .yellow,
    .green {
        display: inline-block;
        margin-right: 5px;
        width: 12px;
        height: 12px;
        outline: none;
        border: 0;
        border-radius: 6px;
        box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.1);
    }

    .red {
        background-color: #ed5b44;
    }
    .yellow {
        background-color: #fcc846;
    }
    .green {
        background-color: #60d657;
    }

    h2 {
        margin: 0;
        font-size: 16px;
        font-weight: normal;
        text-align: center;
    }
}

.context-menu {
    position: fixed;
    margin: 0;
    background: #eee;
    overflow: hidden;
    border-radius: 4px;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
    min-width: 250px;

    ul {
        margin: 0;
        padding: 0;
    }

    li {
        list-style-type: none;
        line-height: 28px;
        padding: 0 24px;

        &:hover {
            color: #fff;
            background-color: #42b6f4;
        }
    }

    .menu-item {
        cursor: pointer;
    }

    hr {
        background: #ccc;
        height: 1px;
        border: none;
    }
}

.artboard {
    position: relative;
    width: 5000px;
    height: 5000px;
    background-color: #f3f3f3;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj4gIDxyZWN0IGZpbGw9InJnYmEoMCwgMCwgMCwgMC4wNSkiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMCIgaGVpZ2h0PSIxMCIgLz4gIDxyZWN0IGZpbGw9InJnYmEoMCwgMCwgMCwgMC4wNSkiIHg9IjEwIiB5PSIxMCIgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiAvPjwvc3ZnPg==');
    overflow: hidden;
}

.shape {
    position: absolute;
    height: 100px;
    width: 100px;
    box-sizing: border-box;

    &.square {
        height: 100px;
        width: 100px;
        border: solid 20px #ba8bf4;
    }
    &.circle {
        height: 130px;
        width: 130px;
        border-radius: 50%;
        border: solid 20px #fcec92;
        &:before {
            border-radius: 50%;
        }
    }
    &.diamond {
        height: 80px;
        width: 80px;
        border: solid 20px #67efa2;
        transform: rotate(45deg);
        transform-origin: 50% 50%;
        &:before {
            transform: rotate(45deg);
            transform-origin: 50% 50%;
        }
    }

    &:before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: #42b6f4;
        animation: shape-added 0.5s ease-out forwards;
    }
}

@keyframes shape-added {
    0% {
        transform: scale(1.5);
        opacity: 0.5;
    }
    100% {
        transform: scale(2.5);
        opacity: 0;
    }
}
View Compiled
/**
 * -------------------------------------------------------
 * Context Menu
 * -------------------------------------------------------
 */

const ContextMenu = ({
    addShape,
    closeMenu,
    portalEl,
    menuX,
    menuY,
    artX,
    artY,
}) => {
    const menu = (
        <nav
            className="context-menu"
            style={{
                top: menuY,
                left: menuX,
            }}
        >
            <ul>
                <li
                    className="menu-item"
                    onClick={event => {
                        addShape({ x: artX, y: artY, type: 'square' });
                        closeMenu();
                    }}
                >
                    Add Shape: Square
                </li>
                <li
                    className="menu-item"
                    onClick={event => {
                        addShape({ x: artX, y: artY, type: 'circle' });
                        closeMenu();
                    }}
                >
                    Add Shape: Circle
                </li>
                <li
                    className="menu-item"
                    onClick={event => {
                        addShape({ x: artX, y: artY, type: 'diamond' });
                        closeMenu();
                    }}
                >
                    Add Shape: Diamond
                </li>
                {/*
                    Fake items to make the menu bigger and more likely to not fit in the window
                */}
                <hr />
                <li>Fake Menu Item: Foo</li>
                <li>Fake Menu Item: Bar</li>
                <hr />
                <li>Fake Menu Item: Bat</li>
                <li>Fake Menu Item: Bin</li>
                <li>Fake Menu Item: Foe</li>
                <hr />
                <li>Fake Menu Item: Fat</li>
                <li>Fake Menu Item: Bot</li>
            </ul>
        </nav>
    );

    // if the portal el is available render into it.
    // This is just for demo purposes to toggle between states.

    return portalEl ? ReactDOM.createPortal(menu, portalEl) : menu;
};


/**
 * -------------------------------------------------------
 * Window
 * -------------------------------------------------------
 */

const Window = ({ children, title }) => (
    <section className="window">
        <header>
            <h2>{title}</h2>
            <div className="controls">
                <span className="red" />
                <span className="yellow" />
                <span className="green" />
            </div>
        </header>
        {children}
    </section>
);

/**
 * -------------------------------------------------------
 * Shape
 * -------------------------------------------------------
 */

class Shape extends React.Component {
    render() {
        const { x, y, type } = this.props;
        return <div className={`shape ${type}`} style={{ top: y, left: x }} />;
    }
}

/**
 * -------------------------------------------------------
 * Artboard
 * -------------------------------------------------------
 */

function randomIntBetween(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

class Artboard extends React.Component {
    constructor() {
        super();
        this.portalEl = document.getElementById('context-menu');
    }

    state = {
        portalActive: false,
        menuOpen: false,
        menuX: 0,
        menuY: 0,
        shapes: this.generateRandomArt(),
    };

    generateRandomArt() {
        const mw = 5000;
        const mh = 5000;
        const types = ['circle', 'square', 'diamond'];
        const shapes = new Array(randomIntBetween(80, 150))
            .fill({})
            .map(shape => ({
                x: randomIntBetween(0, mw),
                y: randomIntBetween(0, mh),
                type: types[randomIntBetween(0, types.length)],
            }));
        return shapes;
    }

    handleClick = event => {
        const { clientX: x, clientY: y } = event;
        const { top, left } = this.artboard.getBoundingClientRect();
        const artX = x - left;
        const artY = y - top;

        this.setState(({ menuOpen }) => ({
            menuOpen: !menuOpen,
            menuX: x,
            menuY: y,
            artX,
            artY,
        }));
    };

    handleCloseMenu = () => {
        this.setState(() => ({
            menuOpen: false,
        }));
    };

    handleTogglePortal = () => {
        this.setState(({ portalActive }) => ({
            portalActive: !portalActive,
        }));
    };

    handleAddObject = newShape => {
        const shapes = [...this.state.shapes, newShape];
        this.setState(() => ({ shapes }));
    };

    render() {
        const { children } = this.props;
        const {
            portalActive,
            menuOpen,
            menuX,
            menuY,
            artX,
            artY,
            shapes,
            event,
        } = this.state;

        return (
            <React.Fragment>
                <div className="scroll" onClick={this.handleClick}>
                    <div
                        className="artboard"
                        ref={ref => (this.artboard = ref)}
                    >
                        {shapes.map((config, i) => (
                            <Shape key={i} {...config} />
                        ))}
                    </div>
                </div>

                {/*
                    This is our context menu that should behave as though
                    it's a global menu item, even though we are rendering
                    within the confines of the <Window>
                */}
                {menuOpen && (
                    <ContextMenu
                        menuX={menuX}
                        menuY={menuY}
                        artX={artX}
                        artY={artY}
                        portalEl={portalActive ? this.portalEl : null}
                        addShape={this.handleAddObject}
                        closeMenu={this.handleCloseMenu}
                    />
                )}

                {/*
                    This button and state only exists to toggle the
                    portal on/off for illustrative purposes.
                */}
                <button
                    className="portal-toggle"
                    onClick={this.handleTogglePortal}
                >
                    {portalActive ? 'Disable Portal' : 'Activate Portal'}
                </button>
            </React.Fragment>
        );
    }
}

const App = () => (
    <main>
        <Window title="Artboard Window">
            <Artboard />
        </Window>
    </main>
);

const run = () => {
    const root = document.getElementById("root");

    ReactDOM.render(<App />, root);
};

run();
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.min.js
  2. https://unpkg.com/prop-types@15.5.10/prop-types.js
  3. https://unpkg.com/react@16/umd/react.development.js
  4. https://unpkg.com/react-dom@16/umd/react-dom.development.js