Let's build a personal website with react, Vite and Tailwind CSS

Here's a step-by-step guide on how to build a website with react and tailwind CSS.

Let's build a personal website with react, Vite and Tailwind CSS
Photo by Eftakher Alam / Unsplash

Project Setup

First, make sure you have Node.js and NPM (Node Package Manager) installed on your machine. You can check if you have these by running the following commands in your terminal:

node -v
npm -v

Next, create a new directory for your project and navigate to it in your terminal. Run the following command to create a new React app using create vite@latest:

npm create vite@latest

Vite will offer a wide array of options to use as a UI framework/library like Vue.JS, Svelte and React. Let's use React in this example.

Install project dependencies

npm install tailwindcss yup formik clsx react-router-dom @radix-ui/react-alert-dialog

Next, create a configuration file for Tailwind CSS. Run the following command to generate a default configuration file:

npx tailwindcss init

Now, let's create a CSS file for our Tailwind styles. Create a new file called tailwind.css in the src directory of your project and add the following line to it:

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

Creating Routes

Creating a router helps us navigate to different pages inside the project. For this case, we need a home page, and a dynamic project detail page. Before working with the router, let first configure it in the main.tsx page

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { BrowserRouter } from 'react-router-dom'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
)

Next, create a router folder with a Router.tsx file

import React from 'react'
import { HomePage, ProjectPage } from '../pages'
import { Route, Routes } from 'react-router-dom'

function Router() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />}></Route>
      <Route path="/projects/:slug" element={<ProjectPage />}></Route>
    </Routes>
  )
}

export default Router

Finally, add the router component as a child of the App component in app.tsx

import { Router } from './router'
import './App.css'
import { Layout, HomePage } from './components'
import { AlertProvider } from './context/AlerctContext'
function App() {
  return (
    <AlertProvider>
      <Layout>
        <Router />
      </Layout>
    </AlertProvider>
  )
}

export default App

Creating the project Layout

The layout component will wrap our App and add a footer and a nav bar to it. This helps us avoid adding these components to every page, as w know all our pages will display them.

import React, { ReactNode } from 'react'
import Navbar from './Navbar'
import Footer from './Footer'
import { Container } from './Container'

function Layout({ children }: { children: ReactNode }) {
  return (
    <div>
      <Navbar />
      <Container>{children}</Container>
      <Footer />
    </div>
  )
}
export default Layout

Creating the navigation bar

  • Create a Navbar.tsx in the components directory.
  • Add the Radix icons package to use the beautifully designed icon pack
npm install @radix-ui/react-icons

Below is the full code for the navbar component.

import React, { useEffect, useRef, useState } from 'react'
import {
  GitHubLogoIcon,
  LinkedInLogoIcon,
  InstagramLogoIcon,
  EnvelopeClosedIcon,
  RocketIcon,
} from '@radix-ui/react-icons'
import cn from 'clsx'

function Navbar() {
  const [yOffset, setYOffset] = useState(window.pageYOffset)
  const [visible, setVisible] = useState(true)

  const handleClickScroll = (sectionId: string) => {
    const element = document.getElementById(sectionId)
    if (element) {
      // 👇 Will scroll smoothly to the top of the next section
      element.scrollIntoView({ behavior: 'smooth' })
    }
  }

  useEffect(() => {
    window.addEventListener('scroll', handleScroll)
    return () => window.removeEventListener('scroll', handleScroll)
  })

  function handleScroll() {
    const currentYOffset = window.pageYOffset
    const visible = yOffset > currentYOffset

    setYOffset(currentYOffset)
    setVisible(visible)
  }

  return (
    <nav
      className={cn('sticky top-0 ease-in duration-300', {
        '-top-16': !visible,
      })}
    >
      <ul className="flex gap-12 justify-center items-center p-4 backdrop-blur-lg border-b bg-white bg-opacity-60 border-gray-200 md-gap-16">
        <a
          className="flex gap-4 justify-center items-center"
          href="https://github.com/AchrafGarai"
        >
          <GitHubLogoIcon width={20} height={20} />
          <li className="hidden md:block">Github</li>
        </a>
        <a
          className="flex gap-4 justify-center items-center"
          href="https://www.linkedin.com/in/achraf-garai/"
        >
          <LinkedInLogoIcon width={20} height={20} />
          <li className="hidden md:block">LinkedIn</li>
        </a>
        <a
          className="flex gap-4 justify-center items-center"
          href="https://www.instagram.com/achraf_garai/"
        >
          <InstagramLogoIcon width={20} height={20} />
          <li className="hidden md:block">Instagram</li>
        </a>
        <button
          onClick={() => handleClickScroll('projectsSection')}
          className="flex gap-4 justify-center items-center"
        >
          <RocketIcon width={20} height={20} />

          <p className="hidden md:block">My Projects</p>
        </button>
        <button
          onClick={() => handleClickScroll('contactSection')}
          className="flex gap-4 justify-center items-center"
        >
          <EnvelopeClosedIcon width={20} height={20} />

          <p className="hidden md:block">Contact me</p>
        </button>
      </ul>
    </nav>
  )
}

export default Navbar

Creating pages

Next, let's create a Pages folder that contains our pages, the Homepage.tsx, and the HrojectDetailsPage.tsx. Each of these components represents a page that is defined in the router.

