• CTRL ALT News
  • Posts
  • Creating a Modern 3D AI Landing Page Using Next.js 14 and Spline (2023)

Creating a Modern 3D AI Landing Page Using Next.js 14 and Spline (2023)

Learning a new framework like Next.js can seem daunting at first. But with the right guidance, building your first Next.js app doesn't have to be complicated. In this step-by-step tutorial, we'll walk through creating a complete Next.js website from scratch, using the latest version 14. We'll set up a clean project, build out visually impressive components like a 3D header animation

Creating a Modern 3D AI Landing Page Using Next.js 14 and Spline (2023)

Learning a new framework like Next.js can seem daunting at first. But with the right guidance, building your first Next.js app doesn't have to be complicated. In this step-by-step tutorial, we'll walk through creating a complete Next.js website from scratch, using the latest version 14. We'll set up a clean project, and build out visually impressive components like a 3D header animation.

Get the source on GitHub here.

Watch the video version here:

Setup

To get started we need to initialize a Next.js 14 project. To do this you need to run one of the following commands in your CMD, depending on your package manager:

npx create-next-app@latest
yarn create next-app
pnpm create next-app
bunx create-next-app

You will be prompted to select different options, to set up everything correctly, select the following options:

What is your project named?  my-app
Would you like to use TypeScript? Yes
Would you like to use ESLint?  Yes
Would you like to use Tailwind CSS?  Yes
Would you like to use `src/` directory?  No 
Would you like to use App Router? (recommended)  Yes
Would you like to customize the default import alias (@/*)?  No

Cleaning Up

You should now be set up with a clean Next.js 14 project. But there are a few things we need to delete to clean everything up.

The first thing we need to do is to clean the CSS file. Make sure the only CSS in your file is the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
}

We also need to clean the page.tsx file. Make sure your page.tsx file looks like this:

"use client";

export default function Home() {
  return (
    <main></main>
  )
}

Importing packages

We need to remember to import all the packages of our application:

"use client";

import Spline from '@splinetool/react-spline'
import Image from 'next/image'
import Link from 'next/link'
import infoCards from './libs/InfoCards'
import { CheckCheck, LucideIcon } from 'lucide-react'
import { ReactElement } from 'react'
import pricingCards from './libs/PricingCards'

export default function Home() {
  return (
    <main></main>
  )
}

If some of these packages show up as none existing currently, don’t worry, they will be created shortly.

Importing Our Data

  • Inside your app folder, create a new folder called libs.

  • Inside the libs folder, create a new file called InfoCards.ts

  • Inside the InfoCards.ts paste the following code:

app/libs/InfoCards.ts



import { AlarmClockOff, ArrowDownNarrowWide, ArrowUpNarrowWide, LucideIcon } from "lucide-react";

interface IInfoCard {
    title:string;
    icon: LucideIcon;
    bodyText:string;
    id:number;
}

const infoCards: IInfoCard[] = [
    {
        title: "Increased Sales",
        bodyText: " Insightful's predictive analytics identify high-value prospects for targeted pitches, boosting conversion rates and sales by up to 20%.",
        icon: ArrowUpNarrowWide,
        id: 1
    },
    {
        title: "No Time Wasted",
        bodyText: "Insightful automates personalized content creation, freeing up sales reps' time for revenue-focused activities and increased productivity.",
        icon: AlarmClockOff,
        id: 2
    },
    {
        title: "Decreased Churn",
        bodyText: " Insightful's AI lead engagement and renewal tools reduce customer churn by up to 30% through consistent outreach and retention opportunities.",
        icon: ArrowDownNarrowWide,
        id: 3
    },
]

export default infoCards

This code is a simple Typescript object with a few pieces of data that will later be used in an “about” section on our website.

  • Inside the libs folder, create a new file called PricingCards.ts

  • Inside the PricingCards.ts paste the following code:

app/libs/InfoCards.ts

interface IPricingCard {
    price:number;
    title:string;
    benefits:string[];
    id:number;
    oneliner:string;
}

const pricingCards:IPricingCard[] = [
    {
        price: 49,
        title: "Insightful Pro",
        benefits: [
            "Predictive lead scoring",
            "Automated content creation",
            "Personalized messaging at scale",
            "Customer retention tools",
        ],
        id: 1,
        oneliner: "AI-powered sales tools for focused revenue growth"
    },
    {
        price: 99,
        title: "Insightful Enterprise",
        benefits: [
            "Everything In Pro Tier, and:",
            "Data-driven recommendations",
            "Customizable sales workflows",
            "Real-time alerts and notifications",
        ],
        id: 2,
        oneliner: "Comprehensive sales optimization for accelerated revenue gains"
    },
]

export default pricingCards

The same goes for the code above, this is just a simple Typescript object that we will later use for a pricing section.

Background SVG

We also need a single image for our website, an SVG with a whirl pattern on it. The image can be downloaded here. Place the image inside the public folder of your project.

The Navbar

The very first thing we are going to do is create the navbar. Inside your page.tsx file we need to create a new component with the following code:

app/page.tsx

function Navbar() {
  return (
    <div className='w-full h-16 backdrop-filter backdrop-blur-xl bg-opacity-20 border-b flex items-center justify-center'>
      <div className='max-w-7xl w-full flex items-center justify-between p-4'>
        <h6 className='font-bold'>Insightful</h6>
        <ul className='flex gap-8'>
          <li><Link className='hover:text-fuchsia-500 transition-colors text-xs sm:text-base' href="#home">Home</Link></li>
          <li><Link className='hover:text-fuchsia-500 transition-colors text-xs sm:text-base' href="#about">About</Link></li>
          <li><Link className='hover:text-fuchsia-500 transition-colors text-xs sm:text-base' href="#pricing">Pricing</Link></li>
        </ul>
      </div>

    </div>
  )
}

Remember to not delete anything else, you still want to have the Home component and the rest of the elements in your page.tsx file.

Now we can use the Navbar component inside our Home component to show it on our website. But first, we need to do some styling in our Home component. We simply need a single <main> tag with some simple Tailwind CSS styles:

app/page.tsx

export default function Home() {
  return (
    <main className='flex min-h-screen h-fit flex-col items-center justify-center relative'>
    </main>
  )
}

We can now put the Navbar inside our <main> tag:

app/page.tsx

export default function Home() {
  return (
    <main className='flex min-h-screen h-fit flex-col items-center justify-center relative'>
      <Navbar />
    </main>
  )
}

Viewing your code

Now that you have created your first component, you probably want to see how it looks. Simple run the Next.js 14 dev command in your CMD like so:

npm run dev

Unless anything else is stated, your website should now be accessible on the URL: http://localhost:3000/

The navbar is probably in the center of your website at the moment, but don’t worry, this will be fixed later.

The Header (And 3D!!!)

Now we are ready for the fun stuff, the header with the 3D animation. The following code is everything you need for the header:

<header id="home" className="flex flex-col-reverse md:flex-row w-full h-screen max-w-7xl items-center justify-center p-8 relative overflow-x-hidden">
        <div className='w-full h-2/4 md:h-full md:w-2/5 flex flex-col justify-center items-center md:items-start gap-8'>
          <div className='flex flex-col gap-2'>
            <h1 className='text-4xl font-black md:text-8xl'>Insightful</h1>
            <h2 className='text-md md:text-2xl'>Start growing today!</h2>
          </div>
          <p className='max-w-md text-sm md:text-base text-zinc-500'>Insightful is an AI-powered sales optimization tool that provides data-driven insights to boost sales performance.</p>
          <div className='w-full flex items-center justify-center md:justify-start gap-4'>
            <button className='w-48 h-12 text-sm sm:text-base rounded bg-white text-black hover:bg-fuchsia-700 hover:text-white transition-colors'>Try 7 days free!</button>
            <button className='w-48 h-12 text-sm sm:text-base rounded hover:bg-white hover:text-white hover:bg-opacity-5 transition-colors'>Contact</button>
          </div>
        </div>

        <div className='w-full h-2/4 md:h-full md:w-3/5 flex items-center justify-center relative -z-10'>
          <Spline className="w-full flex scale-[.25] sm:scale-[.35] lg:scale-[.5] items-center justify-center md:justify-start" scene='https://prod.spline.design/pvM5sSiYV2ivWraz/scene.splinecode'/>
        </div>

</header>

The code above is a simple layout with two primary divs, the text and the 3D model. Using the @splinetool/react-spline we render the 3D model, using the exact scene URL is very important. The URL in question is: https://prod.spline.design/pvM5sSiYV2ivWraz/scene.splinecode

The header should be put right underneath the navbar component.

About Section

The next thing we need is the about section. The code for the about section is:

<section id="about" className="h-fit min-h-screen w-full flex relative items-center justify-center p-8">
        <div className='absolute -z-10 h-full w-full overflow-hidden'>
          <Image src="/whirl.svg" fill className="absolute object-cover w-full overflow-visible sm:rotate-90" alt="Background Whirl"/>
        </div>
        <div className="w-full h-full flex items-center justify-center flex-col gap-8 max-w-7xl">
          <h3 className='text-4xl md:text-5xl font-bold'>No More Time Wasted!</h3>
          <div className="w-full grid grid-cols-1 grid-rows-3 md:grid-cols-2 md:grid-rows-2 lg:grid-cols-3 lg:grid-rows-1 gap-4 justify-between relative">
            {infoCards.map((infoCard) => {
              return (
                <InfoCard key={infoCard.id} Icon={infoCard.icon} title={infoCard.title}>
                  <p className="text-sm sm:text-base text-center md:text-left">{infoCard.bodyText}</p>
                </InfoCard>
              )
            })}
          </div>
        </div>
</section>

The code above is also very simple. We are creating a simple image and setting it as our background image, by using z-index and position absolute.

This section should be put right underneath your header.

One thing to remember is that we are using a custom component that we have not created yet. The code for the custom component is:

interface IInfoCardProps {
  title:string;
  Icon:LucideIcon;
  children:ReactElement<any,any>
}

function InfoCard({title,Icon,children}:IInfoCardProps) {
  return (
    <div className='w-full h-80 rounded flex flex-col justify-around items-center p-8 bg-gray-900 rounded bg-clip-padding backdrop-filter backdrop-blur-xl bg-opacity-20'>
      <div className="p-4 bg-fuchsia-700 rounded-full">
        <Icon />
      </div>
      <div>
        <h3 className='text-lg font-bold sm:text-xl'>{title}</h3>
      </div>
      <div>{children}</div>
    </div>
  )
}

This component and interface should be pasted at the bottom of your code file. You should then be able to use it as part of your about section.

Price section

The last thing we need to do is create the pricing section, the code is:

<section id="pricing" className="h-fit min-h-screen w-full flex flex-col items-center justify-center gap-8 p-8">
        <h4 className="text-4xl md:text-5xl font-bold">Pricing</h4>
        <div className='grid grid-cols-1 grid-rows-2 sm:grid-rows-1 sm:grid-cols-2 items-center h-fit w-full max-w-3xl gap-8'>
            {pricingCards.map((pricingCard) => {
              return (
                <PricingCard oneliner={pricingCard.oneliner} title={pricingCard.title} price={pricingCard.price} benefits={pricingCard.benefits} key={pricingCard.id}/>
              )
            })}
        </div>
</section>

The section above should be posted underneath your about section.

As you can see, we are also using a custom component for the price section. We also need to create this component, the code is:

interface IPricingCardProps {
  title:string;
  price:number;
  benefits:string[]
  oneliner:string;
}

function PricingCard({title,price,benefits,oneliner}:IPricingCardProps) {
  return (
    <div className='h-fit w-full rounded flex flex-col p-8 gap-8 bg-gray-900 rounded bg-clip-padding backdrop-filter backdrop-blur-xl bg-opacity-20'>
      <div className='flex flex-col gap-2'>
        <div>
          <h6 className='text-2xl'>{title}</h6>
          <p className='text-sm text-zinc-500'>{oneliner}</p>
        </div>
        <p className='text-4xl font-bold'>
          ${price} <span className='text-sm font-normal text-zinc-500'>/ Month</span>
        </p>
      </div>
      <button className='bg-fuchsia-700 rounded p-2 text-sm transition-colors hover:bg-fuchsia-800'>Try 7 days free!</button>
      <div className='flex flex-col w-full gap-4'>
        {benefits.map((benefit, i) => {
          return(
            <p key={i} className='text-sm text-zinc-500 flex items-center gap-2'>
              <span>
                <CheckCheck />
              </span>
              {benefit}
            </p>
          )
        })}
      </div>
    </div>
  )
}

This component, and the interface, should also be put at the bottom of your file.

Rounding Up

Everything should now be working. If something is not working for you, the source code is linked at the top.

Credits

The 3D asset used in the article was originally created by "Kamilkoziel" on Spline. Link to his work: https://app.spline.design/community/file/927883a9-fc91-4de7-a434-ecf48acf819c