The best websites in the world tend to follow a similar pattern; they use 3D animations that bring the content to life when a page is scrolled. In this tutorial, we’ll build a simple 3D portfolio using Vite, React, Three.js and Strapi.
A portfolio is a collection of samples of your works. Your portfolio website is likely to be the first place someone goes after reading your CV. So, it’s vital to make a great first impression.
Strapi is an open-source Content Management System. It enables the creation of customizable APIs in any frontend application. Strapi is so simple to use because it allows you to create flexible APIs with unique features that you'll love.
To keep things organized, you can create custom content types and relationships between them. It also includes a media library where you can store your images and audio files. We’ll be using Strapi for the content that we’ll be using in our portfolio.
Vite is a modern, lightning-fast tool for scaffolding and project-bundling. It is quickly gaining traction due to its near-instant code compilation and rapid hot module replacement. It is a JavaScript development server that greatly enhances the frontend development experience. As a result, we can call it a development tool.
It converts your code into something that your browser can understand. Vite allows you to run a development server. It also handles and updates your files based on changes. This process is completed quickly in order to reflect your changes on the browser.
Vite is the next generation front-end tool to build apps faster. It’s why I decided to use it instead of the traditional CRA (create react app).
Three.js is a JavaScript framework that allows you to quickly build a 3D or 2D graphic on your web page. With Three.js, we don’t have to use WebGL directly. It’s an abstract layer built on WebGL to make it easier to use. Anyone can use a web browser to view 3D graphics without downloading any extra framework.
Since Three.js is built on JavaScript, adding interactivity between 3D objects and user interfaces is quite simple. This makes Three.js ideal for creating 3D games on the web platform. It also offers exceptional features such as effects, scenes, cameras, etc.
To follow through this article, you’ll need:
Npm or yarn installed (v6 of npm is more suited to install strapi).
Note: If you have issues while installing strapi, I'll suggest clearing npm
cache completely and downgrade to node 14 by uninstalling the higher version of node completely from your system and also anywhere node might appear). Accept the additional installation (the python scripts etc.) this worked for me.
To install and create our content in Strapi, we’ll need to install Strapi. Create a folder called strapi-portfolio
, cd into it, and run either of the following commands below:
npx create-strapi-app@latest my-project --quickstart
#or
yarn create strapi-app my-project --quickstart
This will install all the packages needed for this project. After the installation, our Strapi app will automatically be launched in our browser. We should have something like this:
Following registration, we will be directed to the admin homepage to configure our backend content and APIs.
Let us begin by creating an Estate collection type. Click Content-Type Builder
on your admin homepage, then create a new collection type
. Give it a display name, and make sure it's singular, not plural. Strapi automatically pluralizes it. We will call our collection portfolio
.
For our portfolio, we’ll need a name in form of text type, about me in form of rich text type, project in form of text type, and project description in form of rich text type.
Let’s go ahead and create them.
Click on Save to save our collection and publish to publish it. Let’s move on to populate our collection:
At the top-left corner of the admin page, select Content Manager. This will navigate you to the page where we will add contents to our database.
For this tutorial, I'll be working with dummy texts. Once we are done, we can either save and test it first or we skip that and go ahead to publish it.
Make the portfolio
available to consume it in our React frontend. Navigate to Roles
under Users and Permissions
Plugins. and click on the public
. Then, scroll down to permissions.
Under the Portfolio dropdown, select the find
and findOne
options by clicking on Public. This will make the portfolio content available to the general public.
So, if we try to retrieve it by using the Strapi API, it sends us the data. Now that we’re done, let’s move on to the frontend.
As previously stated, React would serve as our frontend. So, let's get started by installing React and all the necessary packages.
npx create-react-app 3d-portfolio
cd
into the just installed folder: cd 3d-portfolio
axios
npm install axios
npm install three
Now that we are done, let’s start our react app:
npm start
For this project, we’ll use three.js
to create our background and axios
to fetch our api
. We'll give some styling to our portfolio and we are done! As a lover of space, this project will be built with an aspiration matching outer space. You can get some of the images used here.
NOTE: This is just for tutorial purpose.
Navigate to app.jsx
in our src
folder and edit it to this:
1 import { useState } from 'react'
2 import './App.css'
3 function App() {
4 const [count, setCount] = useState(0)
5 return (
6 <div className="App">
7 <canvas id="bg"></canvas>
8 </div>
9 )
10 }
11 export default App
Here, we just displayed a canvas. This is all we need for a three.js scene. I’ll give an explanation on what scenes are as we’re building.
Let’s head over to index.css
to give the canvas fixed positioning and also pin it to the top left corner of the screen:
1 * {
2 margin: 0;
3 padding: 0;
4 }
5 body {
6 color: #fff;
7 font-family: Arial, Helvetica, sans-serif;
8 }
9 canvas {
10 position: fixed;
11 top: 0;
12 left: 0;
13 z-index: -1;
14 }
This will serve as a background for our portfolio. Now, we’ll go back to our app.jsx
file and import our Three.js library and also useEffect
from react:
1 import { useSEffect } from 'react';
2 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
3 import * as THREE from "three";
Our three.js codes will be written inside a ueEffect function. Let’s create that inside our function app()
:
1 useEffect(() => {
2
3 }, []);
There are some things we need when working with Three.js for 3D development. These are some examples:
Let’s paste this code block inside our useEffect function:
1 const scene = new THREE.Scene();
2 const camera = new THREE.PerspectiveCamera(
3 75,
4 window.innerWidth / window.innerHeight,
5 0.1,
6 1000
7 );
8 const renderer = new THREE.WebGLRenderer({
9 canvas: document.querySelector("#bg"),
10 });
11 renderer.setPixelRatio(window.devicePixelRatio);
12 renderer.setSize(window.innerWidth, window.innerHeight);
13 camera.position.setZ(30);
14 renderer.render(scene, camera);
We created a scene and a perspective camera. The perspectiveCamera
behaves similar to how the human eyeballs do. We also initailized a renderer. The renderer is going to take in the canvas element and also some other configurations. Let’s paste this right after our renderer.render
1 const geometry = new THREE.TorusGeometry(10, 3, 16, 100);
2 const material = new THREE.MeshStandardMaterial({ color: 0xff6347 });
3 const torus = new THREE.Mesh(geometry, material);
4 scene.add(torus);
5 const pointLight = new THREE.PointLight(0xffffff);
6 pointLight.position.set(5, 5, 5);
7 const ambientLight = new THREE.AmbientLight(0xffffff);
8 scene.add(pointLight, ambientLight);
9 const lightHelper = new THREE.PointLightHelper(pointLight);
10 const gridHelper = new THREE.GridHelper(200, 50);
11 scene.add(lightHelper, gridHelper);
12 const controls = new OrbitControls(camera, renderer.domElement);
We created geometry, material, and torus. A geometry is a set of vectors that define the object itself. For example, in Three.js, there are a bunch of geometry such as cylinder, cone, box, etc.
We are also instantiating a new pointLight
with a colour of white, setting its values, and adding it to our scene. We also instantiated an ambient light. It will give lightening to the entire scene equally.
We also added a ightHelper
and a gridHelper
. A lightHelper
is used to show the position of a pointLight
, while the gridHelper
draws a two-dimensional grid.
Lastly, for our background, let’s paste this :
1 function addStar() {
2 const geometry = new THREE.SphereGeometry(0.25);
3 const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
4 const star = new THREE.Mesh(geometry, material);
5 const [x, y, z] = Array(3)
6 .fill()
7 .map(() => THREE.MathUtils.randFloatSpread(100));
8 star.position.set(x, y, z);
9 scene.add(star);
10 }
11 Array(250).fill().forEach(addStar);
12 const spaceTexture = new THREE.TextureLoader().load("your image");
13 scene.background = spaceTexture;
14 const avatarTexture = new THREE.TextureLoader().load(
15 "your image"
16 );
17 const avatar = new THREE.Mesh(
18 new THREE.BoxGeometry(3, 3, 3),
19 new THREE.MeshBasicMaterial({ map: avatarTexture })
20 );
21 scene.add(avatar);
22 const planetTexture = new THREE.TextureLoader().load(
23 "your image"
24 );
25 const planet = new THREE.Mesh(
26 new THREE.SphereGeometry(3, 32, 32),
27 new THREE.MeshStandardMaterial({ map: planetTexture })
28 );
29 scene.add(planet);
30 planet.position.z = 30;
31 planet.position.setX(-10);
32 function moveCamera() {
33 const t = document.body.getBoundingClientRect().top;
34 planet.rotation.x += 0.05;
35 planet.rotation.y += 0.075;
36 planet.rotation.z += 0.05;
37 avatar.rotation.y += 0.01;
38 avatar.rotation.z += 0.01;
39 camera.position.z = t * -0.01;
40 camera.position.x = t * -0.0002;
41 camera.rotation.y = t * -0.0002;
42 }
43 document.body.onscroll = moveCamera;
44 function animate() {
45 requestAnimationFrame(animate);
46 torus.rotation.x += 0.01;
47 torus.rotation.y += 0.005;
48 torus.rotation.y += 0.01;
49 controls.update();
50 renderer.render(scene, camera);
51 }
52 animate();
What we did here is:
textureLoader
.These are all the configurations that we need for our portfolio.
Inside our src
folder, let's create a folder called component. Inside this folder, create a file called index.jsx
and paste this:
1 import axios from "axios";
2 const url = "http://localhost:1337/api/portfolios";
3 export const readPortfolios = () => axios.get(url);
Here, we are:
Axios will be responsible for fetching data from our API and also adding to it. We then go back to our app.jsx
and import the just created API folder containing the index.jsx
to our app:
1 import * as component from "./component";
2
3 const [portfolios, setPortfolios] = useState([]);
4 useEffect(() => {
5 const fetchData = async () => {
6 const result = await component.readPortfolios();
7 setPortfolios(result.data.data);
8 console.log(result.data);
9 };
10 fetchData();
Almost done! We just need to do two things. First, we need to map and display our contents. we’ll do this by creating writng basic html and calling out our contents in there. Paste this:
1 <h1>Hello world</h1>
2 {portfolios.length > 0 ? (
3 portfolios.map((portfolio, i) => (
4 <div className="title" key={i}>
5 <div className="portfolio">
6 <h1 className="title"> {portfolio.attributes.myname}</h1>
7 <p className="text">
8 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
9 eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
10 enim ad minim veniam, quis nostrud exercitation ullamco laboris
11 nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
12 in reprehenderit in voluptate velit esse cillum dolore eu fugiat
13 nulla pariatur. Excepteur sint occaecat cupidatat non proident,
14 sunt in culpa qui officia deserunt mollit anim id est laborum.
15 </p>
16
17 </div>
18 <div className="projects">
19 <h1>Projects</h1>
20 <div className="projects-table">
21 <div className="projects-card">
22 <p className="title">Porject1</p>
23 <p className="info">
24 {portfolio.attributes.projectdescription}
25 </p>
26 <button className="btn">check it out here</button>
27 </div>
28 <div className="projects-card">
29 <p className="title">project2</p>
30 <p className="info">
31 {portfolio.attributes.projectdescription}
32 </p>
33 <button className="btn">check it out here</button>
34 </div>
35 <div className="projects-card">
36 <p className="title">project3</p>
37 <p className="info">
38 {portfolio.attributes.projectdescription}
39 </p>
40 <button className="btn">check it out here</button>
41 </div>
42 </div>
43 </div>
44 </div>
45 ))
46 ) : (
47 <h2></h2>
48 )}
Lastly, let’s add some basic styling to it. Go back to index.css
and add this:
1 .portfolio {
2 padding: 4em 3em;
3 width: calc(50% - 2em);
4 }
5 .portfolio .title {
6 color: orange;
7 font-size: 3em;
8 margin-bottom: .5em;
9 }
10 .portfolio.text {
11 letter-spacing: 1px;
12 line-height: 1.5;
13 color: rgb(226, 208, 208);
14 margin-bottom: 1em;
15 }
16 .btn {
17 background: rgb(194, 189, 181);
18 color: #fff;
19 padding: .5em 1em;
20 border-radius: 3px;
21 text-decoration: none;
22 font-size: 1em;
23 margin-top: 1em;
24 border: none;
25 margin-right: .5em;
26 text-transform: uppercase;
27 font-weight: bold;
28 letter-spacing: 1px;
29 }
30 .projects h1 {
31 text-align: center;
32 font-size: 2.5em;
33 }
34 .projects-table {
35 padding: 3em;
36 display: flex;
37 flex-direction: row;
38 }
39 .projects-card {
40 background-color: rgba(255, 255, 255, .3);
41 padding: 1em;
42 border-radius: 5px;
43 transform: translate(-100px, 100px);
44 opacity: 0;
45 animation: 1s slideIn;
46 animation-delay: 2s;
47 animation-fill-mode: forwards;
48 }
49 @keyframes slideIn {
50 to {
51 transform: translate(0, 0);
52 opacity: 1;
53 }
54 }
55 .projects-card:nth-child(2) {
56 margin: 0 2.5em;
57 transform: translate(0, 100px);
58 animation-delay: 2.2s;
59 }
60 .projects-card:nth-child(3) {
61 transform: translate(100px, 100px);
62 animation-delay: 2.4s;
63 }
64 .projects-card .title {
65 font-size: 1.5em;
66 color: #fff;
67 font-weight: bold;
68 }
69 .projects-card .info {
70 margin: 1em 0;
71 }
72 .projects-card .btn {
73 background: orange;
74 }
Check the result in your browser. We have our portfolio running! Here is the link to the code on my Github.
We talked about Vite and Three.js. We also learnt how to create a simple 3D portfolio website with them using Strapi for our content. There are many other things we can build with Strapi. This is why many companies use it so why not you?
I'm a web developer and writer. I love to share my experiences and things I've learned through writing.