Creating the home page component

On the home page lets create and import three components for our Hero, projects and contact sections.

import LandingSection from '../components/LandingSection'
import ProjectsSection from '../components/ProjectsSection'
import ContactSection from '../components/ContactSection'
import { Container } from '../components/Container'
import { Helmet } from 'react-helmet'

function HomePage() {
  return (
    <Container>
      <Helmet>
        <title>Welcome to my blog 🔥</title>
      </Helmet>
      <LandingSection />
      <ProjectsSection />
      <ContactSection />
    </Container>
  )
}
export default HomePage

Let's create a components folder to hold all our non-page components.

Landing section

Next in the components folder, create a HeroSection.tsx file

import React from 'react'

const greeting = 'Hello, I am Achraf!'
const bio1 =
  'A frontend developer and designer. I am also passionate about code and UI'
const bio2 = 'specialised in React'

function LandingSection() {
  return (
    <div className="text-center flex flex-col items-center justify-center gap-8 ">
      <img
        className=" object-cover my-16 w-64 h-96 rounded-full"
        src="/profile.jpg"
        alt="Achraf Garai"
      />
      <h1 className="my-4 text-5xl font-serif">{greeting}</h1>
      <p className="text-xl">{bio1}</p>
    </div>
  )
}

export default LandingSection

Projects Section

The projects section loads a JavaScript object contains the project details in the Data folder (Full Code available in the GitHub repo). The component renders each component as a card in a Grid layout.

import { projects } from '../data/posts'
import ProjectCard from './ProjectCard'

function ProjectsSection() {
  return (
    <div
      id="projectsSection"
      className="my-8 py-8 grid grid-cols-1 gap-8 md:grid-cols-2"
    >
      {projects.map((project) => (
        <ProjectCard
          key={project.title}
          title={project.title}
          image={project.image}
          description={project.description}
          slug={project.slug}
        />
      ))}
    </div>
  )
}

export default ProjectsSection

For each project, we render a project card components with is added in ProjectCard.tsx

import { Project } from '../data/posts'
import { Link } from 'react-router-dom'

function ProjectCard({ title, description, image, slug }: Project) {
  return (
    <Link to={`/projects/${slug}`}>
      <div className="border border-slate-100 rounded-lg overflow-hidden">
        <img src={image} className=" object-cover w-full h-96 mb-4" alt="" />
        <div className="p-4">
          <p className="mb-4 text-2xl font-bold text-gray-800">{title}</p>
          <p className="text-lg text-gray-500">{description}</p>
        </div>
      </div>
    </Link>
  )
}

export default ProjectCard

Contact section

First, make sure you have the Formik and Yup libraries installed in your project (see project setup section). Next, import the Formik and Yup libraries in your component file:

import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

Now, let's define a validation schema for our form using Yup. This will define the rules for each form field, such as whether it is required or has a minimum length.

import { Formik, Form, Field, ErrorMessage  } from 'formik';
import { AlertType } from '../context/AlerctContext';
import * as Yup from 'yup';
import { useAlertContext } from '../context/AlerctContext';


 const SignupSchema = Yup.object().shape({
   name: Yup.string()
     .min(2, 'Name is too Short!')
     .max(50, 'Too Long!')
     .required('This field is required!'),
    message: Yup.string()
     .min(2, 'Your message is too short!')
     .max(140, 'Too Long!')
     .required('This field is required!'),
    email: Yup.string()
      .email('Invalid email')
      .required('This field is required!'),
    enquiry: Yup
    .string()
    .oneOf(["freelance", "open source", "other"])
    .required("This field is required!"),
 })

Finally, we add the Contact From components (see the GitHub repo for the full code)

Project Details

The components use UseParams from React to take the URL path for the project (example /projects/react-state). Then we fetch the project with the correct title from our data object.

import { useParams } from 'react-router-dom'
import BackButton from '../components/BackButton'
import ContactSection from '../components/ContactSection'
import { Container } from '../components/Container'
import { projects } from '../data/posts'
import NotFound from './NotFound'
import { Helmet } from 'react-helmet'

function ProjectPage() {
  const params = useParams()
  const slug = params.slug || ''
  const project = projects.find((p) => {
    return p.slug === slug
  })

  if (project == undefined) {
    return <NotFound />
  } else {
    return (
      <div>
        <Helmet>
          <title>{project.title}</title>
        </Helmet>
        <Container variant="blog">
          <BackButton to="/" />
          <img
            src={project.image}
            alt={project.title}
            className=" object-cover w-full h-96 rounded-2xl"
          />
          <section>
            <h1 className="py-16 text-4xl font-bold text-slate-800 text-center">
              {project.title}
            </h1>
          </section>
          <section></section>
          <section>{project.description}</section>
        </Container>
        <div className="my-24">
          <ContactSection />
        </div>
      </div>
    )
  }
}

export default ProjectPage

Click the button below to grab the full project files and test the project.

GitHub - AchrafGarai/Developer-Portfolio-Starter-Project-with-React---Tailwind-CSS: A simple portfolio website starter project with TailwindCSS, React and Vite
A simple portfolio website starter project with TailwindCSS, React and Vite - GitHub - AchrafGarai/Developer-Portfolio-Starter-Project-with-React---Tailwind-CSS: A simple portfolio website starter ...