We are going to be creating a product landing page which will utilize 3D models and particle effects to take product showcasing to a whole new level. The goal of this tutorial is to introduce you to the concepts of working with a 3D environment in the browser, while using modern tooling, to create your own highly performant 3D sites.
The final project can be viewed at 3d-product-page.netlify.app/
And the final code can be viewed at github.com/molebox/3d-product-page
This tutorial assumes some prior knowledge of modern frontend development but will safely guide you through the whole setup and development process.
Helpful to know
We are going to be using snowpack as our build tool. Its a modern tool that is similar to Webpack, but takes a slightly different approach. Instead of bundling our entire application and recompiling on every code change and save, snowpack only rebuilds single files where the changes have been made. This results in a very fast development process. The term used by the snowpack team is unbundled development where individual files are loaded to the browser during development with ESM syntax.
Our application will be written in React and use Chakra-ui for styling. Chakra is an accessibility first component library which comes with superb defaults and enables us to build accessible, modular components at speed. Think of styled components with easy theming and composability.
We will be utilizing Threejs by way of a wonderful React library called react-three-fiber, which allows us to easily interact with Three using common React techniques. The library is a renderer for Three, using it we can skip a lot of mundane work such as scene creation and concentrate on composing our components in a declarative way with props and states.
The renderer allows us to use all of the Three classes, objects and properties as elements in our markup. All classes constructors arguments can be accessed via an
args prop. A simple mesh with a box class can be seen below. Don't worry if you don't understand what this means, we will go over everything shortly.
Our page will be rendered in MDX, a format which allows us to write JSX and include React components in markdown files. It's a wonderful development experience and one I hope you will fall in love with once we reach the end of the tutorial.
I have created a handy snowpack template that creates a project with snowpack, chakra and MDX all installed. It also comes with React Router v6 but we wont be using that so will remove that boilerplate.
Open a new terminal and navigate to your desired project folder and run the following which will create our new project. Change
my-new-app to your apps name.
Next we can install our projects dependencies.
Now that we have our dependencies installed we can begin to tear out some of the stuff we wont need. Our landing page will encompass a single page so we can open the
mdx-routes.js file and remove the
Nav component and the page-two route from the
MDXRoutes component. We'll return to this file later to add some styling but for now we can move on.
Inside the pages folder delete page-two and remove the contents from page-one. Inside the components folder delete the emoji component and add a new folder called 3d. And that's it, we are now ready to begin coding some sick 3D landing page goodness!
mdx-layout.js file located in the components folder. This will wrap our whole app, in our case our one landing page. Our page will consist of a css grid, we'll use grid areas to get a nice visual representation of how our page will layout. Remove what is currently in there and add the following.
Using Chakras Grid component we set the amount of columns to have a responsive padding of 10% of the viewport width on each side of two flexible one fractional units of space. This basically means that the meat of our page will live in the two fractional columns, with each taking up as much space as they need before they hit the 10% padding on each side. Our rows follow the same logic except we save 10% for our header row and the rest takes up as much space as needed. As you can see, we have a background color set on the bg (background) prop. But where does that value come from and what does it mean?
theme.js file located in the
src folder. This is our global theme for our app. We are importing the default theme from Chakra, which itself uses the Tailwind default preset. We are then overriding the colors with our own brand colors. The font sizes are also being overridden to allow us to use slightly different sizes to the default. Go ahead and copy the following colors object into the file instead of the current one.
MDX is just markdown that you can write JSX in. So that means that we can write normal markdown like so:
But we can also add to that React components We can even compose React components right in the MDX file! Let's open up the
index.js file in the
src folder and check out how we can add components to our MDX file without using imports.
Let's break down what's going on in here. If we scroll to the bottom we can see an
MDXProvider wrapping our app. It accepts a components prop into which we have passed a components object declared above. The components object allows us to map React components to markdown elements as well as passing in custom components for use in our MDX files. As you can see, this template has set this all up for us by mapping some basic markdown elements to some Chakra components. Where there is no object key we have passed in a custom component which can be used in the MDX file without importing it as you would in a normal js or jsx file.
MDX accepts a special key called
wrapper which will wrap the entire file with whatever is passed to it. In our case it will take our previously created layout component along with it's grid and use that to wrap our MDX file. Now that we know where the components are coming from when using them in our MDX file, let's go ahead and write some React in markdown!
Opening up the
page-one.mdx file located in the pages folder, add the following.
We are using the Flex component provided to us from Chakra via the
MDXProvider. This component allows us to quickly apply flex box props to the base element, a div. Even though the component is based upon a div we can give it semantic meaning by utilizing the
as props and setting it the header. If we check our layout file again and look at our grid areas we can see that we have
edge on the first and second rows. So we have set the grid area to edge and the row to 1.
This places our component in the top left hand corner of the page. We have given it a margin-left (ml) so that it doesn't hit the edge. As you can see from the code block above, we are inserting an image. If you navigate to this url you will see that it's a Nike swish (swoosh, tick? I dunno)
Let's add some copy to our page. This will be in the first column of our two middle columns. It will hold the title to our page and some copy about the Nike Air Jordan 1's, the product we are showcasing. Directly below the first Flex code block in the
page-one.mdx file add the following:
Here we have added another Flex container component, given the grid area of text and some other positional properties. Inside we have added our title and two paragraphs or copy, describing the trainers.
Next we are going to get a bit fancy and create a custom component to display some text on a vertical axis. As we will be re-using this component we will create it with some defaults but allow for customization. Inside the components folder create a new file called
custom-text.js and add the following.
We could have used text-orientation here but I found it wasn't flexible enough for this use case so instead decided to use a good old fashioned transform on the text. We use a styled component so that we can add a text effect (-webkit-text-stroke) which isn't available as a prop with a Chakra Text component. This effect allows us to give the text an stroked outline. It takes the color provided as a prop or just uses the set default grey color. Finally our component accepts some size and orientation props, as well as the actual text it is to display. Next we need to add our new component to the components object which is passed into the
We'll use this new component to display some vertical text alongside out copy. Below the copy add the following.
If you now run
npm run start from the root of the project you should see a red Nike tick in the top left, a title of Air Jordan 1 and some copy below it. To the left of that cop you should see the work Innovation written vertically with an grey outline. It's not much to look at so far, let's spice things up a little with a 3D model!
Before we dive into adding a 3D model to our page let's take a little time to understand how we are going to do that. This isn't some deep dive into Threejs, WebGL and how the react-three-fiber renderer works, rather we will look at what you can use and why you should use it.
For us to render a 3D model on the page we will need to create a Three scene, attach a camera, some lights, use a mesh to create a surface for our model to live on and finally render all that to the page. We could go vanilla js here and type out all that using Three and it's classes and objects, but why bother when we can use react-three-fiber and rather lovely abstraction library call drei (Three in German).
We can import a canvas from react-three-fiber which takes care of adding a scene to our canvas. It also lets us configure the camera and numerous other things via props. It's just a React component at the end of the day, all be it one that does a ton of heavy lifting for us. We'll use our canvas to render our model on. The canvas component renders Three elements, not DOM elements. It provides access to Three classes and objects via it's context so any children rendered within it will have access to Three.
Our canvas can go anywhere on our page but it's important to remember that it will take up the height and width or it's nearest parent container. This is important to remember as if you wanted to display your canvas on the whole screen you would have to do something of a css reset like this:
In order to render something, like a shape, to our canvas we need to use a mesh. A mesh is like a base skeleton that an object is made from, like a wireframe. to create a basic shape, such as a sphere, we would have to attach a geometry so that the wireframe can form into a shape, and a material so that it no longer looks just like a wireframe. I like to think of it like chicken wire. You can have a flat piece of chicken wire which you then form into a shape (geometry attached). You can then cover that chicken wire in some material such as a cloth (material attached). To decide where to place an object on the canvas we can use the position prop on the mesh, this prop takes an array as [x, y, z] which follows the logical axis with z as depth.
Each Three class takes constructor arguments which enable you to modify it's appearance. To pass these constructor arguments to our Three element we use the args prop which again uses the array syntax. Let's look at an example of this. The box geometry class accepts 3 main arguments, width, height and depth. These can be used like so with react-three-fiber
When creating objects or models it's important to remember to provide the scene with a light source, otherwise the only thing you will be able to see is a black outline of whatever it is you are trying to render. This makes sense if you think about it. You wouldn't be able to see a shape in a dark room, add a light source of any kind and that shape suddenly takes form and has a surface with colors and contours.
An oldie but a goodie, article in smashing magazine that outlines some of the light you can use in Three.
We can view the following example which uses the react-three-fiber Three elements and also outlines examples or doing the same thing but with the drei helper library.
Now that we have an understanding of what to use let's create a component for a our product model. Inside the 3d folder create a new file called
model.js and add the following.
Our component is fairly generic due to the props it takes. The scene path refers to the path to the gltf file that houses the model. The position props which is passed down to the mesh positions the model on the canvas, and the rotation sets the rotation of the model But what is gltf? In a nutshell, it's a specification for loading 3D content. It accepts both JSON (.gltf) or binary (.glb) formats. Instead of storing a single texture or assets like .jgp or .png, gltf packages up all that is needed to show the 3D content. That could include everything from the mesh, geometry, materials and textures. For more information checkout the Three docs.
To load our model files we use a helper hook from drei (useGLTF) which uses useLoader and GTLFLoader under the hood. We use the useFrame hook to run a rotation effect on the model using a ref which we connect to the mesh. The mesh we rotate on the X axis and position according to the provided props.
We use a primitive placeholder and attach the model scene and finally pass in a separate lights component which we will soon create.
For our model we will be downloading a free 3D model from Sketchfab. Create a free account and head to this link to download the Nike Air Jordan 1's model. You will want to download the Autoconverted format (glTF), which is the middle option. To access our model files in our application open the public folder at our projects root and add a new folder called shoes, inside this folder paste over the textures folder, scene.bin and scene.gltf files. Now that we have created our product model component and downloaded the model files we need to create the canvas that the model shall live in on our page. Inside the 3d folder create a new file called
canvas-container.js and add the following.
Our new component has a container div (Box) which takes props for it's width, height and anything else we might fancy passing in. It's z-index is set to a high value as we will be placing some text beneath if. The canvas has a camera set with a field of view (where the higher the number the further away the view). We wrap the children in a
Suspense so that the application doesn't crash while it's loading.
Now create a new file in the same folder called
product.js and add the following code.
We want to let our user interact with out model. Importing the orbital controls from drei allows the user to zoom in/out and spin around the model all with their mouse letting them view it from any angle, a cool touch.
But we won't be able to see anything if we don't add any lights to our canvas. Inside the 3d folder create a new file called model-lights and add the following.
Now it's time to add these bad boys to the MDX file. Add the
Product component to the components object the same way we did with the
Now add the following below the Flex component that sets the innovation text.
Setting the grid area to product places our model in the correct row and column of our grid. We give the Flex component a position of relative as we want to absolutely position the text that is underneath the model. This gives our page a sense of depth that is accentuated by the 3D model. If we run our development server again we should we the shoes spinning around to the right of the copy!
Our page is looking pretty dope but there are a few more finishing touches that would make it sparkle just that little brighter. Head over to Sktechfab again and download this basketball model. Inside the 3d folder create a new file called
basketball.js and add the following.
Utilizing out generic canvas and model components we are able to create a new component that will render a basketball to the page. We are going to position this basketball to the left of the Air Jordan title text. Noice. Add the new Basketball component to the component s object like we have done before and open the MDX file and add the new component under the title text.
Sweet! It's almost complete. Subtle animations that aren't obvious to the user straight away are a nice addition to any website. Let's add a glitch effect to our title text which only runs when the site visitor hovers their mouse over the text.
Inside the components folder create a new file called
glitch-text.js and add the following.
Our new component uses a styled div component to set its internal css. We state that the following effect shall only run when the element is hovered and then use the pseudo elements to insert some glitchy goodness. The pseudo content is the text passed in as children, we animate some clip paths via some keyframes and give the effect that the text is moving. Add this new component to the components object as
GlitchText and then wrap the title text in the new component in the MDX markup.
We've come so far and we have covered some steep terrain. We have taken a broad overview of working with 3D components and models in React, looked at designing layouts using css grid. Utilized a component library to make our life easier and explored how to create cool, interactive markdown pages with MDX. Our product page is basically complete, anyone who came across this on the interwebs would certainly be more drawn in than your run of the mill static product pages. But there is one last thing I would like you to add, something subtle to make the page pop. Let's add some particles!
We have already installed the package so create a new file inside the component folder called background and add the following.
This will serve as our background to our site. We have absolutely positioned the parent container of the particles so that they take up the whole of the page. Next open the routes file and add a Box component and the new
Start up the development server marvel at your handy work! Great job. If everything went according to plan then your site should look just like the demo site 3d-product-page.netlify.app/
We accomplished quite a lot during this tutorial and hopefully there are some take homes that can be used on other projects you create. If you have any questions shoot me a message on Twitter @studio_hungry, I'd be more than happy to have a chinwag about your thoughts and would love to see what you create with your new found 3D knowledge!