This code creates a visually engaging curved navigation. The JavaScript-based animation smoothly transitions between navigation items. Helpful for adding an eye-catching interactive element to your webpage.
The core functionality involves smoothly transitioning between navigation items as the user interacts with the curved menu. This code helps create engaging and interactive user interfaces, adding a touch of creativity to website navigation.
How to Create Animated Curved Nav Using Javascript
1. First of all, include the Veloxi library by adding the following CDN link to your HTML’s <head>
section:
<script src='https://unpkg.com/veloxi/dist/veloxi.min.js'></script>
2. Next, structure your HTML with a container for the curved navigation and placeholders for content and navigation items:
<div class="curved-nav"> <div class="content-wrapper"> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > A </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > B </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > C </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > D </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > E </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > F </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > G </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > H </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > I </div> <div class="content-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="contentItem" > J </div> </div> <div class="nav-container-wrapper"> <div class="nav-container"> <div class="nav-items" data-vel-plugin="CurvedNavPlugin" data-vel-view="itemsContainer" data-vel-data-active-index="0" > <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > A </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > B </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > C </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > D </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > E </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > F </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > G </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > H </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > I </div> <div class="nav-item" data-vel-plugin="CurvedNavPlugin" data-vel-view="item" > J </div> </div> </div> </div> </div>
To add your own content and navigation items, modify the HTML structure within the content-wrapper and nav-container sections. Adjust the text, images, or other elements to suit your website’s needs.
3. Apply basic styling to create a visually appealing layout. Customize the colors, sizes, and other styling properties as needed. The following CSS code includes styles for the curved effect and responsiveness.
* { box-sizing: border-box; } body { margin: 0; padding: 0; display: flex; align-items: center; justify-content: center; width: 100%; height: 100vh; background: whitesmoke; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; overflow: hidden; max-width: 100%; } .curved-nav { width: 100%; height: 100%; position: relative; will-change: transform, opacity; } .curved-nav::before { position: absolute; content: ''; width: 100%; aspect-ratio: 1; border-radius: 20rem; background: radial-gradient( 63.62% 69.52% at 100% 0%, rgba(247, 214, 98, 0.8) 0%, rgba(247, 214, 98, 0.168) 52.08%, rgba(247, 214, 98, 0) 100% ), linear-gradient( 208.42deg, #f0422a 7.46%, rgba(240, 88, 42, 0.18) 42.58%, rgba(240, 101, 42, 0) 64.13% ), radial-gradient( 114.51% 122.83% at 0% -15.36%, #e74f6a 0%, rgba(231, 79, 106, 0.22) 66.72%, rgba(231, 79, 106, 0) 100% ), linear-gradient( 333.95deg, rgba(83, 208, 236, 0.85) -7.76%, rgba(83, 208, 236, 0.204) 19.67%, rgba(138, 137, 190, 0) 35.42% ), radial-gradient( 109.15% 148.57% at 4.46% 98.44%, #1b3180 0%, rgba(27, 49, 128, 0) 100% ), linear-gradient(141.57deg, #4eadeb 19.08%, rgba(78, 173, 235, 0) 98.72%); background-blend-mode: normal, normal, normal, normal, multiply, normal; filter: blur(84px); will-change: transform; backface-visibility: hidden; transform: translate3d(0, 0, 0); } .nav-container { display: flex; width: 100%; max-width: 375px; touch-action: none; will-change: transform, opacity; } .nav-container-wrapper { overflow: hidden; padding: 40px 50px; } .nav-items { display: flex; width: 100%; --item-size: calc(375px / 5); } .nav-item { width: var(--item-size); height: var(--item-size); border-radius: 10px; background: white; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; color: #222; user-select: none; -webkit-user-select: none; flex-shrink: 0; will-change: transform, opacity; touch-action: none; } .curved-nav { width: 100%; max-width: 375px; margin: 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; } .content-wrapper { border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 10px; width: 250px; height: 250px; display: flex; align-items: center; justify-content: center; will-change: transform, opacity; } .content-item { color: white; font-size: 300px; line-height: 0.7; position: absolute; will-change: opacity; } @media (max-width: 500px) { .nav-items { --item-size: calc(375px / 6); } .curved-nav { overflow: hidden; } }
4. Finally, add the following JavaScript function between <script> tag or external JS file. This code leverages the Veloxi library to create the animated curved navigation. Ensure the Veloxi library is properly loaded in your HTML file.
// Built with Veloxi: https://veloxijs.com/ // Inspired by: https://twitter.com/huseyingayiran/status/1711294397009080397 const { EventBus, Events, Plugin, createApp, DragEventPlugin, DragEvent } = Veloxi const OFFSET = 10 class SetActiveIndexEvent { index constructor({ index }) { this.index = index } } class NavItem { view index container initialized = false constructor(view, index, container) { this.view = view this.index = index this.container = container this.view.position.animator.set('dynamic', { speed: 5 }) this.view.scale.animator.set('dynamic', { speed: 5 }) } init() { requestAnimationFrame(() => { this.enableTransition() }) this.initialized = true } enableTransition() { this.view.styles.transition = '0.2s opacity linear' } update() { this.updatePosition() this.updateOpacity() this.updateScale() } updateWithOffset(offset) { const targetSlot = offset > 0 ? this.nextSlot : this.previousSlot const percentage = Math.abs(offset / this.container.stepSize) this.updatePositionWithOffset(offset, targetSlot, percentage) this.updateOpacityWithPercentage(targetSlot, percentage) this.updateScaleWithPercentage(targetSlot, percentage) } updatePositionWithOffset(offset, targetSlot, percentage) { const x = this.currentPosition.x let y = this.currentPosition.y switch (targetSlot) { case slotIndex.ACTIVE: y -= 10 * percentage break case slotIndex.FIRST_NEXT: case slotIndex.FIRST_PREVIOUS: const fromActive = this.slotIndex === slotIndex.ACTIVE y += 25 * (fromActive ? 1 : -1) * percentage break case slotIndex.SECOND_NEXT: case slotIndex.SECOND_PREVIOUS: const fromFirst = [ slotIndex.FIRST_NEXT, slotIndex.FIRST_PREVIOUS ].includes(this.slotIndex) y += 40 * percentage * (fromFirst ? 1 : -1) break default: const fromSecond = [ slotIndex.SECOND_NEXT, slotIndex.SECOND_PREVIOUS ].includes(this.slotIndex) y += 40 * percentage * (fromSecond ? 1 : -1) } this.view.position.set({ x: x + offset, y }) } updateScaleWithPercentage(targetSlot, percentage) { let scale = this.currentScale switch (targetSlot) { case slotIndex.ACTIVE: scale += 0.1 * percentage break case slotIndex.FIRST_NEXT: case slotIndex.FIRST_PREVIOUS: const fromActive = this.slotIndex === slotIndex.ACTIVE if (fromActive) { scale -= 0.1 * percentage } break } this.view.scale.set({ x: scale, y: scale }) } updateOpacityWithPercentage(targetSlot, percentage) { let opacity = this.currentOpacity switch (targetSlot) { case slotIndex.ACTIVE: opacity += 0.2 * percentage break case slotIndex.FIRST_PREVIOUS: case slotIndex.FIRST_NEXT: const fromActive = this.slotIndex === slotIndex.ACTIVE opacity += 0.2 * percentage * (fromActive ? -1 : 1) break case slotIndex.SECOND_PREVIOUS: case slotIndex.SECOND_NEXT: const fromFirst = [ slotIndex.FIRST_NEXT, slotIndex.FIRST_PREVIOUS ].includes(this.slotIndex) if (!fromFirst) { if ( this.firstItemIndexInStart === this.index || this.firstItemIndexInEnd === this.index ) { opacity += 0.2 * percentage * (fromFirst ? -1 : 1) } else { opacity = 0 } } else { opacity += 0.2 * percentage * (fromFirst ? -1 : 1) } break default: const fromSecond = [ slotIndex.SECOND_NEXT, slotIndex.SECOND_PREVIOUS ].includes(this.slotIndex) if (fromSecond) { opacity += 0.4 * percentage * (fromSecond ? -1 : 1) } else { opacity = 0 } } this.view.styles.opacity = `${opacity}` } updatePosition() { const shouldAnimate = this.container.shouldAnimateMap.get(this.index) const x = this.slotPosition.x let y = this.slotPosition.y switch (this.slotIndex) { case slotIndex.ACTIVE: break case slotIndex.FIRST_NEXT: case slotIndex.FIRST_PREVIOUS: y += 10 break case slotIndex.SECOND_NEXT: case slotIndex.SECOND_PREVIOUS: y += 40 break default: y += 100 } this.view.position.set({ x, y }, shouldAnimate) } get currentPosition() { const x = this.slotPosition.x let y = this.slotPosition.y switch (this.slotIndex) { case slotIndex.ACTIVE: break case slotIndex.FIRST_NEXT: case slotIndex.FIRST_PREVIOUS: y += 10 break case slotIndex.SECOND_NEXT: case slotIndex.SECOND_PREVIOUS: y += 40 break default: y += 80 } return { x, y } } get currentOpacity() { let opacity = 0 switch (this.slotIndex) { case slotIndex.ACTIVE: opacity = 1 break case slotIndex.FIRST_PREVIOUS: case slotIndex.FIRST_NEXT: opacity = 0.425 break case slotIndex.SECOND_PREVIOUS: case slotIndex.SECOND_NEXT: opacity = 0.2 break } return opacity } get currentScale() { let scale = 0.75 switch (this.slotIndex) { case slotIndex.ACTIVE: scale = 1 break case slotIndex.FIRST_PREVIOUS: case slotIndex.FIRST_NEXT: scale = 0.75 break case slotIndex.SECOND_PREVIOUS: case slotIndex.SECOND_NEXT: scale = 0.75 break } return scale } updateOpacity() { let opacity = 0 switch (this.slotIndex) { case slotIndex.ACTIVE: opacity = 1 break case slotIndex.FIRST_PREVIOUS: case slotIndex.FIRST_NEXT: opacity = 0.425 break case slotIndex.SECOND_PREVIOUS: case slotIndex.SECOND_NEXT: opacity = 0.2 break } this.view.styles.opacity = `${opacity}` } updateScale() { let scale = 0.75 switch (this.slotIndex) { case slotIndex.ACTIVE: scale = 1 break case slotIndex.FIRST_PREVIOUS: case slotIndex.FIRST_NEXT: scale = 0.75 break case slotIndex.SECOND_PREVIOUS: case slotIndex.SECOND_NEXT: scale = 0.75 break } this.view.scale.set({ x: scale, y: scale }, this.initialized) } get nextSlot() { return wrapAround(this.slotIndex, Object.keys(slotIndex).length, 1) } get previousSlot() { return wrapAround(this.slotIndex, Object.keys(slotIndex).length, -1) } get slotPosition() { return this.container.getSlotPositionForItemIndex(this.index) } get activeIndex() { return this.container.activeIndex } get slotIndex() { return this.container.getSlotForIndex(this.index) } getItemIndexForSlot(slot) { return this.container.getItemIndeciesForSlot(slot)[0] } get firstItemIndexInStart() { const secondPreviousIndex = this.getItemIndexForSlot( slotIndex.SECOND_PREVIOUS ) return wrapAround(secondPreviousIndex, this.container.totalItems, -1) } get firstItemIndexInEnd() { const secondNextItemIndex = this.getItemIndexForSlot( slotIndex.SECOND_NEXT ) return wrapAround(secondNextItemIndex, this.container.totalItems, 1) } } const slotIndex = { START: 0, SECOND_PREVIOUS: 1, FIRST_PREVIOUS: 2, ACTIVE: 3, FIRST_NEXT: 4, SECOND_NEXT: 5, END: 6 } function wrapAround(current, total, amount) { return (current + total + amount) % total } function flipMap(map) { const flippedMap = new Map() for (const [key, value] of map) { if (!flippedMap.has(value)) { flippedMap.set(value, [key]) } else { flippedMap.get(value).push(key) } } return flippedMap } class NavContainer { plugin view activeIndex shouldAnimateMap = new Map() _itemIndexSlotMap = new Map() _slotItemIndexMap = new Map() constructor(plugin, view) { this.plugin = plugin this.view = view this.activeIndex = this.view.data.activeIndex ? parseInt(this.view.data.activeIndex) : 0 for (let index = 0; index < this.totalItems; index++) { this.shouldAnimateMap.set(index, false) } } get stepSize() { return this.plugin.stepSize } get itemSize() { return this.plugin.itemSize } get totalItems() { return this.plugin.totalItems } updateWithOffset(offset) { const steps = Math.floor(Math.abs(offset / this.stepSize)) const queue = [] let currentIndex = this.activeIndex for (let step = 0; step < steps; step++) { const stepDirection = offset < 0 ? 1 : -1 const itemIndex = wrapAround( currentIndex, this.totalItems, stepDirection ) queue.push(itemIndex) currentIndex = itemIndex } queue.forEach((itemIndex, index) => { setTimeout(() => { this.plugin.setActiveIndex(itemIndex) }, 100 * index) }) } setActiveIndex(newActiveIndex) { const previousItemIndexSlot = this.itemIndexSlotMap this.activeIndex = newActiveIndex this.setItemIndexSlotMap() const newItemIndexSlot = this.itemIndexSlotMap const visibleSlots = [ slotIndex.ACTIVE, slotIndex.FIRST_PREVIOUS, slotIndex.SECOND_PREVIOUS, slotIndex.FIRST_NEXT, slotIndex.SECOND_NEXT ] for (let index = 0; index < this.totalItems; index++) { const shouldAnimate = visibleSlots.includes(previousItemIndexSlot.get(index)) || visibleSlots.includes(newItemIndexSlot.get(index)) this.shouldAnimateMap.set(index, shouldAnimate) } } get slotItemIndexMap() { return this._slotItemIndexMap } get itemIndexSlotMap() { return this._itemIndexSlotMap } getSlotForIndex(itemIndex) { return this.itemIndexSlotMap.get(itemIndex) } getItemIndeciesForSlot(slot) { return this.slotItemIndexMap.get(slot) } setItemIndexSlotMap() { this._itemIndexSlotMap.clear() const activeIndex = this.activeIndex const firstPreviousIndex = wrapAround(activeIndex, this.totalItems, -1) const secondPreviousIndex = wrapAround(activeIndex, this.totalItems, -2) const firstNextIndex = wrapAround(activeIndex, this.totalItems, 1) const secondNextIndex = wrapAround(activeIndex, this.totalItems, 2) for (let index = 0; index < this.totalItems; index++) { if (index === activeIndex) { this._itemIndexSlotMap.set(index, slotIndex.ACTIVE) continue } if (index === firstPreviousIndex) { this._itemIndexSlotMap.set(index, slotIndex.FIRST_PREVIOUS) continue } if (index === secondPreviousIndex) { this._itemIndexSlotMap.set(index, slotIndex.SECOND_PREVIOUS) continue } if (index === firstNextIndex) { this._itemIndexSlotMap.set(index, slotIndex.FIRST_NEXT) continue } if (index === secondNextIndex) { this._itemIndexSlotMap.set(index, slotIndex.SECOND_NEXT) continue } if (index === wrapAround(secondNextIndex, this.totalItems, 1)) { this._itemIndexSlotMap.set(index, slotIndex.END) continue } if (index === wrapAround(secondNextIndex, this.totalItems, 2)) { this._itemIndexSlotMap.set(index, slotIndex.END) continue } if (index === wrapAround(secondPreviousIndex, this.totalItems, -1)) { this._itemIndexSlotMap.set(index, slotIndex.START) continue } if (index === wrapAround(secondPreviousIndex, this.totalItems, -2)) { this._itemIndexSlotMap.set(index, slotIndex.START) continue } if (index > activeIndex) { this._itemIndexSlotMap.set(index, slotIndex.END) continue } if (index < activeIndex) { this._itemIndexSlotMap.set(index, slotIndex.START) continue } } this._slotItemIndexMap = flipMap(this.itemIndexSlotMap) } getSlotPositionForItemIndex(index) { const slot = this.itemIndexSlotMap.get(index) return this.slotPositions[slot] } get indicatorPosition() { return { x: this.view.position.x + this.view.size.width / 2, y: this.view.position.y + this.view.size.height / 2 } } get slotPositions() { const result = [] // Active Slot result[slotIndex.ACTIVE] = { x: this.indicatorPosition.x - this.itemSize / 2, y: this.indicatorPosition.y - this.itemSize / 2 } result[slotIndex.FIRST_PREVIOUS] = { x: result[slotIndex.ACTIVE].x - OFFSET - this.itemSize, y: result[slotIndex.ACTIVE].y } result[slotIndex.SECOND_PREVIOUS] = { x: result[slotIndex.FIRST_PREVIOUS].x - this.itemSize, y: result[slotIndex.FIRST_PREVIOUS].y } result[slotIndex.START] = { x: result[slotIndex.SECOND_PREVIOUS].x - this.itemSize, y: result[slotIndex.SECOND_PREVIOUS].y } result[slotIndex.FIRST_NEXT] = { x: result[slotIndex.ACTIVE].x + OFFSET + this.itemSize, y: result[slotIndex.ACTIVE].y } result[slotIndex.SECOND_NEXT] = { x: result[slotIndex.FIRST_NEXT].x + this.itemSize, y: result[slotIndex.FIRST_NEXT].y } result[slotIndex.END] = { x: result[slotIndex.SECOND_NEXT].x + this.itemSize, y: result[slotIndex.SECOND_NEXT].y } return result } } class ContentItem { view index navItemsContainer constructor(view, index, navItemsContainer) { this.view = view this.index = index this.navItemsContainer = navItemsContainer this.init() this.update() } init() { requestAnimationFrame(() => { this.enableTransition() }) } enableTransition() { this.view.styles.transition = '0.2s ease-in-out opacity' } update() { if (this.index !== this.activeIndex) { this.hide() } else { this.show() } } hide() { this.view.styles.opacity = '0' } show() { this.view.styles.opacity = '1' } get activeIndex() { return this.navItemsContainer.activeIndex } } class CurvedNavPlugin extends Plugin { static pluginName = 'CurvedNavPlugin' items itemsContainer contentItems lastDragOffset = 0 isDragging = false dragEventPlugin = this.useEventPlugin(DragEventPlugin) setup() { const itemViews = this.getViews('item') const itemsContainerView = this.getView('itemsContainer') this.itemsContainer = new NavContainer(this, itemsContainerView) this.dragEventPlugin.addView(itemsContainerView) this.dragEventPlugin.on(DragEvent, this.onDrag.bind(this)) this.items = itemViews.map( (view, index) => new NavItem(view, index, this.itemsContainer) ) this.itemsContainer.setItemIndexSlotMap() this.items.forEach((item) => { item.update() item.init() }) this.contentItems = this.getViews('contentItem').map( (view, index) => new ContentItem(view, index, this.itemsContainer) ) } onDrag(event) { if (event.isDragging) { this.isDragging = true } else { requestAnimationFrame(() => { this.isDragging = false }) } if (event.isDragging) { const diff = Math.abs(event.previousX - event.x) const damping = diff > 50 ? 0.2 : 1 const offset = damping * (event.width + this.lastDragOffset * -1) if (Math.abs(offset) >= this.stepSize) { this.lastDragOffset = event.width } this.itemsContainer.updateWithOffset(offset) this.items.forEach((item) => { item.updateWithOffset(offset) }) } else { this.lastDragOffset = 0 this.items.forEach((item) => { item.updateWithOffset(0) }) } } onDataChanged(data) { if (data.dataName === 'activeIndex') { const activeIndex = parseInt(data.dataValue) this.itemsContainer.setActiveIndex(activeIndex) this.items.forEach((item) => item.update()) this.contentItems.forEach((item) => item.update()) } } subscribeToEvents(eventBus) { eventBus.subscribeToEvent(Events.PointerClickEvent, ({ target }) => { if (this.isDragging) return this.items.forEach((item, index) => { if (target === item.view.element) { this.setActiveIndex(index) } }) }) } setActiveIndex(index) { this.emit(SetActiveIndexEvent, { index }) } get totalItems() { return this.getViews('item').length } get itemSize() { return this.items[0].view.size.width } get stepSize() { return this.itemSize + OFFSET } } const app = createApp() app.addPlugin(CurvedNavPlugin) app.run() const containerNav = document.querySelector( '[data-vel-view="itemsContainer"]' ) app.onPluginEvent(CurvedNavPlugin, SetActiveIndexEvent, ({ index }) => { containerNav.dataset.velDataActiveIndex = `${index}` })
That’s all! hopefully, you have successfully created an animated curved nav using JavaScript. If you have any questions or suggestions, feel free to comment below.
Similar Code Snippets:
I code and create web elements for amazing people around the world. I like work with new people. New people new Experiences.
I truly enjoy what I’m doing, which makes me more passionate about web development and coding. I am always ready to do challenging tasks whether it is about creating a custom CMS from scratch or customizing an existing system.