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="loading">
  		<main>
			<div data-scroll>
				<div class="frame">
					<div class="frame__title-wrap">
						<h1 class="frame__title">Smooth scrolling and inner image animation</h1>
					</div>
				</div>
				<div class="content">
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t1"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">Central view</h2>
							<p class="item__caption-copy">Great turbulent clouds emerged into consciousness citizens.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t2"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">Lost in time</h2>
							<p class="item__caption-copy">Brain is the seed of intelligence the sky calls to us a very small stage.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t3"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">Ready to land</h2>
							<p class="item__caption-copy">Cosmos encyclopaedia galactica a billion trillion culture cosmic ocean.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t1"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">All equal</h2>
							<p class="item__caption-copy">Network of wormholes dream of the mind's eye finite but unbounded concept.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t2"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">Connections</h2>
							<p class="item__caption-copy">Two ghostly white figures in coveralls and helmets are softly dancing vastness.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t3"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">The state of divergence</h2>
							<p class="item__caption-copy">Vastness is bearable only through love invent the universe vanquish.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t1"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">Open perspective</h2>
							<p class="item__caption-copy">The only home we've ever known concept of the number one.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t3"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">Discovery of shapes</h2>
							<p class="item__caption-copy">Decipherment explorations tesseract as a patch of light.</p>
						</div>
					</div>
					<div class="item">
						<div class="item__img-wrap"><div class="item__img item__img--t2"></div></div>
						<div class="item__caption">
							<h2 class="item__caption-title">The Observer</h2>
							<p class="item__caption-copy">Astonishment muse about dispassionate extraterrestrial observer.</p>
						</div>
					</div>
				</div>
			</div>
		</main>
</body>
              
            
!

CSS

              
                *,
*::after,
*::before {
	box-sizing: border-box;
}

:root {
	font-size: 16px;
}

body {
	margin: 0;
	--color-text: #F8B425;
	--color-bg: #07294d;
	--color-link: #F8B425;
	--color-link-hover: #F8B425;
	--color-deco: #F8B425;
	color: var(--color-text);
	background-color: var(--color-bg);
	font-family: paralucent, sans-serif;
	font-family: quiroh, sans-serif;
	font-family: mr-eaves-xl-sans, sans-serif;
}

/* Page Loader */
.js .loading::before {
	content: '';
	position: fixed;
	z-index: 100000;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: var(--color-bg);
}

.js .loading::after {
	content: '';
	position: fixed;
	z-index: 100000;
	top: 50%;
	left: 50%;
	width: 60px;
	height: 60px;
	margin: -30px 0 0 -30px;
	pointer-events: none;
	border-radius: 50%;
	opacity: 0.4;
	background: var(--color-link);
	animation: loaderAnim 0.7s linear infinite alternate forwards;
}

@keyframes loaderAnim {
	to {
		opacity: 1;
		transform: scale3d(0.5,0.5,1);
	}
}

a {
	text-decoration: none;
	color: var(--color-link);
	outline: none;
}

a:hover,
a:focus {
	color: var(--color-link-hover);
	outline: none;
}

[data-scroll] {
	will-change: transform;
}

.frame {
	padding: 2.5rem 3rem;
	position: absolute;
	z-index: 10;
}

.frame__title {
	font-size: 1rem;
	margin: 0 0 2.5rem;
}

.frame__links {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
}

.frame a {
	margin: 0.25rem 0;
	text-transform: lowercase;
}

.frame__github,
.frame__links a:not(:last-child),
.frame__demos a:not(:last-child) {
	margin-right: 1rem;
}

.frame__demos {
	margin: 1rem 0;
}

.frame__demo--current,
.frame__demo--current:hover {
	color: var(--color-text);
}

.content {
	display: flex;
	flex-direction: column;
	position: relative;
	align-items: center;
	padding: 12rem 0;
	counter-reset: figure; 
}

.item {
	margin: 10vh auto;
	max-width: 100%;
	position: relative;
	will-change: transform;
}

.item::before {
	counter-increment: figure;
	content: counter(figure, decimal-leading-zero);
	position: absolute;
	font-family: paralucent, sans-serif;
	font-size: 10rem;
	color: var(--color-deco);
	bottom: calc(100% - 3rem);
}

.item:nth-child(even)::before {
	right: 0;
}

