Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
<body class="panel">
	<div class="container" id="panelcontainer">
		<div class="content">
			<div class="section-container">
				<section class="panel" id="panel_0"></section>
				<section class="panel" id="panel_1"></section>
				<section class="panel" id="panel_2"></section>
				<section class="panel" id="panel_3"></section>
			</div>
		</div>
	</div>
</body>
</html>
              
            
!

CSS

              
                @charset "UTF-8";

html,
body {
	overscroll-behavior: none;
}

html {
	height: 100%
}
body.panel {
	width: 100%;
	height: 100vh;
	position: absolute;
	overflow-x: hidden;
	top: 0;
	left: 0;
	padding: 0;
	margin: 0;

	div.container {
		div.content {
			padding: 0;
			margin: 0;
			max-width: none;
			height: 100%;
			div.section-container {
				position: relative;
				width: 100%;
				height: 100%;
				top: 0;
				left: 0;
				overflow: hidden;

				section.panel {
					position: absolute;
					width: 100%;
					height: 100vh;
					background-repeat: no-repeat;
					background-size: cover;
					background-position: center center;
					will-change: opacity, transform;

					&.active {
						pointer-events: all;
					}
				}
			}
		}
	}
}
#panel_0 {
	background-image: url(https://assets.codepen.io/9556641/img-10.webp);
	background-color: olive;
}

#panel_1 {
	background-image: url(https://assets.codepen.io/9556641/img-8.webp);
	background-color: teal;
}

#panel_2 {
	background-image: url(https://assets.codepen.io/9556641/img-21.webp);
	background-color: steelBlue;
}

#panel_3 {
	background-image: url("https://assets.codepen.io/9556641/img-26.webp");
	background-color: sienna;
}
              
            
!

JS

              
                var mainManager: PanelManager;
document.addEventListener('DOMContentLoaded', init);

function init(){
	if ('scrollRestoration' in history) {
		history.scrollRestoration = 'manual';
	}
	window.scroll(0,0);
	if(document.readyState === 'complete'){
		buildManager();
	} else {
		window.addEventListener('load', buildManager);
	}
}

function buildManager():void{
	gsap.registerPlugin(ScrollTrigger);
	mainManager = new PanelManager();
}

class PanelManager {

	public static containerId:string = 'panelcontainer';

	private container:HTMLElement;
	private currentPanel:Panel;
	private timeline:GSAPTimeline = null;
	private scrollTrigger:ScrollTrigger = null;
	private dummyCalculationCounter:number = 0;
	public winW:number = 0;
	public winH:number = 0;
	public devPixRatio:number = 1;

	public winW_beforeResize:number = null;
	public winH_beforeResize:number = null;
	public devPixRatio_beforeResize:number = null;

	public panels:Array<Panel> = [];

	public yPercentToAnimateInFrom:number = 4;
	public scaleToAnimateInFrom:number = 1.08;


	constructor (){

		this.container = document.getElementById(PanelManager.containerId);
		this.calculateViewPort();
		let nodes:NodeList = this.container.querySelectorAll('section.panel');
		for (let i:number = 0; i < nodes.length; i++) {
			const node:HTMLElement = nodes[i] as HTMLElement;
			const slide:Panel = new Panel(this, i, node);
			this.panels.push(slide);
		}
		this.currentPanel = this.panels[0];
		this.currentPanel.setActive();
		this.setupSizes();
		this.preparePanels4Show();
		this.buildTimeline();
		this.createScrolltrigger();
		window.addEventListener('delayedResize', this.handleResize);
	}

	private handleResize = (e:Event):void => {
		if(panelDelayedResize.instance.likelyIsMobile && panelDelayedResize.instance.likelyIsMobileAddressBarHiding){
			//console.log('%cWe ignore this resize, as it is likely irrelevant.', 'color:azure');
			return;
		}
		console.trace('🔥🔥 resize to handle 🔥🔥');
		this.winH_beforeResize = this.winH;
		this.winW_beforeResize = this.winW;
		this.devPixRatio_beforeResize = this.devPixRatio;
		try{
			this.scrollTrigger.kill();
			this.scrollTrigger = null;
		} catch {
			console.log(this.scrollTrigger);
		} finally {
			console.log('Number of scroll triggers after attempt to kill:' + ScrollTrigger.getAll().length);
		}
		window.scroll(0,0);
		this.setupSizes();
		this.unPrepareSlides4Show();
		window.requestAnimationFrame( ()=> window.requestAnimationFrame(this.initiatePanelDummyCalculations));
	}

