Easy reactive web framework with no setup required
<template>
<button @click="greet">Click</button>
</template>
<script>
export default class {
greet() {
alert('Welcome to Peak.js!')
}
}
</script>
<style>
button { font-size: large }
</style>- Reactive web framework based on Web Components
- No build step necessary
- Reusable single-file components
- Scoped CSS styles per component
- Optional url-based view routing
- Lightweight at ~5kb gzipped
- Support for server-side rendering (SSR)
- Optional Vite plugin for bundling and HMR
Components are defined in plain HTML files, with each file having a <template>, an optional <script>, and optional <style>.
<!-- components/x-counter.html -->
<template>
<button @click="increment">
<span x-text="count" />
</button>
</template>
<script>
export default class {
initialize() {
this.count = 0
}
increment() {
this.count++
}
}
</script>
<style>
button:active {
filter: invert(1);
}
</style>Register components and use them directly in markup:
<!-- index.html -->
<x-counter></x-counter>
<script type="module">
import { component } from '/vendor/peak.js'
component('x-counter', '/components/x-counter.html')
</script>Conditionally render a block
<img x-if="loading" src="spinner.svg">Also available are x-else and x-else-if
<template x-if="loading">
<img src="spinner.svg">
</template>
<template x-else-if="error">
<img src="error.svg">
</template>
<template x-else>
<x-content />
</template>Render some HTML for each item in an array
<ul>
<li x-for="item in items">
<span x-text="item.title" />
</li>
</ul>Set the text content of an element
<span x-text="`Hello, ${name}`" />Set the HTML content of an element
<div x-html="markdown.render('# Page title')"></div>Set the visibility of an element
<div x-show="open">Content...</div>Refer to an HTML element via $refs
<input x-ref="searchInput">
<button @click="$refs.searchInput.focus()">Search</button>Specify props using a static props array:
<template>
Greetings, <span x-text="name" />!
</template>
<script>
export default class {
static props = ['name']
}
</script>Run code when the component is initialized before mounted
<script>
export default class {
initialize() {
// initialize the component
this.pollerId = setInterval(_ => {
this.items = fetch('/feed')
}, 30_000)
}
}
</script>Run code when the component is mounted
Run teardown code when the component is to be destroyed
<script>
export default class {
initialize() {
// ...
}
teardown() {
// clean up when the component is destroyed
clearInterval(this.pollerId)
}
}
</script>Run methods when reactive data changes
<template>
<button @click="count++" x-text="count" />
</template>
<script>
export default class {
initialize() {
this.count = 0
this.$watch('count', () => {
console.log("count is now", this.count)
})
}
}
</script>Emit events that bubble up to parent components
<template>
<input @input="$emit('change')">
</template>Handle emitted events native and custom
initialize- component has been initialized but not yet mountedmounted- component has been mounted in the documentteardown- component is no longer mounted
Refer to the event being handled
<template>
<button @click="incrementBy(10)">Add 10</button>
</template>
<script>
export default class {
incrementBy(n) {
this.$event.stopPropagation()
this.count += n
}
}
</script>Refer to elements within the component by the name in their x-ref attribute
Use instance getters for display formatting, and other derived properties
<template>
<div x-text="formattedTime" />
</template>
<script>
export default class {
get formattedTime() {
return new this.time.toISOString()
}
created() {
this.time = new Date;
}
}
</script>Styles defined in the component are scoped to the component — they won't leak up to ancestor elements, nor down into nested components.
<template>
<h1>Article title</h1>
<p>This text in red won't leak to other components</p>
<x-body />
</template>
<style>
p { color: red }
</style>Peak comes with an optional built-in router. Register views to route patterns for integration with the History API. Views are just regular components, associated with a route.
<nav>
<a href="/">HOME</a>
<a href="/about">ABOUT</a>
</nav>
<x-router-view></x-router-view>
<script type="module">
import { router } from 'peak'
router.route('/', '/views/home.html')
router.route('/about', '/views/about.html')
router.on('navigation', e => console.log(e))
router.on('notFound', e => console.warn(e))
</script>Optionally, build with Vite in order to get HMR in dev, and bundling for production.
// vite.config.js
import { defineConfig } from 'vite'
import peakPlugin from 'peak/vite'
export default defineConfig({
plugins: [peakPlugin()]
})Make sure to import virtual:peak-components built by the vite build:
// index.html
<script type="module">
import "virtual:peak-components"
import { router } from './peak.js'
router.route('/', '/views/home.html')
</script>