.item__img-wrap {
	--aspect-ratio: 1/1.5;
	overflow: hidden;
	width: 500px;
	margin: 0 auto;
	padding-bottom: calc(100% / (var(--aspect-ratio)));
	max-width: calc(100% - 2rem);
	will-change: transform;
}

.item:first-child .item__img-wrap {
	--aspect-ratio: 8/10;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/1.jpg);
}

.item:nth-child(2) .item__img-wrap {
	width: 1000px;
	--aspect-ratio: 120/76;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/2.jpg);
}

.item:nth-child(3) .item__img-wrap {
	--aspect-ratio: 60/75;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/3.jpg);
}

.item:nth-child(4) .item__img-wrap {
	width: 800px;
	--aspect-ratio: 900/505;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/4.jpg);
}

.item:nth-child(5) .item__img-wrap {
	--aspect-ratio: 6/8;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/5.jpg);
}

.item:nth-child(6) .item__img-wrap {
	width: calc(100vw - 2rem);
	--aspect-ratio: 1500/844;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/6.jpg);
}

.item:nth-child(7) .item__img-wrap {
	width: 900px;
	--aspect-ratio: 1000/749;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/7.jpg);
}

.item:nth-child(8) .item__img-wrap {
	width: 900px;
	--aspect-ratio: 1000/562;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/8.jpg);
}