	private resizeFinalize = ():void => {
		this.reInstateScrollTrigger();
	}

	private reInstateScrollTrigger = ():void => {
		console.trace('%cRe-Instantiate ScrollTrigger', 'color:red');
		const targetWinScrollTop: number = Math.floor(this.currentPanel.myID * this.winH);
		this.timeline.pause();
		this.preparePanels4Show();
		this.createScrolltrigger();
		if(this.currentPanel.myID !== 0){
			console.log('actual scroll after scrollTrigger creation: ' + document.documentElement.scrollTop);
			this.scrollTrigger.scroll(targetWinScrollTop);
			console.log('actual scroll after scrollTrigger scroll(): ' + document.documentElement.scrollTop);
		}
	}

	private createScrolltrigger = ():void => {
		if(this.scrollTrigger !== null){
			console.warn('ScrollTrigger not null');
		}
		ScrollTrigger.config(
			{
				autoRefreshEvents: "DOMContentLoaded"
			}
		);

		this.scrollTrigger = ScrollTrigger.create({
			trigger: this.container,
			start: 'top top',
			end: 'bottom bottom',
			scrub: true,
			pin: true,
			invalidateOnRefresh: true,
			snap: {
				snapTo: 'labelsDirectional',
				duration: {min: 0.1, max: .3},
				directional: true,
				inertia: false
			},
			animation: this.timeline,
			pinSpacing: false,
			id: 'panelScrollTrigger',
			onToggle: self => {
				this.findCurrentSlide((self.animation as GSAPTimeline).currentLabel(), 'toggle');
			},
			onSnapComplete: self => {
				this.findCurrentSlide((self.animation as GSAPTimeline).currentLabel(), 'complete');
			},
			onEnter: self => {
				console.log('🏁 Entering scroll trigger at:' + self.scroll());
			},
			onUpdate: self => {
				console.log('🔹 updating: ' + self.scroll());
			},
			onRefresh: self => {
				console.groupCollapsed('🟠 refresh trace');
				console.trace('scroll trigger refresh: ' + self.scroll());
				console.groupEnd();
			}

		})
		ScrollTrigger.addEventListener('scrollEnd', this.endScrollHandle);
		ScrollTrigger.addEventListener('scrollStart', this.startScrollHandle);
	}

	private startScrollHandle = ():void => {
		console.log('🚀 start scroll win pos: ' + document.documentElement.scrollTop);
	}

	private endScrollHandle = ():void => {
		console.log('🛑 END-scroll Handler win pos: ' + document.documentElement.scrollTop);
		const slideID:number = this.findLastSlideVisible();
		if(slideID !== this.currentPanel.myID){
			this.currentPanel.setInActive();
			this.currentPanel = this.panels[slideID];
			this.currentPanel.setActive();
		} 
	}

	private buildTimeline = ():void => {
		if(this.timeline !== null){
			console.warn('Timeline already constructed');
		}
		this.timeline = gsap.timeline();
		this.timeline.pause(0);
		this.timeline.addLabel('panel0');
		for (let i:number = 1; i < this.panels.length; i++) {
			const panel:HTMLElement = this.panels[i].htmlElement;
			this.timeline.to(panel, {yPercent: 0, autoAlpha: 1, scale: 1});
			this.timeline.addLabel('panel' + (i));
		}
	}
  
	private preparePanels4Show = ():void => {
		for (let i:number = 1; i < this.panels.length; i++) {
			const slide:Panel = this.panels[i];
			gsap.set(slide.htmlElement, {yPercent: this.yPercentToAnimateInFrom, autoAlpha: 0, scale: this.scaleToAnimateInFrom});
		}
	}
	private unPrepareSlides4Show = ():void => {
		for (let i:number = 1; i < this.panels.length; i++) {
			const slide:Panel = this.panels[i];
			gsap.set(slide.htmlElement, {yPercent: 0, autoAlpha: 1, scale: 1});
		}
	}

	private findLastSlideVisible = ():number => {
		for (let i = this.panels.length -1; i > -1;  i--) {
			const element = this.panels[i].htmlElement;
			if(element.style.opacity === '1' || element.style.opacity.length === 0){
				let topSlideID:number = parseInt(element.id.replace('panel_', ''));
				return topSlideID;
			}
		}
		return -1;
	}

