<div id="nav">
  <div id="content">
    <h1>Full-Page Navigation</h1>
    <div>
      <a data-color="#2196F3" style="color: #2196F3" href="">Blue Page</a>
      <a data-color="#F44336" style="color: #F44336" href="">Red Page</a>
      <a data-color="#8BC34A" style="color: #8BC34A" href="">Green Page</a>
      <a data-color="#FF9800" style="color: #FF9800" href="">Orange Page</a>
    </div>

  </div>
  <svg id="rope" xmlns="http://www.w3.org/2000/svg">
    <path fill="transparent" stroke-width="10px" stroke-linejoin="round"/>
    <circle stroke-width="10px" r="20" id="handle"></circle>
  </svg>
</div>

<div id="page">
  <h1>Pull</h1>
</div>
html{
	height: 100%;
}

body{
	font-family: Arial;
	margin: 0px;
	height: 100%;
	--color: #2196F3;
	--bgColor: #424242;
	color: var(--color);
	background-color: var(--bgColor);
}

*{
	-webkit-touch-callout: none; /* iOS Safari */
		  -webkit-user-select: none; /* Safari */
		   -khtml-user-select: none; /* Konqueror HTML */
			 -moz-user-select: none; /* Old versions of Firefox */
			  -ms-user-select: none; /* Internet Explorer/Edge */
				  user-select: none;
}

#nav{
	position: fixed;
	transform: translateY(-70vh);
	overflow: visible;
}

canvas{
	display: none;
}

#nav > #content{
	box-sizing: border-box;
	margin: 0px;
	padding: 20vh 10px 10vh 10px;
	height: 70vh;
	box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
	background-color: #616161;
}

#nav > #content > h1{
	color: #eee;
	margin: 0px;
	text-align: center;
}

#nav > #content > div{
	margin: 30px auto;
	display: flex;
	justify-content: center;
	align-items: center;
}

#nav > #content a{
	margin: 10px;
	font-size: 20px;
}

#nav > #rope{
	width: 100vw;
	height: 100vh;
	display: block;
}

#nav > #rope > circle{
	fill: var(--bgColor);
	stroke: var(--color);
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
}

#nav > #rope > path{
	stroke: var(--color);
}

#page{
	width: 100%;
	height: 100%;
	display: flex;
	justify-content: center;
	align-items: center;
	color: var(--textColor);
}

#page > h1{
	font-size: 50px;
}
class PhysicsEngine{
	constructor(){
		this.navclosedPos = -30;
		this.navHeight = 50;
		this.canvasWidth = 500;
		this.canvasHeight = 600;
		this.initWorld();
	}

	initWorld(){
		const engine = Matter.Engine.create();
		const render = Matter.Render.create({
			element: document.body,
			engine: engine
		});

		let bodies = this.createBodies();
		let constraints = this.createConstraints();
		Matter.World.add(engine.world, [...bodies, ...constraints]);

		Matter.Engine.run(engine);
		Matter.Render.run(render);
	}

	createBodies(){
		this.nav = Matter.Bodies.rectangle(this.canvasWidth/2, this.navclosedPos, this.canvasWidth, this.navHeight, {isSensor: true, inertia: Infinity, mass: 0.1});
		this.rope = this.createRope();
		this.handle = this.rope.bodies[this.rope.bodies.length-1];
		return [this.nav, this.rope];
	}

	createRope(){
		const ropeParts = Matter.Body.nextGroup(true);
		const rope = Matter.Composites.stack(this.canvasWidth/2, this.navclosedPos, 8, 1, -30, 0, (x, y) => {
			return Matter.Bodies.circle(x, y, 15, {
				collisionFilter: { group: ropeParts }
			});
		});

		Matter.Composites.chain(rope, 0, 0.2, 0, -0.2, {stiffness: 1, damping: 0.6, length: 3});
		return rope;
	}

	createConstraints(){
		this.fixMenuToTop = Matter.Constraint.create({ 
			bodyA: this.nav,
			pointA: { x: 0, y: this.navHeight/2},
			pointB: { x: this.canvasWidth/2, y: this.navclosedPos},
			stiffness: 0.5,
			damping: 0.1,
			length: 0
		})

		this.fixMenuToBottom = Matter.Constraint.create({ 
			bodyA: this.nav,
			pointA: { x: 0, y: this.navHeight/2},
			pointB: { x: this.canvasWidth/2, y: 0},
			stiffness: 0.01,
			damping: 0.1,
			length: 0
		})

		const fixRopeToMenu = Matter.Constraint.create({ 
			bodyA: this.nav,
			pointA: { x: 0, y: this.navHeight-20 },
			bodyB: this.rope.bodies[0],
			stiffness: 1,
			length: 0
		})	

		this.fixMouseToHandle = Matter.Constraint.create({ 
			bodyA: this.handle,
			pointB:{x: 0, y: 0},
			stiffness: 0.000000000000001,
			length: 0
		}) 

		return [this.fixMenuToTop, this.fixMenuToBottom, fixRopeToMenu, this.fixMouseToHandle];
	}

	grabHandle(x,y){
		this.moveHandle(x,y);
		this.fixMouseToHandle.stiffness = 1;
	}

	moveHandle(x,y){
		this.fixMouseToHandle.pointB.x = x;
		this.fixMouseToHandle.pointB.y = y;
	}

