The app/browser directory is where JavaScript files for the browser live. These JavaScript files can import other modules from your project as well as any installed packages that can run in the browser. Files in the app/browser directory will be bundled to the /public/browser/ directory in your project and will be exposed the to browser at /_public/browser/ for loading by script tags.

<script type="module" src="/_public/browser/my-file.mjs"></script>

Add a browser bundle

Create a JavaScript file for the browser.

├── browser ........... browser JavaScript
│   └── index.mjs
└── pages ............. file-based routing
    └── index.html
const message = document.getElementById('message')
message.innerHTML = '👋 Hello from your bundle!'

Source a bundle in a page

Add a script tag and load it from /_public/browser/index.mjs

  <h1>My awesome page</h1>
  <p id="message"></p>
<script type="module" src="/_public/browser/index.mjs"></script>

The /_public endpoint is created for you so that Enhance can do the tedious work of replacing your authored file name with a fingerprinted one to avoid caching issues.

Share elements with the browser

Now that you get the basics of the browser bundle workflow let’s look at how you might share your elements with the browser for progressive enhancement.

Add a my-message element

Add the file app/elements/my-message.mjs

├── browser ........... browser JavaScript
│   └── index.mjs
├── elements .......... custom element pure functions
│   └── my-message.mjs
└── pages ............. file-based routing
    └── index.html

Write a custom element pure function

Write a pure function for returning the HTML markup for the my-message.mjs custom element.

export default function MyMessage({ html, state }) {
  const { attrs={} } = state
  const { message='' } = attrs

  return html`<h1>${ message }</h1>`

Reuse your pure function in the browser

Import the my-message.mjs element in the app/browser/index.mjs file to reuse your pure function in the browser.

import MyMessage from '../elements/my-message.mjs'

class MyMessageElement extends HTMLElement {
  constructor() {
    const templateID = `${this.tagName.toLowerCase()}-template`
    const template = document.getElementById(templateID)
    if (template) {
      this.template = template
    else {
      this.template = document.createElement('template')
      this.template.innerHTML = MyMessage({
         html: this.html,
         state: {}
      this.template.setAttribute('id', templateID)

    this.heading = this.querySelector('h1')

  html(strings, ...values) {
    return String.raw({ raw: strings }, ...values)

  static get observedAttributes() {
    return [ 'message' ]

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      if (name === 'message') {
        this.heading.textContent = newValue
customElements.define('my-message', MyMessageElement)

Using Enhance Element

Same thing as above but with @enhance/element

import enhance from '@enhance/element'
import MyMessage from '../elements/my-message.mjs'

enhance('my-message', {
  attrs: [ 'message' ],
  render: MyMessage

Add the script to your page

All that’s left now is to add a script tag to app/pages/index.html that sources your browser bundle.

<my-message message="Howdy!"></my-message>
<script type="module" src="/_public/browser/index.mjs"></script>

Changing the message attribute will trigger an update to your Custom Element.
Try it out in dev-tools.

That’s it

Now you can use this built-in pattern for sharing elements with the browser and progressively enhancing your pages with JavaScript.

Community Resources


Visit Enhance on GitHub.


Join our Discord server to chat about development and get help from the community.


Follow Enhance in the Fediverse. We're huge fans of the IndieWeb!