	private findCurrentSlide = (label:string, type: 'toggle' | 'complete'):void =>{
		//console.log('💡' + type + ' @: ' + label + ' id: ' + label.replace('panel', ''));
		let reportedID:number = parseInt(label.replace('panel', ''));
		let topSlideID:number = this.findLastSlideVisible();
		if(topSlideID !== reportedID){
			console.log('missmatch topSlideID: ' + topSlideID);
			reportedID = topSlideID;
		}
		if(this.currentPanel.myID !== reportedID){
			this.currentPanel.setInActive();
			this.currentPanel = this.panels[reportedID];
			this.currentPanel.setActive();
		}
	}
  
  private setupSizes = ():void => {
		this.calculateViewPort();
		for (let i = 0; i < this.panels.length; i++) {
			const slide:Panel = this.panels[i];
			slide.adjustSize(false);
		}
		gsap.set(this.container, {height: (this.panels.length * Math.floor(this.winH)).toString() + 'px', width: this.winW + 'px'});
	}

	private calculateViewPort = ():void => {
		this.winW = Math.min(window.innerWidth, document.documentElement.clientWidth);
		this.winH = Math.max(window.innerHeight, document.documentElement.clientHeight);
		this.devPixRatio = window.devicePixelRatio;
	}
  
	public panelDummyCalculationHandler(id:number):void{
		this.dummyCalculationCounter ++;
		if(this.dummyCalculationCounter === this.panels.length){
			this.resizeFinalize();
		}
	}

	private initiatePanelDummyCalculations = ():void =>{
		for (let i:number = 0; i < this.panels.length; i++) {
			const slide:Panel = this.panels[i];
			const rect:DOMRect = slide.htmlElement.getBoundingClientRect();
			slide.doDummyCalculation();
		}
	}

	public getCurrentSlide():Panel{
		return this.currentPanel;
	}

	public getCurrentSlideId():number{
		return this.currentPanel.myID;
	}
}

class Panel {
	private myManager:PanelManager;
	public myID:number;
	private isActive:boolean = false;
	public htmlElement:HTMLElement;


	constructor (myManager:PanelManager, myID:number, htmlElement:HTMLElement){
		this.myManager = myManager;
		this.myID = myID;
		this.htmlElement = htmlElement;
	}
	public setActive():void{
		this.isActive = true;
		this.htmlElement.classList.add('active');
	}
	public setInActive():void{
		this.isActive = false;
		this.htmlElement.classList.remove('active');
	}
	public adjustSize = (isResize:boolean = false):void => {
		gsap.set(this.htmlElement, {height:  Math.floor(this.myManager.winH) + 'px'});
		gsap.set(this.htmlElement, {width:  Math.floor(this.myManager.winW) + 'px'});
	}
	public doDummyCalculation(){
		this.myManager.panelDummyCalculationHandler(this.myID);
	}
}


class panelDelayedResize {

	private static _instance:panelDelayedResize = null;

	private timeoutId:number;
	private delay:number = 300; // delay in milliseconds
	private isRunning:boolean = false;

	private vhTester:HTMLElement;

	private _lastWinW:number = null;
	private _lastWinH:number = null;
	private _hasVertScrollBar:boolean = false;
	private _hasHorScrollBar:boolean = false;
	private _lastvhHeight:number = null;
	private _likelyIsMobile:boolean = false;
_
	private _widthHasChanged:boolean = false;
	private _heightHasChanged:boolean = false;
	private _verticalScrollBarHasChanged: boolean = false;
	private _horizontalScrollBarHasChanged: boolean = false;
	private _hvHasChanged:boolean = false;
	private _likelyIsMobileAddressBarHiding:boolean = false;



	constructor(isDebug:boolean = false) {
		if(panelDelayedResize._instance !== null) return;
		window.onresize = this.runner;
		panelDelayedResize._instance = this;
		this._likelyIsMobile = localStorage.mobile ||  window.navigator.maxTouchPoints > 1;
		this.vhTester = document.getElementById('vh-tester');
		if(this.vhTester === null){
			this.vhTester = document.createElement('div');
			this.vhTester.id = 'vh-tester';
			this.vhTester.style.position = 'fixed';
			this.vhTester.style.width = '1px';
			this.vhTester.style.height = '90hv';
			this.vhTester.style.pointerEvents = 'none';
			this.vhTester.style.top = '0';
			this.vhTester.style.right = '0';
			this.vhTester.style.opacity = '0';
			this.vhTester.style.zIndex = '-10';
			document.body.prepend(this.vhTester);
		}
		this.doCalculations(true);
	}

