HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
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.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
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.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
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.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<!-- The `.container` element will contain all the images -->
<!-- It will be used also to perform the 'custom scroll' behavior -->
<div class="container">
<!-- Each following `div` correspond to one image -->
<!-- The images will be set using CSS backgrounds -->
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
<div class="image vh-fix"></div>
</div>
body {
// `overflow-x` should be hidden, so horizontal scrollbar
// don't appears when performing transformations
overflow-x: hidden;
// Setup the background
height: 100vh;
background-color: #9f9eac;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 1600 800'%3E%3Cg %3E%3Cpath fill='%23aeadbc' d='M486 705.8c-109.3-21.8-223.4-32.2-335.3-19.4C99.5 692.1 49 703 0 719.8V800h843.8c-115.9-33.2-230.8-68.1-347.6-92.2C492.8 707.1 489.4 706.5 486 705.8z'/%3E%3Cpath fill='%23bdbccc' d='M1600 0H0v719.8c49-16.8 99.5-27.8 150.7-33.5c111.9-12.7 226-2.4 335.3 19.4c3.4 0.7 6.8 1.4 10.2 2c116.8 24 231.7 59 347.6 92.2H1600V0z'/%3E%3Cpath fill='%23cdccdd' d='M478.4 581c3.2 0.8 6.4 1.7 9.5 2.5c196.2 52.5 388.7 133.5 593.5 176.6c174.2 36.6 349.5 29.2 518.6-10.2V0H0v574.9c52.3-17.6 106.5-27.7 161.1-30.9C268.4 537.4 375.7 554.2 478.4 581z'/%3E%3Cpath fill='%23dcdbee' d='M0 0v429.4c55.6-18.4 113.5-27.3 171.4-27.7c102.8-0.8 203.2 22.7 299.3 54.5c3 1 5.9 2 8.9 3c183.6 62 365.7 146.1 562.4 192.1c186.7 43.7 376.3 34.4 557.9-12.6V0H0z'/%3E%3Cpath fill='%23ecebff' d='M181.8 259.4c98.2 6 191.9 35.2 281.3 72.1c2.8 1.1 5.5 2.3 8.3 3.4c171 71.6 342.7 158.5 531.3 207.7c198.8 51.8 403.4 40.8 597.3-14.8V0H0v283.2C59 263.6 120.6 255.7 181.8 259.4z'/%3E%3Cpath fill='%23dcdbee' d='M1600 0H0v136.3c62.3-20.9 127.7-27.5 192.2-19.2c93.6 12.1 180.5 47.7 263.3 89.6c2.6 1.3 5.1 2.6 7.7 3.9c158.4 81.1 319.7 170.9 500.3 223.2c210.5 61 430.8 49 636.6-16.6V0z'/%3E%3Cpath fill='%23cdccdd' d='M454.9 86.3C600.7 177 751.6 269.3 924.1 325c208.6 67.4 431.3 60.8 637.9-5.3c12.8-4.1 25.4-8.4 38.1-12.9V0H288.1c56 21.3 108.7 50.6 159.7 82C450.2 83.4 452.5 84.9 454.9 86.3z'/%3E%3Cpath fill='%23bdbccc' d='M1600 0H498c118.1 85.8 243.5 164.5 386.8 216.2c191.8 69.2 400 74.7 595 21.1c40.8-11.2 81.1-25.2 120.3-41.7V0z'/%3E%3Cpath fill='%23aeadbc' d='M1397.5 154.8c47.2-10.6 93.6-25.3 138.6-43.8c21.7-8.9 43-18.8 63.9-29.5V0H643.4c62.9 41.7 129.7 78.2 202.1 107.4C1020.4 178.1 1214.2 196.1 1397.5 154.8z'/%3E%3Cpath fill='%239f9eac' d='M1315.3 72.4c75.3-12.6 148.9-37.1 216.8-72.4h-723C966.8 71 1144.7 101 1315.3 72.4z'/%3E%3C/g%3E%3C/svg%3E");
/* background by SVGBackgrounds.com */
background-attachment: fixed;
background-position: center;
background-size: cover;
}
// The styles for a `div` element (inserted with Javascript)
// Used to make the page scrollable
// Will be setted a proper `height` value using Javascript
.fake-scroll {
position: absolute;
top: 0;
width: 1px;
}
// The container for all images
.container {
// 2 columns grid
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0 10%;
justify-items: end; // This will align all items (images) to the right
// Fixed positioned, so it won't be affected by default scroll
// It will be moved using `transform`, to achieve a custom scroll behavior
position: fixed;
top: 0;
left: 0;
width: 100%;
}
// Styles for image elements
// Mainly positioning and background styles
.image {
position: relative;
width: 300px;
height: 100vh;
background-repeat: no-repeat;
background-position: center;
// This will align all even images to the left
// For getting centered positioned images, respect the viewport
&:nth-child(2n) {
justify-self: start;
}
// Set each `background-image` using a SCSS `for` loop
@for $i from 1 through 10 {
&:nth-child(#{$i}) {
background-image: url('https://cdn.jsdelivr.net/gh/lmgonzalves/scroll-based-animation/img/image#{$i}.jpg');
}
}
}
// Adjusting layout for small screens
@media screen and (max-width: 760px) {
.container {
// 1 column grid
grid-template-columns: 1fr;
// Fix image centering
justify-items: center;
}
// Fix image centering
.image:nth-child(2n) {
justify-self: center;
}
}
/**
* Regex tested and matched against the following userAgents:
* iPhone
* Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X)
* AppleWebKit/602.1.50 (KHTML, like Gecko)
* CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1
* iPad
* Mozilla/5.0 (iPad; CPU OS 9_0 like Mac OS X)
* AppleWebKit/600.1.4 (KHTML, like Gecko)
* CriOS/45.0.2454.89 Mobile/13A344 Safari/600.1.4 (000205)
*/
const iOSChromeDetected = /CriOS/.test(navigator.userAgent);
if (iOSChromeDetected) {
const getHeight = function getComputedHeightFrom(element) {
const computedHeightString = getComputedStyle(element).height;
const elementHeight = Number(computedHeightString.replace('px', ''));
return elementHeight;
};
const calculateVh = function calculateVhFrom(elementHeight) {
const approximateVh = (elementHeight / initialViewportHeight) * 100;
const elementVh = Math.round(approximateVh);
return elementVh;
};
const setDataAttribute = function setDataAttributeUsing(elementVh, element) {
const dataAttributeValue = `${elementVh}`;
element.setAttribute('data-vh', dataAttributeValue);
};
const setHeight = function setHeightBasedOnVh(element) {
const landscape = orientation;
const vhRatio = Number(element.dataset.vh / 100);
if (landscape) {
element.style.height = `${vhRatio * landscapeHeight}px`;
} else {
element.style.height = `${vhRatio * portraitHeight}px`;
}
};
const initialize = function initializeDataAttributeAndHeight(element) {
const elementHeight = getHeight(element);
const elementVh = calculateVh(elementHeight);
setDataAttribute(elementVh, element);
setHeight(element);
};
const initialViewportHeight = window.innerHeight;
const elements = Array.from(document.getElementsByClassName('vh-fix'));
const statusBarHeight = 20;
const portraitHeight = screen.height - statusBarHeight;
const landscapeHeight = screen.width - statusBarHeight;
window.onload = function() {
window.addEventListener('orientationchange', function() {
elements.forEach(setHeight);
});
elements.forEach(initialize);
};
}
// DEMO
(function() {
// Easing function used for `translateX` animation
// From: https://gist.github.com/gre/1650294
function easeOutQuad (t) {
return t * (2 - t)
}
// Returns a random number (integer) between `min` and `max`
function random (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// Returns a random number as well, but it could be negative also
function randomPositiveOrNegative (min, max) {
return random(min, max) * (Math.random() > 0.5 ? 1 : -1)
}
// Set CSS `tranform` property for an element
function setTransform (el, transform) {
el.style.transform = transform
el.style.WebkitTransform = transform
}
// Current scroll position
var current = 0
// Target scroll position
var target = 0
// Ease or speed for moving from `current` to `target`
var ease = 0.075
// Utility variables for `requestAnimationFrame`
var rafId = undefined
var rafActive = false
// Container element
var container = document.querySelector('.container')
// Array with `.image` elements
var images = Array.prototype.slice.call(document.querySelectorAll('.image'))
// Variables for storing dimmensions
var windowWidth, containerHeight, imageHeight
// Variables for specifying transform parameters and limits
var rotateXMaxList = []
var rotateYMaxList = []
var translateXMax = -200
// Popullating the `rotateXMaxList` and `rotateYMaxList` with random values
images.forEach(function () {
rotateXMaxList.push(randomPositiveOrNegative(20, 40))
rotateYMaxList.push(randomPositiveOrNegative(20, 60))
})
// The `fakeScroll` is an element to make the page scrollable
// Here we are creating it and appending it to the `body`
var fakeScroll = document.createElement('div')
fakeScroll.className = 'fake-scroll'
document.body.appendChild(fakeScroll)
// In the `setupAnimation` function (below) we will set the `height` properly
// Geeting dimmensions and setting up all for animation
function setupAnimation () {
// Updating dimmensions
windowWidth = window.innerWidth
containerHeight = container.getBoundingClientRect().height
imageHeight = containerHeight / (windowWidth > 760 ? images.length / 2 : images.length)
// Set `height` for the fake scroll element
fakeScroll.style.height = containerHeight + 'px'
// Start the animation, if it is not running already
startAnimation()
}
// Update scroll `target`, and start the animation if it is not running already
function updateScroll () {
target = window.scrollY || window.pageYOffset
startAnimation()
}
// Start the animation, if it is not running already
function startAnimation () {
if (!rafActive) {
rafActive = true
rafId = requestAnimationFrame(updateAnimation)
}
}
// Do calculations and apply CSS `transform`s accordingly
function updateAnimation () {
// Difference between `target` and `current` scroll position
var diff = target - current
// `delta` is the value for adding to the `current` scroll position
// If `diff < 0.1`, make `delta = 0`, so the animation would not be endless
var delta = Math.abs(diff) < 0.1 ? 0 : diff * ease
if (delta) { // If `delta !== 0`
// Update `current` scroll position
current += delta
// Round value for better performance
current = parseFloat(current.toFixed(2))
// Call `update` again, using `requestAnimationFrame`
rafId = requestAnimationFrame(updateAnimation)
} else { // If `delta === 0`
// Update `current`, and finish the animation loop
current = target
rafActive = false
cancelAnimationFrame(rafId)
}
// Update images
updateAnimationImages()
// Set the CSS `transform` corresponding to the custom scroll effect
setTransform(container, 'translateY('+ -current +'px)')
}
// Calculate the CSS `transform` values for each `image`, given the `current` scroll position
function updateAnimationImages () {
// This value is the `ratio` between `current` scroll position and image's `height`
var ratio = current / imageHeight
// Some variables for using in the loop
var intersectionRatioIndex, intersectionRatioValue, intersectionRatio
var rotateX, rotateXMax, rotateY, rotateYMax, translateX
// For each `image` element, make calculations and set CSS `transform` accordingly
images.forEach(function (image, index) {
// Calculating the `intersectionRatio`, similar to the value provided by
// the IntersectionObserver API
intersectionRatioIndex = windowWidth > 760 ? parseInt(index / 2) : index
intersectionRatioValue = ratio - intersectionRatioIndex
intersectionRatio = Math.max(0, 1 - Math.abs(intersectionRatioValue))
// Calculate the `rotateX` value for the current `image`
rotateXMax = rotateXMaxList[index]
rotateX = rotateXMax - (rotateXMax * intersectionRatio)
rotateX = rotateX.toFixed(2)
// Calculate the `rotateY` value for the current `image`
rotateYMax = rotateYMaxList[index]
rotateY = rotateYMax - (rotateYMax * intersectionRatio)
rotateY = rotateY.toFixed(2)
// Calculate the `translateX` value for the current `image`
if (windowWidth > 760) {
translateX = translateXMax - (translateXMax * easeOutQuad(intersectionRatio))
translateX = translateX.toFixed(2)
} else {
translateX = 0
}
// Invert `rotateX` and `rotateY` values in case the image is below the center of the viewport
// Also update `translateX` value, to achieve an alternating effect
if (intersectionRatioValue < 0) {
rotateX = -rotateX
rotateY = -rotateY
translateX = index % 2 ? -translateX : 0
} else {
translateX = index % 2 ? 0 : translateX
}
// Set the CSS `transform`, using calculated values
setTransform(image, 'perspective(500px) translateX('+ translateX +'px) rotateX('+ rotateX +'deg) rotateY('+ rotateY +'deg)')
})
}
// Listen for `resize` event to recalculate dimmensions
window.addEventListener('resize', setupAnimation)
// Listen for `scroll` event to update `target` scroll position
window.addEventListener('scroll', updateScroll)
// Initial setup
setupAnimation()
})()
Also see: Tab Triggers