Project Overview
This project involved designing and developing a responsive portfolio website from scratch. The goal was to create a clean, modern, and performance-optimized website that effectively showcases my projects and skills while incorporating interesting interactive elements.
The website features custom animations, dark mode support, responsive design for all device sizes, and React integration for dynamic components. The development process focused on maintaining clean, modular code while optimizing for performance and accessibility.
HTML5
CSS3
JavaScript
React
Dark Mode
Responsive Design
Performance Optimization
Key Features
A seamless dark/light mode toggle with localStorage persistence to remember user preferences. The implementation uses CSS variables to create a consistent theming system throughout the site.
JavaScript
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = themeToggle.querySelector('i');
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
} else {
document.documentElement.classList.add('dark');
themeIcon.classList.remove('fa-moon');
themeIcon.classList.add('fa-sun');
if (!savedTheme) {
localStorage.setItem('theme', 'dark');
}
}
The CSS variables make it easy to define color schemes for both light and dark modes:
CSS
:root {
--bg-color: #f8f9fa;
--text-primary: #212529;
--accent-primary: #5b46ff;
}
.dark {
--bg-color: #0a0d14;
--text-primary: #eef1f8;
--accent-primary: #6c63ff;
}
The site uses React for interactive elements like the animated text headers. This integration allows for reusable components without the need for a full React build pipeline.
JSX
import React, { useEffect, useRef } from 'react';
const HeaderPressure = ({ text, className = '' }) => {
const containerRef = useRef(null);
const titleRef = useRef(null);
const spansRef = useRef([]);
const mouseRef = useRef({ x: 0, y: 0 });
const cursorRef = useRef({ x: 0, y: 0 });
const chars = text.split('');
useEffect(() => {
const handleMouseMove = (e) => {
cursorRef.current.x = e.clientX;
cursorRef.current.y = e.clientY;
};
window.addEventListener('mousemove', handleMouseMove);
}, []);
};
The React components are mounted into the DOM using the createRoot API:
JavaScript
import React from 'react';
import { createRoot } from 'react-dom/client';
import HeaderPressure from './HeaderPressure';
const headerElements = [
{
id: 'nameHeader',
text: 'Sia Khorsand',
className: 'text-4xl font-title text-primary-950'
},
];
headerElements.forEach(({ id, text, className }) => {
const container = document.getElementById(id);
if (container) {
const root = createRoot(container);
root.render(
<React.StrictMode>
<HeaderPressure text={text} className={className} />
</React.StrictMode>
);
}
});
The portfolio includes various interactive elements to create an engaging experience, from subtle hover animations to 3D tilt effects on project cards.
JavaScript
function applyProjectCardTiltEffects() {
document.querySelectorAll('.project-card').forEach(card => {
card.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const x = e.clientX - rect.left - centerX;
const y = e.clientY - rect.top - centerY;
const rotateX = -(y / centerY) * 20;
const rotateY = (x / centerX) * 20;
card.style.transform = `
perspective(800px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
translateZ(15px)
scale3d(1.05, 1.05, 1.05)
`;
const shadowX = -rotateY / 1.5;
const shadowY = rotateX / 1.5;
card.style.boxShadow = `
${shadowX}px ${shadowY}px 25px rgba(0, 0, 0, 0.15),
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -4px rgba(0, 0, 0, 0.1)
`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'perspective(800px) rotateX(0deg) rotateY(0deg) translateZ(0) scale3d(1, 1, 1)';
card.style.boxShadow = 'var(--card-shadow)';
});
});
}
The website is fully responsive, adapting to all screen sizes from mobile phones to large desktop monitors. Special attention was paid to optimizing performance on mobile devices.
CSS
@media (max-width: 767px) {
* {
transition-duration: 0.2s !important;
}
.profile-photo-wrapper,
.profile-photo,
.text-pressure-title span {
animation: none !important;
transform: none !important;
transition: none !important;
will-change: auto !important;
}
.project-image {
height: 140px;
}
}
JavaScript
document.addEventListener('DOMContentLoaded', function() {
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
document.body.classList.add('touch-device');
}
const isMobile = window.innerWidth <= 767;
if (isMobile) {
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('loading')) {
img.setAttribute('loading', 'lazy');
}
});
}
function setVHVariable() {
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
setVHVariable();
window.addEventListener('resize', setVHVariable);
window.addEventListener('orientationchange', setVHVariable);
});
Architecture and Design Decisions
Several key architectural decisions were made to ensure the website's performance, maintainability, and user experience:
No Framework Approach: The site was built without a heavy framework like Next.js or Gatsby, focusing instead on vanilla HTML/CSS/JS with targeted React integration where needed. This approach provides faster initial load times and simpler maintenance.
CSS Variables for Theming: Using CSS variables for colors and other theme elements allows for easier maintenance and the seamless implementation of dark mode.
Progressive Enhancement: Core content is accessible without JavaScript, with interactive elements layered on top for enhanced experience when JS is available.
Mobile-First Optimizations: Performance considerations for mobile devices were prioritized, including simplified animations, lazy loading, and touch-friendly interfaces.
Modular JavaScript: Code is organized into discrete modules with clear responsibilities for better maintainability.
Animation Techniques
The website uses several animation techniques to create an engaging experience without sacrificing performance:
Text Animation
The "About Me" and project overview sections feature a subtle text transformation effect that reveals the text character by character with a randomized animation.
JavaScript
document.addEventListener('DOMContentLoaded', () => {
const projectOverviewEl = document.getElementById('project-overview');
const originalHTML = projectOverviewEl.innerHTML;
const lines = originalHTML.split(/
/);
const characters = 'abcdefghijklmnopqrstuvwxyz_';
let interval;
let iteration = 0;
const totalIterations = 10;
function transformLine(line, iteration, totalIterations) {
const progress = iteration / totalIterations;
const charsToReveal = Math.floor(line.length * progress);
let transformed = "";
for (let j = 0; j < line.length; j++) {
if (j < charsToReveal) {
transformed += line[j];
} else {
if (line[j].match(/[<>\/]/) || line[j] === ' ' || line[j].match(/[.,!?]/)) {
transformed += line[j];
} else {
transformed += characters[Math.floor(Math.random() * characters.length)];
}
}
}
return transformed;
}
function startTransformationAnimation() {
iteration = 0;
clearInterval(interval);
interval = setInterval(() => {
iteration++;
const newLines = lines.map(line => transformLine(line, iteration, totalIterations));
projectOverviewEl.innerHTML = newLines.join('
');
if (iteration >= totalIterations) {
clearInterval(interval);
projectOverviewEl.innerHTML = originalHTML;
}
}, 60);
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
startTransformationAnimation();
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
observer.observe(projectOverviewEl);
});
Scroll Progress
A subtle scroll progress indicator at the top of the page shows readers how far they've scrolled through the content.
JavaScript
const scrollProgress = document.querySelector('.scroll-progress');
window.addEventListener('scroll', () => {
const scrollPosition = window.scrollY;
const totalHeight = document.body.scrollHeight - window.innerHeight;
const scrollPercentage = (scrollPosition / totalHeight) * 100;
scrollProgress.style.width = `${scrollPercentage}%`;
});
The CSS for the progress indicator:
CSS
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 6px;
background-color: var(--accent-primary);
width: 0%;
z-index: 100;
transition: width 0.1s ease-out;
}
Performance Optimizations
Several optimizations were implemented to ensure the website loads quickly and runs smoothly on all devices:
Lazy Loading: Images are lazy-loaded to improve initial page load time and reduce data usage.
Animation Throttling: Complex animations are disabled on mobile devices to improve performance.
Efficient Event Handling: Event listeners use passive options and requestAnimationFrame where appropriate to prevent jank.
Minimal Dependencies: The site has very few external dependencies, reducing download and parsing time.
CSS Variables: Using CSS variables for theming reduces stylesheet size and complexity.
JavaScript
function setupSidebar() {
if (window.innerWidth >= 768) {
let ticking = false;
function updateSidebarPosition() {
ticking = false;
}
function onScroll() {
if (!ticking) {
window.requestAnimationFrame(updateSidebarPosition);
ticking = true;
}
}
window.addEventListener('scroll', onScroll, { passive: true });
}
}
Explore the Code
Check out the complete source code for this portfolio website on GitHub:
View on GitHub