	private runner = (e:Event): void =>{
		if(this.isRunning){
			window.clearTimeout(this.timeoutId);
			this.isRunning = false;
		}
		this.isRunning = true;
		this.timeoutId = window.setTimeout(this.dispatchDelayedEvent, this.delay);
	}

	private doCalculations = (isFirstRun:boolean = false):boolean =>{
		if(isFirstRun){
			this._lastWinW = window.innerWidth;
			this._lastWinH = window.innerHeight;
			if(window.innerWidth > document.documentElement.clientWidth){
				this._hasVertScrollBar = true;
			}
			if(window.innerHeight > document.documentElement.clientHeight){
				this._hasHorScrollBar = true;
			}
			this._lastvhHeight = parseFloat(getComputedStyle(this.vhTester).getPropertyValue('height').trim());
		} else {
			let currentWidth:number = window.innerWidth;
			let currentHeight:number = window.innerHeight;
			let nowHasVertScrollBar:boolean = false;
			let nowHasHorScrollBar:boolean = false;
			if(window.innerWidth > document.documentElement.clientWidth){
				nowHasVertScrollBar = true;
			}
			if(window.innerHeight > document.documentElement.clientHeight){
				nowHasHorScrollBar = true;
			}
			let currentVHHeight = parseFloat(getComputedStyle(this.vhTester).getPropertyValue('height').trim());

			this._widthHasChanged = !(this._lastWinW === currentWidth);
			this._heightHasChanged = !(this._lastWinH === currentHeight);
			this._verticalScrollBarHasChanged = !(nowHasVertScrollBar === this._hasVertScrollBar);
			this._horizontalScrollBarHasChanged = !(nowHasHorScrollBar === this._hasHorScrollBar);
			this._hvHasChanged = !(currentVHHeight === this._lastvhHeight);

			this._lastWinW = currentWidth;
			this._lastWinH = currentHeight;
			this._hasHorScrollBar = nowHasHorScrollBar;
			this._hasVertScrollBar = nowHasVertScrollBar;
			this._lastvhHeight = currentVHHeight;

			if(!this._hvHasChanged && !this._widthHasChanged && this._heightHasChanged && this._likelyIsMobile){
				this._likelyIsMobileAddressBarHiding = true;
			} else {
				this._likelyIsMobileAddressBarHiding = false;
			}
			if(!this._heightHasChanged && !this._widthHasChanged && !this._horizontalScrollBarHasChanged && !this._verticalScrollBarHasChanged && !this._hvHasChanged){
				return false;
			}
			//this.log('likely is mobile address-bar hiding: ' + this._likelyIsMobileAddressBarHiding);
			return true;
		}
	}

	private dispatchDelayedEvent = ():void => {
		this.isRunning = false;
		if(this.doCalculations()){
			let delayedResize:CustomEvent = new CustomEvent('delayedResize', {bubbles: false, cancelable: false, detail: undefined});
			window.dispatchEvent(delayedResize);
		}
		console.info('delayed resize triggered');
	}
	public get lastWinW (){
		return this._lastWinW;
	}
	public get lastWinH (){
		return this._lastWinH;
	}
	public get hasVertScrollBar (){
		return this._hasVertScrollBar;
	}
	public get hasHorScrollBar (){
		return this._hasHorScrollBar;
	}
	public get lastvhHeight (){
		return this._lastvhHeight;
	}
	public get likelyIsMobile (){
		return this._likelyIsMobile;
	}
	public get widthHasChanged (){
		return this._widthHasChanged;
	}
	public get heightHasChanged (){
		return this._heightHasChanged;
	}
	public get verticalScrollBarHasChanged (){
		return this._verticalScrollBarHasChanged;
	}
	public get horizontalScrollBarHasChanged (){
		return this._horizontalScrollBarHasChanged;
	}
	public get hvHasChanged (){
		return this._hvHasChanged;
	}
	public get likelyIsMobileAddressBarHiding (){
		return this._likelyIsMobileAddressBarHiding;
	}
	public static get instance(){
		return panelDelayedResize._instance;
	}
}


document.addEventListener('DOMContentLoaded', () => {new panelDelayedResize()});
              
            
!
999px

Console