Add slower smooth scrolling and scroll to explore indicator
All checks were successful
build-website / build (push) Successful in 1m30s
All checks were successful
build-website / build (push) Successful in 1m30s
Features added: - Custom smooth scroll composable with adjustable duration (1.5 seconds) - Replaced native browser smooth scroll with custom implementation - Added 'Scroll to explore' text with chevron at bottom of hero - Animated bounce effect for scroll indicator - Chevron icon with subtle animation - Click handler to scroll to services section - Hidden on mobile for cleaner experience All anchor links now scroll more slowly and smoothly for better UX.
This commit is contained in:
@@ -36,7 +36,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useSmoothScroll } from '~/composables/useSmoothScroll'
|
||||||
|
|
||||||
|
const { scrollToElement, scrollToTop: smoothScrollToTop } = useSmoothScroll()
|
||||||
const isScrolled = ref(false)
|
const isScrolled = ref(false)
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
@@ -62,17 +64,12 @@ const handleSmoothScroll = (event: Event, href: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = document.querySelector(href)
|
|
||||||
if (!target) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
scrollToElement(href, 1500) // 1.5 seconds for slower scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
smoothScrollToTop(1500) // 1.5 seconds for slower scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -65,6 +65,12 @@
|
|||||||
View Our Services
|
View Our Services
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scroll to explore indicator -->
|
||||||
|
<div class="scroll-to-explore" @click="handleScrollToExplore">
|
||||||
|
<span>Scroll to explore</span>
|
||||||
|
<LucideChevronDown class="chevron-icon" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -74,7 +80,9 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useParallax } from '~/composables/useParallax'
|
import { useParallax } from '~/composables/useParallax'
|
||||||
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
|
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
|
||||||
import { useRipple } from '~/composables/useRipple'
|
import { useRipple } from '~/composables/useRipple'
|
||||||
|
import { useSmoothScroll } from '~/composables/useSmoothScroll'
|
||||||
|
|
||||||
|
const { scrollToElement } = useSmoothScroll()
|
||||||
const videoLoaded = ref(false)
|
const videoLoaded = ref(false)
|
||||||
const videoContainer = ref<HTMLElement | null>(null)
|
const videoContainer = ref<HTMLElement | null>(null)
|
||||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||||
@@ -93,10 +101,11 @@ const handlePhoneClick = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleServicesClick = () => {
|
const handleServicesClick = () => {
|
||||||
const element = document.querySelector('#services')
|
scrollToElement('#services', 1500) // 1.5 seconds for slower scrolling
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleScrollToExplore = () => {
|
||||||
|
scrollToElement('#services', 1500) // Scroll to services section
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleVideoVisibility = () => {
|
const handleVideoVisibility = () => {
|
||||||
@@ -127,10 +136,7 @@ const initSmoothScroll = () => {
|
|||||||
if (!href) {
|
if (!href) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const target = document.querySelector(href)
|
scrollToElement(href, 1500) // 1.5 seconds for slower scrolling
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,4 +216,67 @@ onMounted(() => {
|
|||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
max-width: 100vw !important;
|
max-width: 100vw !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scroll to explore indicator */
|
||||||
|
.scroll-to-explore {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-explore:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-explore span {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
animation: chevronBounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateX(-50%) translateY(-10px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateX(-50%) translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chevronBounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide on mobile for better UX */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scroll-to-explore {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
48
apps/website/composables/useSmoothScroll.ts
Normal file
48
apps/website/composables/useSmoothScroll.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export const useSmoothScroll = () => {
|
||||||
|
const smoothScrollTo = (target: number | Element, duration: number = 1200) => {
|
||||||
|
const targetPosition = typeof target === 'number'
|
||||||
|
? target
|
||||||
|
: target.getBoundingClientRect().top + window.pageYOffset
|
||||||
|
const startPosition = window.pageYOffset
|
||||||
|
const distance = targetPosition - startPosition
|
||||||
|
let startTime: number | null = null
|
||||||
|
|
||||||
|
const animation = (currentTime: number) => {
|
||||||
|
if (startTime === null) startTime = currentTime
|
||||||
|
const timeElapsed = currentTime - startTime
|
||||||
|
const progress = Math.min(timeElapsed / duration, 1)
|
||||||
|
|
||||||
|
// Easing function for smooth deceleration
|
||||||
|
const easeInOutCubic = (t: number) => {
|
||||||
|
return t < 0.5
|
||||||
|
? 4 * t * t * t
|
||||||
|
: 1 - Math.pow(-2 * t + 2, 3) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
window.scrollTo(0, startPosition + distance * easeInOutCubic(progress))
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToElement = (selector: string, duration: number = 1200) => {
|
||||||
|
const element = document.querySelector(selector)
|
||||||
|
if (element) {
|
||||||
|
smoothScrollTo(element, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToTop = (duration: number = 1200) => {
|
||||||
|
smoothScrollTo(0, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
smoothScrollTo,
|
||||||
|
scrollToElement,
|
||||||
|
scrollToTop
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user