FreshJuice DEV uses Alpine.js as its JavaScript framework, providing a lightweight and declarative way to add interactivity to your modules.
JavaScript Architecture
The theme’s JavaScript is organized in the source/js/ directory:
source/js/
├── main.js # Main entry point
├── modules/
│ ├── _debugLog.js # Debug logging utility
│ ├── _loadScript.js # Dynamic script loader
│ ├── _loadStylesheet.js # Dynamic stylesheet loader
│ ├── _getSearchParams.js # URL parameter utility
│ └── Alpine.data/
│ └── DOM.js # Alpine.js data component
Main JavaScript File
The main.js file is the entry point for all JavaScript:
import debugLog from './modules/_debugLog' ;
import loadScript from './modules/_loadScript' ;
import flyingPages from "flying-pages-module" ;
import Alpine from "alpinejs" ;
import intersect from "@alpinejs/intersect" ;
import collapse from "@alpinejs/collapse" ;
import focus from "@alpinejs/focus" ;
import dataDOM from "./modules/Alpine.data/DOM" ;
window . Alpine = Alpine ;
// Add plugins to Alpine
Alpine . plugin ( intersect );
Alpine . plugin ( collapse );
Alpine . plugin ( focus );
Alpine . data ( "xDOM" , dataDOM );
// Start Alpine when the page is ready
domReady (() => {
Alpine . start ();
flyingPages ({
// Prefetch all pages by default
});
});
Alpine.js Basics
Alpine.js is a rugged, minimal framework for composing JavaScript behavior in your markup. Think of it as jQuery for the modern web.
Core Directives
x-data: Component State
Define reactive data for your component: < div x-data = "{ open: false }" >
< button @click = "open = !open" > Toggle </ button >
< div x-show = "open" > Content </ div >
</ div >
x-show: Conditional Display
Show/hide elements based on state: < div x-data = "{ visible: true }" >
< div x-show = "visible" > I'm visible! </ div >
< button @click = "visible = !visible" > Toggle </ button >
</ div >
@click: Event Handling
Handle user interactions: < button @click = "count++" x-data = "{ count: 0 }" >
Clicked < span x-text = "count" ></ span > times
</ button >
x-text: Text Content
Bind text content to data: < div x-data = "{ message: 'Hello World' }" >
< p x-text = "message" ></ p >
</ div >
Real-World Examples
Tabs Component
Here’s how the Tabs module uses Alpine.js:
theme/modules/tabs.module/module.html
< div
x-data = "{
selectedId: null,
init() { this.$nextTick(() => this.select(this.$id('tab', 1))) },
select(id) { this.selectedId = id },
isSelected(id) { return this.selectedId === id },
whichChild(el, parent) { return Array.from(parent.children).indexOf(el) + 1 }
}"
x-id = "['tab']" >
< ul
x-ref = "tablist"
@keydown.right.prevent.stop = "$focus.wrap().next()"
@keydown.left.prevent.stop = "$focus.wrap().prev()"
role = "tablist" >
{% for item in module.tabs %}
< li >
< button
:id = "$id('tab', whichChild($el.parentElement, $refs.tablist))"
@click = "select($el.id)"
@focus = "select($el.id)"
:tabindex = "isSelected($el.id) ? 0 : -1"
:aria-selected = "isSelected($el.id)"
:class = "isSelected($el.id) ? 'border-gray-200 bg-cursor' : 'border-transparent'"
class = "inline-flex rounded-t-md border-t border-l border-r px-5 py-2.5"
type = "button"
role = "tab" >
{{ item.tab_name }}
</ button >
</ li >
{% endfor %}
</ ul >
< div role = "tabpanels" >
{% for item in module.tabs %}
< section
x-show = "isSelected($id('tab', whichChild($el, $el.parentElement)))"
:aria-labelledby = "$id('tab', whichChild($el, $el.parentElement))"
role = "tabpanel" >
< div class = "prose max-w-full" >
{{ item.tab_content }}
</ div >
</ section >
{% endfor %}
</ div >
</ div >
Key features:
x-data defines component state and methods
init() runs when component initializes
x-ref creates references to elements
:class binds classes dynamically
@click and @focus handle user events
$id() generates unique IDs for accessibility
Accordion Component
Create an expandable accordion:
< div x-data = "{ activeId: null }" >
{% for item in module.accordion_items %}
< div class = "border-b" >
< button
@click = "activeId = (activeId === {{ loop.index }}) ? null : {{ loop.index }}"
class = "w-full flex justify-between items-center py-4 text-left" >
< span class = "font-semibold" > {{ item.title }} </ span >
< svg
:class = "activeId === {{ loop.index }} ? 'rotate-180' : ''"
class = "w-5 h-5 transition-transform"
fill = "none"
stroke = "currentColor"
viewBox = "0 0 24 24" >
< path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M19 9l-7 7-7-7" ></ path >
</ svg >
</ button >
< div
x-show = "activeId === {{ loop.index }}"
x-collapse
class = "pb-4" >
{{ item.content }}
</ div >
</ div >
{% endfor %}
</ div >
Modal/Dialog
Create a modal dialog:
< div x-data = "{ open: false }" >
<!-- Trigger button -->
< button @click = "open = true" class = "bg-terminal text-cursor px-6 py-2 rounded-md" >
Open Modal
</ button >
<!-- Modal overlay -->
< div
x-show = "open"
@click = "open = false"
x-transition:enter = "transition ease-out duration-300"
x-transition:enter-start = "opacity-0"
x-transition:enter-end = "opacity-100"
x-transition:leave = "transition ease-in duration-200"
x-transition:leave-start = "opacity-100"
x-transition:leave-end = "opacity-0"
class = "fixed inset-0 bg-black bg-opacity-50 z-40" ></ div >
<!-- Modal content -->
< div
x-show = "open"
@click.outside = "open = false"
x-transition:enter = "transition ease-out duration-300"
x-transition:enter-start = "opacity-0 translate-y-4"
x-transition:enter-end = "opacity-100 translate-y-0"
x-transition:leave = "transition ease-in duration-200"
x-transition:leave-start = "opacity-100 translate-y-0"
x-transition:leave-end = "opacity-0 translate-y-4"
class = "fixed inset-0 z-50 flex items-center justify-center p-4" >
< div class = "bg-white rounded-lg shadow-xl max-w-md w-full p-6" >
< h2 class = "text-2xl font-bold mb-4" > Modal Title </ h2 >
< p class = "mb-4" > Modal content goes here. </ p >
< button @click = "open = false" class = "bg-terminal text-cursor px-4 py-2 rounded-md" >
Close
</ button >
</ div >
</ div >
</ div >
Alpine.js Plugins
FreshJuice DEV includes several Alpine.js plugins:
Intersect Plugin
Detect when elements enter/leave viewport:
< div
x-data = "{ visible: false }"
x-intersect = "visible = true"
:class = "visible ? 'opacity-100' : 'opacity-0'"
class = "transition-opacity duration-1000" >
Fades in when scrolled into view
</ div >
Collapse Plugin
Smooth height transitions:
< div x-data = "{ expanded: false }" >
< button @click = "expanded = !expanded" > Toggle </ button >
< div x-show = "expanded" x-collapse >
This content smoothly expands and collapses
</ div >
</ div >
Focus Plugin
Manage focus within components:
< div
x-data = "{ open: false }"
@keydown.escape = "open = false" >
< button @click = "open = true" > Open Menu </ button >
< nav
x-show = "open"
@keydown.tab = "$focus.wrap()" >
< a href = "#" > Link 1 </ a >
< a href = "#" > Link 2 </ a >
< a href = "#" > Link 3 </ a >
</ nav >
</ div >
Module-Specific JavaScript
For module-specific JavaScript, create a module.js file in your module directory:
theme/modules/my-module.module/module.js
// This runs when the module is loaded
document . addEventListener ( 'DOMContentLoaded' , function () {
const elements = document . querySelectorAll ( '.my-module' );
elements . forEach ( element => {
// Initialize your module
});
});
Modules with JavaScript files will load that JavaScript on every page where the module appears. Keep module JS minimal and consider using Alpine.js instead.
Utility Functions
Debug Logging
import debugLog from './modules/_debugLog' ;
debugLog ( 'Debug message' , { data: 'value' });
Loading External Scripts
import loadScript from './modules/_loadScript' ;
loadScript ( 'https://example.com/script.js' , 'async' , () => {
console . log ( 'Script loaded!' );
});
Getting URL Parameters
import getSearchParams from './modules/_getSearchParams' ;
const params = getSearchParams ();
console . log ( params . utm_source ); // Access URL parameters
Custom Alpine Components
Create reusable Alpine components:
source/js/modules/Alpine.data/MyComponent.js
export default () => ({
count: 0 ,
increment () {
this . count ++ ;
} ,
decrement () {
this . count -- ;
} ,
reset () {
this . count = 0 ;
}
}) ;
Register it in main.js:
import myComponent from './modules/Alpine.data/MyComponent' ;
Alpine . data ( 'myComponent' , myComponent );
Use it in your templates:
< div x-data = "myComponent" >
< p x-text = "count" ></ p >
< button @click = "increment" > + </ button >
< button @click = "decrement" > - </ button >
< button @click = "reset" > Reset </ button >
</ div >
Flying Pages (Prefetching)
FreshJuice DEV includes Flying Pages for intelligent page prefetching:
flyingPages ({
delay: 0 , // Prefetch delay in ms
ignoreKeywords: [], // URLs to ignore
maxRPS: 3 , // Max requests per second
hoverDelay: 50 // Delay before prefetch on hover
});
Lazy Loading
Lazy load heavy components:
< div
x-data = "{ loaded: false }"
x-intersect.once = "loaded = true" >
< template x-if = "loaded" >
<!-- Heavy component loads only when visible -->
< div > Heavy content here </ div >
</ template >
</ div >
Best Practices
Keep It Declarative Use Alpine.js directives in HTML rather than imperative JavaScript when possible.
Minimal Module JS Avoid adding JavaScript to module.js files. Use Alpine.js in templates instead.
Accessibility First Use proper ARIA attributes and keyboard navigation in interactive components.
Progressive Enhancement Ensure core functionality works without JavaScript when possible.
Debugging
Install the Alpine.js Devtools browser extension:
Console Access
Alpine is exposed on window.Alpine for debugging:
// Access Alpine from console
window . Alpine . version
// Inspect component state
$data // In browser console
Next Steps
Creating Modules Learn how to create custom HubSpot modules
Styling Components Style your components with Tailwind CSS
Alpine.js Docs Official Alpine.js documentation
Module Reference Browse all available modules