	releaseHandle(){
		this.fixMouseToHandle.stiffness = 0.000000000000001;
	}
}

class Nav{
	constructor(){
		this.physicsEngine = new PhysicsEngine();
		this.canvasWidth = this.physicsEngine.canvasWidth;
		this.canvasHeight = this.physicsEngine.canvasHeight;

		this.navElm = document.getElementById('content');
		this.ropeContainer = document.getElementById('rope');
		this.ropeElm = this.ropeContainer.querySelector('path');
		this.handleElm = document.getElementById('handle');
		this.pullText = document.querySelector('#page > h1');
		for(let link of Array.from(document.querySelectorAll('a'))){
			link.addEventListener('click', e =>{
				e.preventDefault();
				this.navigateTo(link.getAttribute('data-color'));
			})
		}

		this.handleElm.addEventListener('mousedown', this.grab.bind(this));
		document.body.addEventListener('mousemove', this.move.bind(this));
		document.body.addEventListener('mouseup', this.release.bind(this));

		this.handleElm.addEventListener('touchstart', this.grab.bind(this));
		document.body.addEventListener('touchmove', this.move.bind(this), {passive: false});
		document.body.addEventListener('touchend', this.release.bind(this));

		this.grabbed = false;
		this.isOpen = false;
		this.shouldOpen = false;
		this.inTransition = false;

		this.onResize();
		window.addEventListener('resize', this.onResize.bind(this));

		this.render();
	}

	onResize(){
		this.width = window.innerWidth;
		this.height = window.innerHeight;
		this.ropeContainer.setAttribute('viewport', `0 0 ${this.width} ${this.height}`);
		this.navOpenPos = this.height/10*6;
		this.physicsEngine.fixMenuToBottom.pointB.y = this.getCanvasY(this.navOpenPos);
	}

	grab(e){
		let x = e.clientX || e.touches[0].clientX;
		let y = e.clientY || e.touches[0].clientY;
		this.grabbed = true;
		this.physicsEngine.grabHandle(this.getCanvasX(x), this.getCanvasY(y));
		this.inTransition = false;
		console.log(this.grabbed);
	}

	move(e){
		e.preventDefault();
		
		let x = e.clientX || e.touches[0].clientX;
		let y = e.clientY || e.touches[0].clientY;
		let navPos = this.getScreenY(this.physicsEngine.nav.position.y + this.physicsEngine.navHeight/2)
		if(navPos >= 40 && !this.isOpen && !this.inTransition){
			this.release();
			this.open();
		}
		else if((navPos >= this.navOpenPos + 25 || navPos <= this.navOpenPos - 10) && this.isOpen && !this.inTransition){
			this.release();
			this.close();
			
		}
		else if(this.grabbed)
			this.physicsEngine.moveHandle(this.getCanvasX(x), this.getCanvasY(y));
	}

	release(){
		if(this.grabbed){
			this.physicsEngine.releaseHandle();
			this.grabbed = false;
		}			
	}

	open(){
		this.shouldOpen = true;
		this.inTransition = true;
	}

	close(){
		this.shouldOpen = false;
		this.inTransition = true;
	}

	render(){
		window.requestAnimationFrame(this.render.bind(this));

		if(this.shouldOpen && !this.isOpen){
			if(this.physicsEngine.fixMenuToTop.stiffness >= 0.01){
				this.physicsEngine.fixMenuToTop.stiffness -= 0.02;
				this.physicsEngine.fixMenuToBottom.stiffness += 0.02;
			}
			else
				this.isOpen = true;
		}

		if(!this.shouldOpen && this.isOpen){
			if(this.physicsEngine.fixMenuToTop.stiffness <= 0.5){
				this.physicsEngine.fixMenuToTop.stiffness += 0.03;
				this.physicsEngine.fixMenuToBottom.stiffness -= 0.03;
			}
			else
				this.isOpen = false;
		}

		let path = `M ${this.width/2} ${this.getScreenY(this.physicsEngine.nav.position.y)}`;

		for(let body of this.physicsEngine.rope.bodies){
			path += `L ${this.getScreenX(body.position.x)} ${this.getScreenY(body.position.y)}`;
		}

		let lastBody = this.physicsEngine.rope.bodies[this.physicsEngine.rope.bodies.length - 1];
		this.handleElm.setAttribute('cx', this.getScreenX(lastBody.position.x));
		this.handleElm.setAttribute('cy', this.getScreenY(lastBody.position.y));

		this.ropeElm.setAttribute('d', path);
		//console.log(this.physicsEngine.nav.position.y);
		this.navElm.style.transform = `translate(${0}px, ${this.getScreenY(this.physicsEngine.nav.position.y + this.physicsEngine.navHeight/2)}px)`;
	}

	getScreenX(canvasX){
		return canvasX / this.canvasWidth * this.width;
	}

	getScreenY(canvasY){
		return canvasY / this.canvasHeight * this.height;
	}

	getCanvasX(screenX){
		return screenX / this.width * this.canvasWidth;
	}

	getCanvasY(screenY){
		return screenY / this.height * this.canvasHeight;
	}

	navigateTo(page){
		document.body.style.setProperty('--color', page);
		this.close();
	}
}

const nav = new Nav();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.14.2/matter.min.js