drupal
Four Kitchens: Creating depth and motion: A step-by-step guide to parallax
Frontend Engineer
Mari is a high-achieving Drupal frontend developer who has shown great proficiency in tackling complex frontend problems.
January 1, 1970
Crafting a visually engaging website isn’t just about eye-catching colors and typography — today it’s also about creating immersive experiences that captivate users as they scroll. One of the most compelling ways to achieve this is by using a parallax effect, where elements move at different speeds to create a sense of depth and motion.
With a thoughtful approach and some JavaScript, you can seamlessly add this effect to your site, enhancing storytelling and making your pages more dynamic.
This post will guide you through the process of integrating a custom parallax effect into your site. Whether building a feature-rich landing page or enhancing storytelling elements, this technique can bring your site to life. Let’s begin.
Building our component
Our example site was built in Drupal, but the overall concept would be the same in any CMS. First, we will need to build a component that has all the necessary fields that we want to display in our parallax. In this example, we will use Paragraph types and have two kinds of slides: one with an image and another without an image.
Parallax image slide
This slide will let us add an image, a title, and the caption or information we want to tell about that specific slide, the alignment of the information (left, center, or right), and an option if we want to hide the credit of the image or show it.

Parallax blank slide
This slide is similar to the previous one, but there are key differences. This one won’t include an image and anything else related to the image, and we allow a lot more text formatting on blank slides. This means that we can have the text on a blank slide take up much more of the available space without worrying about color contrast issues with advanced text formatting.
Once both paragraphs have been created, let’s create a ‘Parallax Slideshow’ paragraph that will only have a field that references the previous paragraphs created.
Connecting our component to the custom theme
Once our component is ready, the next step is to integrate it into our custom theme. In this example, we’re using Emulsify, our design system, as our custom theme, to ensure a consistent and modular approach to theming.
First, we will have our paragraph--parallax-slideshow.html.twig
that will include a parallax-slideshow.twig
, which has a JavaScript library called parallax-slideshow that is in charge of all logic to make our parallax effect work, and also some required styles.
Here’s what our parallax-slideshow.twig looks like. Notice the empty <div class=””parallax-slideshow__image-wrapper””></div>. This is where the slide images will be rendered and where the fade-in and fade-out effects between images will occur.
{% set classes = [ paragraph.bundle|clean_class, "parallax-slideshow", ] %} <div{{ attributes.addClass(classes) }} data-id="{{ slideshow_id }}"> <div class="parallax-slideshow__wrapper"> <div class="parallax-slideshow__image-wrapper"></div> {{ slides }} </div> </div>Then, we will have a paragraph--parallax-image-slide.html.twig
and a paragraph--parallax-blank-slide.html.twig
. Both files include a parallax-slide.twig
, which is a molecule in the design system that organizes the content of each slide and adds all the needed styles. They are almost identical — the only difference being that the blank-slide will not pass the slide image to the parallax-slide.twig
file.
Here’s what our parallax-slide.twig
looks like:
Preloading parallax slideshow data
To prevent a visible delay between slides, the component needs to preload the first two images on page load. As the user begins scrolling, additional images are loaded dynamically in the background. This ensures a seamless transition between slides without noticeable lag and enhances the overall user experience.
We need to pass structured data from the backend to JavaScript. Below is a function that loads the data and attaches it to drupalSettings
for use in a theme.
Once the data is attached to drupalSettings
in our JavaScript file, we can access parallaxSlideshowData
to dynamically load images and control the parallax effect.
JavaScript implementation of the parallax slideshow
Below is a breakdown of how the JavaScript file works to bring the parallax slideshow to life.
Drupal.behaviors.parallaxSlideshow = { attach: function (context) { const parallaxSlideshowData = drupalSettings.yourTheme.parallaxSlideshowData; if (!parallaxSlideshowData) return; const slideshows = once('parallax-slideshow', '.parallax-slideshow', context); slideshows.forEach((slideshow) => { const loadedSlideIds = new Set(); const loadedImages = new Set(); initializeParallaxSlideshow(slideshow, parallaxSlideshowData, loadedSlideIds, loadedImages); }); }, };Let’s start by retrieving the slideshow data from drupalSettings
and ensuring the script only runs once per slideshow element. The function initializeParallaxSlideshow
is responsible for setting up and managing the parallax slideshow experience by initializing each slideshow. By tracking which slides have been loaded, we prevent redundant loading:
Then, it calls a preloadSlides
function, which likely preloads images or other resources for the first two slides to prevent a visible delay between slides.
Next, it calls a createImageDiv
helper function that is responsible for creating and managing an image element within the parallax slideshow.
The reason why we check if there is a firstImage
is that we want the initial slide to fade in from black when it’s fully loaded. Once the image loads, it finds the overlay and the slideshow wrapper, fades out the overlay, removes the overlay, and re-enables scrolling.
Let’s go back to the initializeParallaxSlideshow
. After the preloadSlides
function there’s a scroll
event listener for the parallax effect that listens for scroll events to update the slideshow’s image position dynamically.
The idea is to let the image wrapper take the whole height of the viewport, but since there can be components before or after the parallax slideshow, at some point it is necessary to change the position of the image wrapper, to let the user scroll and interact with other components.
window.addEventListener('scroll', () => { const windowHeight = window.innerHeight; const top = slideshow.getBoundingClientRect().top; const bottom = slideshow.getBoundingClientRect().bottom; const slideshowImageWrapper = slideshow.querySelector( '.parallax-slideshow__image-wrapper', ); if (top < 0 && bottom > windowHeight) { slideshowImageWrapper.style.position = 'fixed'; slideshowImageWrapper.style.top = 0; } else { slideshowImageWrapper.style.position = 'absolute'; if (windowHeight > bottom) { slideshowImageWrapper.style.top = 'unset'; slideshowImageWrapper.style.bottom = 0; } if (windowHeight < top) { slideshowImageWrapper.style.top = 0; slideshowImageWrapper.style.bottom = 'unset'; } } });The following logic is to set a scroll hijacking if the parallax slideshow is the first component of the page and if the first slide is an image.
// Check if slideshow is within a parent of .content-top const isContentTopParent = slideshow.closest('.content-top') !== null; // Get the first slide and check if it has the class `parallax-slide--parallax-image-slide` const firstSlide = slideshow.querySelector('.parallax-slide'); const isFirstSlideParallaxImageSlide = firstSlide && firstSlide.classList.contains('parallax-slide--parallax-image-slide'); // Lock scroll if .content-top is present and the first slide is of type image if (isContentTopParent && isFirstSlideParallaxImageSlide) { const overlay = document.createElement('div'); overlay.className = 'parallax-slideshow__overlay'; slideshow .querySelector('.parallax-slideshow__wrapper') .appendChild(overlay); document.body.style.overflow = 'hidden'; }Then, there’s a piece of code that iterates through all slides in the slideshow and calls the initializeSlideObserver()
function on each slide.
Now let’s take a look at the initializeSlideObserver()
function — the one that is responsible for setting up an Intersection Observer to track when a slide enters the viewport and dynamically updates the slideshow’s displayed image accordingly. It ensures that the slideshow loads the next image only when needed, preventing unnecessary rendering and improving performance.
Last but not least, there’s the loadNextSlide
function that is responsible for preloading the next slide’s image to ensure a smooth transition when the user scrolls. This prevents unnecessary reloading of already loaded images. This function is very similar to the preloadSlides
function.
With these functions in place — handling image creation, slide observation, and preloading — you now have a dynamic and efficient parallax slideshow that seamlessly transitions between images as users scroll. By leveraging the Intersection Observer API, preloading logic, and smooth fade effects, the slideshow ensures a visually engaging experience without unnecessary performance overhead.
Once you’ve added the necessary styles to control positioning, animations, and transitions, your parallax slideshow should be fully functional across your site. This approach not only enhances the storytelling aspect of your content, but also keeps interactions smooth and lightweight.
Now, all that’s left is to fine-tune the visuals to match your design, and you’re set to create an immersive scrolling experience!
The post Creating depth and motion: A step-by-step guide to parallax appeared first on Four Kitchens.
jofitz: Drupal AI: first steps
Artificial Intelligence has arrived in the Drupal ecosystem and it is already making huge waves. This is the first in a series of articles about my experiences as I dip a toe into these exciting waters.
The task
I was working on a large website with plenty of content dating back years, including numerous product reviews that required improved categorisation. The goal was to tag all of these review nodes with the relevant Make and Model.
The approach
I chose to write a custom Drush script to loop through all of the review nodes, using Artificial Intelligence to parse the Title and Body fields and return the make and model discussed therein.
public function reviewsMakeModel(): void { $fields = ['title', 'body']; foreach ($review_nodes as $review_node) { $data = []; foreach ($fields as $field) { $data[] = $review_node->get($field)->value; } $text = implode("\n", $data); $makeAndModel = $this->getMakeAndModel($text); // More to follow... } }The provider
I...
Read moreThe Drop Times: From Drupal Core to Static Site Innovation: Samuel Mortenson on Tome, SFC, and Open Source Legacy
DDEV Blog: Using DDEV to spin up a legacy PHP application
This guest post is by DDEV community member and TYPO3 contributor Garvin Hicking.
In my daily work, I develop TYPO3-based projects and also contribute to the TYPO3 CMS OpenSource project itself.
Usually this means working with actively supported and up-to-date PHP versions as well as database systems like MySQL/PostgreSQL/MariaDB.
Just recently I had to migrate a very outdated project: TYPO3 4.5, which utilized MySQL 5.5 and PHP 5.3. When that project was initially developed, it was done with XAMPP and later Vagrant-based VMs. This has been long superseded with using Docker and specifically DDEV for ease-of-use.
So naturally I wanted to be able to use DDEV for the legacy project to get it working just as it is running on the (outdated) hosting provider's shared web servers.
I quickly faced three major issues:
- No PHP 5.3 out-of-the-box support from DDEV; it starts with 5.6 as of the time of this writing
- No MySQL 5.5 ARM64 support either; it starts with 5.7
- Additionally, I use an Apple MacBook Pro M1 with ARM-chipset, which has no "official" MySQL 5.5 support
Thanks to the outstanding DDEV support on Discord, I was quickly able to find a way with minimal effort, just by creating very small custom, additional docker-compose YAML files.
One advantage (of many) of using DDEV instead the underlying Docker Compose is that so many things are pre-configured and "just work". So I really did not want to migrate everything to Docker Compose on my own, do my custom routing, PHP-FPM integration and whatnot.
Just being able to "bait and switch" the PHP and DB container with a different base Docker image was all that was needed for me:
Step 1: Base config
I created the base ~/legacyphp/.ddev/config.yaml
file manually inside my ~/legacyphp
project directory, setting legacyphp
as the project name.
Note that I configured PHP and MySQL versions that are supported by DDEV for this first:
name: legacyphp
type: php
docroot: htdocs
php_version: "8.3"
webserver_type: apache-fpm
database:
type: mysql
version: "8.0"
Step 2: Rewire DB
Next I created the very small file ~/legacyphp/.ddev/docker-compose.db.yaml
in the same directory next to config.yaml
:
services:
db:
platform: linux/amd64
build:
args:
BASE_IMAGE: ddev/ddev-dbserver-mysql-5.5:v1.24.6
entrypoint:
- sh
- -c
- |
cp /docker-entrypoint.sh ~/docker-entrypoint.sh
sed -i '157s|.*|if false; then|' ~/docker-entrypoint.sh
sed -i '175s|.*|echo mysql_8.0 >/var/lib/mysql/db_mariadb_version.txt|' ~/docker-entrypoint.sh
exec ~/docker-entrypoint.sh
Three things are noteworthy:
- Setting
linux/amd64
as the platform will require Rosetta to be available on the macOS ARM64 platform - The
BASE_IMAGE
is set to a DDEVdb
container of legacy Docker images that are still provided. - Changing the
entrypoint
is a workaround to prevent DDEV complaining about a mismatching MySQL version after restarting the project. The small script "tricks" the DDEV inspection into believing, the version matches the one configured in.ddev/config.yaml
.
Step 3: Rewire PHP
Using a different PHP version is just a few lines more work, because we are not replacing the whole web
container of DDEV. Instead, we add an additional PHP container which is executed from the web container via port 9000.
This is done via the file ~/legacyphp/.ddev/docker-compose.php.yaml
:
services:
php:
container_name: ddev-${DDEV_SITENAME}-php
image: devilbox/php-fpm:5.3-work
restart: "no"
expose:
- 9000
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: ${DDEV_APPROOT}
working_dir: /var/www/html
volumes:
- "../:/var/www/html"
- ".:/mnt/ddev_config:ro"
- ddev-global-cache:/mnt/ddev-global-cache
- "./php:/etc/php-custom.d"
environment:
- NEW_UID=${DDEV_UID}
- NEW_GID=${DDEV_GID}
- DDEV_PHP_VERSION
- IS_DDEV_PROJECT=true
web:
depends_on:
- php
Note here that we use devilbox/php-fpm
with our needed version, and a bind-mount takes care the PHP container can access our main project root directory.
A special mount of ~/legacyphp/.ddev/php/
is included so that we can control the php.ini
configuration, if needed. For example you could disable the OPCache+APC in case you're doing some legacy benchmarking that should not be falsified via caching, I created a very small file ~/legacyphp/.ddev/php/php.ini
file with the contents:
# This is an example.
# apc.enabled=Off
# opcache.enable=Off
Step 4: Utilize the PHP container with an Apache proxy
To execute PHP with our external PHP Docker image, I created the following file in ~/legacyphp/.ddev/apache/apache-site.conf
:
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} =https
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d
RewriteRule ^(.+[^/])$ https://%{HTTP_HOST}$1/ [redirect,last]
SetEnvIf X-Forwarded-Proto "https" HTTPS=on
Alias "/phpstatus" "/var/www/phpstatus.php"
DocumentRoot /var/www/html/htdocs
<Directory "/var/www/html/htdocs">
AllowOverride All
Allow from All
</Directory>
CustomLog /var/log/apache2/access.log combined
ProxyFCGIBackendType GENERIC
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://php:9000/var/www/html/htdocs/$1
DirectoryIndex /index.php index.php
</VirtualHost>
Note that if your document root is not htdocs
you would need to adapt this name to your liking (like public
or wwwroot
or anything) in all occurrences of this file.
Step 5: Lift-Off
Now you can execute ddev start
and then ddev launch
to see your project up and running.
You could create a simple ~/legacyphp/htdocs/index.php
file with <?php phpinfo(); ?>
to verify the version.
Using ddev mysql
will connect you to the MySQL 5.5 instance:
~/legacyphp> ddev mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.5.62-log MySQL Community Server (GPL)
Caveats
You can enter the PHP Docker container with a command like docker exec -it -u devilbox ddev-legacyphp-php bash
if you need/want to execute PHP commands on shell-level, because the regular web
container will run with the more recent PHP 8.3 version.
So if you need to perform composer CLI calls, be sure to do this within the matching PHP container.
Another thing to pay attention to is that if you for example want to utilize Mailpit with TYPO3's mail configuration, you can not use localhost:1025
as an SMTP server. localhost
in PHP's case will be that devilbox PHP container, and not the DDEV web container. Instead you need to setup web:1025
as the hostname.
The devilbox PHP config has pretty much all available PHP extensions set up to use, but if you need specific imagemagick or other tools, you will have to either ensure these are executed on the web
container, or make them available with customization of a different base Docker container that you can build yourself.
If you want to use Xdebug with this setup, you'll need to do more internal port forwarding in the docker-compose setup, which is beyond the scope of this article.
Closing words
Having shown you what is possible, I hope you will never need to use it, and you will always use well-supported and current software. :-)
Thanks so much to the DDEV project for getting me across the finish line with just very little effort!
mark.ie: My LocalGov Drupal contributions for May 2025
Great month, lots done, including a new module for Drupal.org - modules_list.
Gábor Hojtsy: I built a thing for api.drupal.org and you can too!
Did you search for Drupal API documentation in the past and ended up on outdated information on api.drupal.org? I heard this story from many people before and it also happens to AI bots. This is a problem the Drupal Association wanted to fix for a while but did not get around to it with all the priorities.
Acquia held an internal hackathon called 48Create on 15-16 May, 2025. I joined the team formed by Ben Mullins around Drupal documentation and I decided to take on this problem.
Gábor Hojtsy Thu, 05/29/2025 - 19:42The Drop Times: Dropmin: What Is It and Why Does It Exist?
amazee.io: Drupaljam 2025: Big Ideas, Bold Tech, and the Power of Community
Talking Drupal: TD Cafe #003 - Mike Anello & Mile Herchel
In this episode, Mike Anello and Mike Herchel dive into a casual conversation covering a wide array of topics. They start by discussing the concept of a podcast with almost no effort required and the mystery of Stephen's involvement. The conversation then quickly shifts to Florida Drupal Camp, mentioning its impressive 16 uninterrupted years, the increase in attendees, and how fun it is. They touch upon single directory components in Drupal, their importance, and intricacies like CSS styling, schemas, and Experience Builder. The discussion also includes insights into popular Drupal events like Florida Drupal Camp, Drupal Dev Days, and the upcoming DrupalCon. They infuse humor and personal anecdotes while engaging in thoughtful technical exchanges and playful banter.
For show notes visit: https://www.talkingDrupal.com/cafe003
Topics Michael AnelloMike, widely recognized by his Drupal.org username "ultimike," is a prominent figure in the Drupal community with over 15 years of experience as a developer, educator, and community leader. As the co-founder and vice president of DrupalEasy, a Florida-based training and consulting firm, he has been instrumental in shaping the careers of countless Drupal professionals through comprehensive programs like Drupal Career Online and Professional Module Development .(drupalcampnj.org, nedcamp.org) Anello's contributions extend beyond education. He has been deeply involved in the Drupal ecosystem, serving as a core contributor to the Migrate module, co-maintaining several contributed modules, and actively participating in issue queues and documentation efforts . His leadership roles include membership in the Drupal Community Working Group and the Conflict Resolution Team, as well as organizing the Florida Drupal Users' Group and Florida DrupalCamp for over a decade .(The Drop Times, nedcamp.org) As the host of the long-running DrupalEasy Podcast, Anello provides insights into Drupal development, community news, and interviews with key contributors, fostering a sense of connection and ongoing learning within the community (DrupalEasy). His dedication to mentoring and community building has made him a respected and influential voice in the Drupal world.
Mike HerchelMike is a seasoned front-end developer and a prominent contributor to the Drupal community, with over 15 years of experience in web development. He is best known as the lead developer of Olivero, Drupal's default front-end theme, which emphasizes accessibility, modern design, and user experience. (ImageX) In addition to his work on Olivero, Mike serves as a core CSS maintainer for Drupal and is the creator of the Quicklink module, which enhances site performance by preloading links in the user's viewport. He also has amazing calves. They're the size of small children. Rumor has it that his vertical jump is over 4.5 inches! He has also contributed to the introduction of Single Directory Components (SDC) into Drupal core, aiming to streamline component-based theming. (The Drop Times, herchel.com) Beyond his technical contributions, Mike is an active community leader. He has served on the Drupal Association Board of Directors and is a primary organizer of Florida DrupalCamp. (Drupal) As a speaker, he has presented at various events, including EvolveDrupal, discussing topics like the future of Drupal theming and the Starshot initiative, which seeks to make Drupal more accessible to site builders. (evolvedrupal.com) Professionally, Mike works as a Senior Front-End Developer at Agileana, where he continues to advocate for accessibility, performance, and the open web. (evolvedrupal.com) He shares his insights and experiences through his personal blog at herchel.com, contributing to the ongoing evolution of Drupal and its community.
Discussion Topics:
- The Best Podcast Idea Ever
- Florida Drupal Camp: A Legacy of Success
- Single Directory Components: Getting Started
- TD Cafe: The Podcast Name Debate
- Deep Dive into Single Directory Components
- Experience Builder and Component Integration
- Custom Themes and Single Directory Components
- Design Tool Integration
- CSS Variables and Component Architecture
- Template File vs Render Array
- CSS Preferences: Plain CSS vs Post CSS
- Top Drupal Events
- Concluding Remarks and Personal Plans
Mike Anello - DupalEasy ultimike
Mike Herchel - herchel.com mherchel
Pagination
- Previous page
- Page 14
- Next page