A minimalist, lightweight frontend framework for building reactive user interfaces.
Paje is a simple, vanilla JavaScript framework that brings reactive state management and virtual DOM rendering to your projects without unnecessary complexity. It's designed for developers who appreciate minimalism and want to understand how modern frameworks work under the hood.
Key Principles:
- 🎯 Minimalist - Only essential features, no bloat
- 📦 Lightweight - ~6.2KB minified JavaScript
- ⚡ Reactive - Automatic UI updates when state changes
- 🔧 Pure JavaScript - No special syntax or build step required
- 🎓 Educational - Learn how reactive frameworks work
✨ Virtual DOM - Efficient rendering through DOM abstraction
🔄 Reactive State Management - Automatic re-rendering on state changes
📡 Pub/Sub Event System - Simple action-based state updates
🏗️ Hyperscript API - Familiar h() function for element creation
🔌 Lifecycle Methods - mount() and unmount() for component control
🎯 Event Handling - Built-in DOM event listener management
🎨 Attribute & Style Management - Easy class, style, and data attribute handling
📄 Fragment Support - Render multiple elements without wrappers
npm install pajeimport { createPaje, h } from 'paje';
// Define your app
const app = createPaje(
// Initial state
{ count: 0 },
// View function - returns virtual DOM
(state, emit) => h('div', { class: 'app' }, [
h('h1', {}, [`Count: ${state.count}`]),
h('button', {
on: { click: () => emit('increment', {}) }
}, ['Increment']),
h('button', {
on: { click: () => emit('decrement', {}) }
}, ['Decrement']),
]),
// Reducers - pure functions that update state
{
increment: (state) => ({ ...state, count: state.count + 1 }),
decrement: (state) => ({ ...state, count: state.count - 1 }),
}
);
// Mount your app to the DOM
app.mount(document.body);The single source of truth for your application. It's a plain JavaScript object that contains all the data your UI needs.
const state = {
count: 0,
todos: [],
user: { name: 'John', email: 'john@example.com' }
};A pure function that takes state and an emit function, returning virtual DOM. This function is called whenever state changes.
(state, emit) => h('div', {}, [
h('p', {}, [`Hello, ${state.user.name}`]),
h('button', { on: { click: () => emit('greet', {}) } }, ['Greet']),
])Pure functions that take the current state and a payload, returning a new state object. They describe how to update state in response to actions.
{
greet: (state, payload) => ({
...state,
greeting: `Hello, ${state.user.name}!`
})
}The emit() function in your view dispatches actions that trigger reducers and update state. The app automatically re-renders when state changes.
h('button', {
on: { click: () => emit('actionName', { data: 'payload' }) }
}, ['Click me'])Creates and returns a Paje application instance.
Parameters:
state(Object) - Initial application stateview(Function) - Pure view function:(state, emit) => vnodereducers(Object) - Map of action names to reducer functions
Returns:
app(Object) - Application instance withmount()andunmount()methods
Example:
const app = createPaje(
{ count: 0 },
(state, emit) => h('div', {}, [`Count: ${state.count}`]),
{ increment: (state) => ({ ...state, count: state.count + 1 }) }
);Creates a virtual DOM element node.
Parameters:
tag(String) - HTML tag name (e.g., 'div', 'button', 'span')props(Object) - Element properties and event handlers- Standard HTML attributes:
id,class,data-*, etc. on(Object) - Event handlers:{ click: handler, input: handler }style(Object|String) - CSS styles
- Standard HTML attributes:
children(Array) - Array of child vnodes or strings
Returns: Virtual DOM element node
Example:
h('button', {
id: 'btn-submit',
class: 'primary-btn',
on: { click: handleSubmit }
}, ['Submit'])Creates a virtual DOM fragment for rendering multiple elements without a wrapper.
Parameters:
vnodes(Array) - Array of virtual DOM nodes
Returns: Virtual DOM fragment node
Example:
hFragment([
h('h2', {}, ['Title']),
h('p', {}, ['Content']),
])Creates a virtual DOM text node.
Parameters:
text(String) - Text content
Returns: Virtual DOM text node
Example:
h('span', {}, [hString('Hello, World!')])Mounts the application to a DOM element.
Parameters:
parentElement(HTMLElement) - DOM element to mount into
Example:
app.mount(document.body);
app.mount(document.getElementById('app'));Unmounts the application and cleans up all listeners and DOM nodes.
Example:
app.unmount();Check the examples/ directory for complete working examples:
A basic counter with increment and decrement buttons demonstrating state management and event handling.
npm run serve:examples
# Open http://localhost:8000/examples/simple-counter/counter.htmlA complete todo application with add, delete, and check functionality showing real-world patterns.
npm run serve:examples
# Open http://localhost:8000/examples/vanilla-todo-app/todo.htmlnpm run buildnpm run lint
npm run lint:fixnpm run test # Watch mode
npm run test:run # Single runThis project follows conventional commits. See docs/COMMIT_CONVENTIONS.md for details.
git commit -m "feat: add new feature"
git commit -m "fix: resolve bug"
git commit -m "docs: update documentation"Paje requires a modern browser with ES6 module support:
- Chrome 61+
- Firefox 67+
- Safari 11+
- Edge 79+
paje/
├── packages/runtime/ # Core framework package
│ ├── src/
│ │ ├── paje.js # Core createPaje function
│ │ ├── h.js # Hyperscript API
│ │ ├── dispatcher.js # Event pub/sub system
│ │ ├── mount-dom.js # Virtual to real DOM rendering
│ │ ├── destroy-dom.js # DOM cleanup
│ │ ├── attributes.js # Attribute/style/class management
│ │ ├── events.js # Event listener management
│ │ └── __tests__/ # Test files
│ ├── dist/ # Compiled bundle
│ └── package.json
├── examples/ # Example applications
│ ├── simple-counter/
│ └── vanilla-todo-app/
├── docs/ # Documentation
└── README.md
- Minified: ~6.2KB
- Gzipped: ~2.1KB
Perfect for projects where size matters!
MIT © 2025 theolodocoder
Contributions are welcome! Please follow the conventional commit guidelines and ensure tests pass.
npm run lint:fix
npm run test:run
git add .
git commit -m "feat: your feature description"Paje is perfect for:
- Learning how reactive frameworks work
- Building small to medium-sized applications
- Projects where bundle size is critical
- Teams that value simplicity and clarity
- Anyone who enjoys vanilla JavaScript
It's not ideal for:
- Large enterprise applications (consider React, Vue, or Angular)
- Projects requiring advanced features like routing or form libraries
- Apps needing TypeScript support (though possible, not built-in)
Q: Can I use Paje in production? A: Yes! It's a stable framework suitable for production use.
Q: Does Paje have TypeScript support? A: Not built-in, but you can use it with TypeScript through declaration files.
Q: How do I handle forms? A: Paje uses standard HTML form elements. Handle form state like any other state through reducers.
Q: Can I use external libraries? A: Yes! Paje plays well with other libraries. Use them alongside your Paje components.
Q: What about routing?
A: Paje doesn't include routing. Use a library like page.js or implement your own routing logic.
Q: How do I debug? A: Use browser DevTools. The virtual DOM nodes include helpful properties for debugging.
- 📚 Examples - Working examples to learn from
- 📖 Commit Conventions - Development guidelines
- 🧪 Tests - Test patterns and examples
Created by theolodocoder as a minimalist approach to frontend development.
Ready to build something amazing? Start with the Quick Start or check out the examples!