.item:nth-child(9) .item__img-wrap {
	--aspect-ratio: 60/75;
	--image: url(https://returntrue.ir/codepen_assets/images/smooth-scrolling-with-inner-image-animations/9.jpg);
}

.item__img {
	--overflow: 40px;
	height: calc(100% + (2 * var(--overflow)));
	top: calc( -1 * var(--overflow));
	width: 100%;
	position: absolute;
	background-image: var(--image);
	background-size: cover;
	background-position: 50% 0%;
	will-change: transform;
}

.item__img--t1 {
	--overflow: 60px;
}

.item__img--t2 {
	--overflow: 80px;
}

.item__img--t3 {
	--overflow: 120px;
}

.item__caption {
	padding: 2rem 1rem;
}

.item__caption-title {
	font-family: paralucent, sans-serif;
	font-weight: 400;
	font-size: 3rem;
	margin: 0;
}

.item__caption-copy {
	margin: 0;
}

.item__caption-copy::before {
	content: '__';
	line-height: 1;
	color: var(--color-link);
	font-weight: 700;
	font-size: 3rem;
	margin: 0 0 1rem;
	display: block;
}

              
            
!

JS

              
                
{
    // helper functions
    const MathUtils = {
        // map number x from range [a, b] to [c, d]
        map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
        // linear interpolation
        lerp: (a, b, n) => (1 - n) * a + n * b
    };

    // body element
    const body = document.body;
    
    // calculate the viewport size
    let winsize;
    const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight};
    calcWinsize();
    // and recalculate on resize
    window.addEventListener('resize', calcWinsize);

    // scroll position and update function
    let docScroll;
    const getPageYScroll = () => docScroll = window.pageYOffset || document.documentElement.scrollTop;
    window.addEventListener('scroll', getPageYScroll);

    // Item
    class Item {
        constructor(el) {
            // the .item element
            this.DOM = {el: el};
            // the inner image
            this.DOM.image = this.DOM.el.querySelector('.item__img');
            this.renderedStyles = {
                // here we define which property will change as we scroll the page and the items is inside the viewport
                // in this case we will be translating the image on the y-axis
                // we interpolate between the previous and current value to achieve a smooth effect
                innerTranslationY: {
                    // interpolated value
                    previous: 0, 
                    // current value
                    current: 0, 
                    // amount to interpolate
                    ease: 0.1,
                    // the maximum value to translate the image is set in a CSS variable (--overflow)
                    maxValue: parseInt(getComputedStyle(this.DOM.image).getPropertyValue('--overflow'), 10),
                    // current value setter
                    // the value of the translation will be:
                    // when the item's top value (relative to the viewport) equals the window's height (items just came into the viewport) the translation = minimum value (- maximum value)
                    // when the item's top value (relative to the viewport) equals "-item's height" (item just exited the viewport) the translation = maximum value
                    setValue: () => {
                        const maxValue = this.renderedStyles.innerTranslationY.maxValue;
                        const minValue = -1 * maxValue;
                        return Math.max(Math.min(MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, minValue, maxValue), maxValue), minValue)
                    }
                }
            };
            // set the initial values
            this.update();
            // use the IntersectionObserver API to check when the element is inside the viewport
            // only then the element translation will be updated
            this.observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => this.isVisible = entry.intersectionRatio > 0);
            });
            this.observer.observe(this.DOM.el);
            // init/bind events
            this.initEvents();
        }
        update() {
            // gets the item's height and top (relative to the document)
            this.getSize();
            // sets the initial value (no interpolation)
            for (const key in this.renderedStyles ) {
                this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
            }
            // translate the image
            this.layout();
        }
        getSize() {
            const rect = this.DOM.el.getBoundingClientRect();
            this.props = {
                // item's height
                height: rect.height,
                // offset top relative to the document
                top: docScroll + rect.top 
            }
        }
        initEvents() {
            window.addEventListener('resize', () => this.resize());
        }
        resize() {
            // on resize rest sizes and update the translation value
            this.update();
        }
        render() {
            // update the current and interpolated values
            for (const key in this.renderedStyles ) {
                this.renderedStyles[key].current = this.renderedStyles[key].setValue();
                this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
            }
            // and translates the image
            this.layout();
        }
        layout() {
            // translates the image
            this.DOM.image.style.transform = `translate3d(0,${this.renderedStyles.innerTranslationY.previous}px,0)`;
        }
    }

    // SmoothScroll
    class SmoothScroll {
        constructor() {
            // the <main> element
            this.DOM = {main: document.querySelector('main')};
            // the scrollable element
            // we translate this element when scrolling (y-axis)
            this.DOM.scrollable = this.DOM.main.querySelector('div[data-scroll]');
            // the items on the page
            this.items = [];
            [...this.DOM.main.querySelectorAll('.content > .item')].forEach(item => this.items.push(new Item(item)));
            // here we define which property will change as we scroll the page
            // in this case we will be translating on the y-axis
            // we interpolate between the previous and current value to achieve the smooth scrolling effect
            this.renderedStyles = {
                translationY: {
                    // interpolated value
                    previous: 0, 
                    // current value
                    current: 0, 
                    // amount to interpolate
                    ease: 0.1,
                    // current value setter
                    // in this case the value of the translation will be the same like the document scroll
                    setValue: () => docScroll
                }
            };
            // set the body's height
            this.setSize();
            // set the initial values
            this.update();
            // the <main> element's style needs to be modified
            this.style();
            // init/bind events
            this.initEvents();
            // start the render loop
            requestAnimationFrame(() => this.render());
        }
        update() {
            // sets the initial value (no interpolation) - translate the scroll value
            for (const key in this.renderedStyles ) {
                this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
            }   
            // translate the scrollable element
            this.layout();
        }
        layout() {
            // translates the scrollable element
            this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0)`;
        }
        setSize() {
            // set the heigh of the body in order to keep the scrollbar on the page
            body.style.height = `${this.DOM.scrollable.scrollHeight}px`;
        }
        style() {
            // the <main> needs to "stick" to the screen and not scroll
            // for that we set it to position fixed and overflow hidden 
            this.DOM.main.style.position = 'fixed';
            this.DOM.main.style.width = this.DOM.main.style.height = '100%';
            this.DOM.main.style.top = this.DOM.main.style.left = 0;
            this.DOM.main.style.overflow = 'hidden';
        }
        initEvents() {
            // on resize reset the body's height
            window.addEventListener('resize', () => this.setSize());
        }
        render() {
            // update the current and interpolated values
            for (const key in this.renderedStyles ) {
                this.renderedStyles[key].current = this.renderedStyles[key].setValue();
                this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
            }
            // and translate the scrollable element
            this.layout();
            
            // for every item
            for (const item of this.items) {
                // if the item is inside the viewport call it's render function
                // this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
                if ( item.isVisible ) {
                    item.render();
                }
            }
            
            // loop..
            requestAnimationFrame(() => this.render());
        }
    }

    /***********************************/
    /********** Preload stuff **********/

    // Preload images
    const preloadImages = () => {
        return new Promise((resolve, reject) => {
            imagesLoaded(document.querySelectorAll('.item__img'), {background: true}, resolve);
        });
    };
    
    // And then..
    preloadImages().then(() => {
        // Remove the loader
        document.body.classList.remove('loading');
        // Get the scroll position
        getPageYScroll();
        // Initialize the Smooth Scrolling
        new SmoothScroll();
    });
}
              
            
!
999px

Console