A minimal library for building web applications with JSX and Web Components. It focuses on simplicity, providing just two core functions:
createElement: Creates virtual DOM elements using JSX.applyDiff: Efficiently applies changes to the real DOM by comparing virtual nodes.
There are a few examples on StackBlitz. If you're impatient (like me), that's probably the easiest way to get started.
- Todo List
- Rotten Tomatoes Mockup
- Boring Dashboard
- Vite Template - A Vite starter template with WebJSX and other presets. Example included.
Install webjsx via npm:
npm install webjsxWebJSX fully supports JSX syntax, allowing you to create virtual DOM elements using createElement and update the real DOM with applyDiff.
import*aswebjsxfrom"webjsx";// Define a simple virtual DOM element using JSXconstvdom=(<divid="main-container"><h1>Welcome to webjsx</h1><p>This is a simple example.</p></div>);// Select the container in the real DOMconstappContainer=document.getElementById("app");// Apply the virtual DOM diff to update the real DOMwebjsx.applyDiff(appContainer,vdom);Let's write a simple Custom Element with JSX.
import*aswebjsxfrom"webjsx";// Define a custom Web ComponentclassMyElementextendsHTMLElement{staticgetobservedAttributes(){return["title","count"];}constructor(){super();this._count=0;}connectedCallback(){this.render();}attributeChangedCallback(name,oldValue,newValue){if(name==="title"||name==="count"){this.render();}}setcount(val){this._count=val;this.render();}getcount(){returnthis._count;}render(){// Use webjsx's applyDiff to render JSX inside the Web Componentconstvdom=(<div><h2>{this.getAttribute("title")}</h2><p>Count: {this.count}</p></div>);webjsx.applyDiff(this,vdom);}}// Register the custom elementif(!customElements.get("my-element")){customElements.define("my-element",MyElement);}// Create a virtual DOM with the custom Web Componentconstvdom=<my-elementtitle="Initial Title"count={10}></my-element>;// Render the custom Web ComponentconstappContainer=document.getElementById("app");webjsx.applyDiff(appContainer,vdom);Attach event listeners directly within your JSX using standard HTML event attributes.
import*aswebjsxfrom"webjsx";// Define an event handlerconsthandleClick=()=>{alert("Button clicked!");};// Create a button with an onclick eventconstvdom=<buttononclick={handleClick}>Click Me</button>;// Render the buttonconstappContainer=document.getElementById("app");webjsx.applyDiff(appContainer,vdom);Group multiple elements without introducing additional nodes to the DOM using <>...</> syntax.
import*aswebjsxfrom"webjsx";// Define a custom Web Component using fragmentsclassMyListextendsHTMLElement{connectedCallback(){constvdom=(<><h2>My List</h2><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul><footer>Total items: 3</footer></>);webjsx.applyDiff(this,vdom);}}// Register the custom elementif(!customElements.get("my-list")){customElements.define("my-list",MyList);}// Render the custom Web ComponentconstappContainer=document.getElementById("app");constvdom=<my-list></my-list>;webjsx.applyDiff(appContainer,vdom);Creates a virtual DOM element.
JSX calls createElement implicitly:
constvdom=(<divid="main-container"><h1>Welcome to webjsx</h1></div>);Usage (Non-JSX):
constvdom=webjsx.createElement("div",{id: "main-container"},webjsx.createElement("h1",null,"Welcome to webjsx"));Applies the differences between the new virtual node(s) and the existing DOM.
Usage:
constvdom=<pclass="text">Updated Text</p>;webjsx.applyDiff(appContainer,vdom);A special type used to group multiple elements without adding extra nodes to the DOM.
Usage:
<><span>Item 1</span><span>Item 2</span></>You probably won't need to use this directly. But if you want to convert a virtual DOM Element into a real DOM Element you can use createDOMElement.
Usage:
constvnode=<div>Hello, world!</div>;constdomNode=webjsx.createDOMElement(vnode);document.body.appendChild(domNode);import*aswebjsxfrom"webjsx";// Define the custom Web ComponentclassCounterElementextendsHTMLElement{staticgetobservedAttributes(){return["title","count"];}constructor(){super();this._count=0;}connectedCallback(){this.render();}attributeChangedCallback(name,oldValue,newValue){if(name==="title"||name==="count"){this.render();}}setcount(val){this._count=val;this.render();}getcount(){returnthis._count;}render(){// Render JSX inside the Web Componentconstvdom=(<div><h2>{this.getAttribute("title")}</h2><p>Count: {this.count}</p><buttononclick={this.increment.bind(this)}>Increment</button></div>);webjsx.applyDiff(this,vdom);}increment(){this.count+=1;}}// Register the custom elementif(!customElements.get("counter-element")){customElements.define("counter-element",CounterElement);}// Create and render the CounterElementconstvdom=<counter-elementtitle="My Counter"count={0}></counter-element>;constappContainer=document.getElementById("app");webjsx.applyDiff(appContainer,vdom);If a class defines the webjsx_suspendRendering and webjsx_resumeRendering methods, WebJSX will call the former before setting properties and the latter after all properties are set. This allows you to suspend rendering while multiple properties are being set, which would otherwise result in multiple re-renders.
In the following example, for the JSX markup , the render() method is called only once after both properties are set:
classMyElementextendsHTMLElement{constructor(){super();this.renderingSuspended=false;}render(){if(!this.renderingSuspended){this.textContent=`Prop1: ${this.getAttribute("prop1")}, Prop2: ${this.getAttribute("prop2")}`;}}__webjsx_suspendRendering(){this.renderingSuspended=true;}__webjsx_resumeRendering(){this.renderingSuspended=false;this.render();// Perform the actual rendering}}Ensure your tsconfig.json is set up to handle JSX.
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "webjsx"
}
}TypeScript will complain that your Custom Element (such as <counter-element>) is not found. That's because it is only aware of standard HTML elements and doesn't know what <counter-element> is.
To fix this you need to declare custom elements in a declarations file, such as custom-elements.d.ts:
import"webjsx";declare global {namespaceJSX{interfaceIntrinsicElements{"counter-element": {count: number;};"sidebar-component": {about: string;email: string;};}}}You can bundle with your favorite bundler, but most apps don't need to.
You can load modules directly on the web page these days:
<!DOCTYPE html><htmllang="en"><head><title>WebJsx Test</title><!-- node_modules or wherever you downloaded webjsx --><scripttype="importmap">{"imports": {"webjsx": "../node_modules/webjsx/dist/index.js","webjsx/jsx-runtime": "../node_modules/webjsx/dist/jsx-runtime.js"}}</script><!-- This is your entry point --><scripttype="module" src="../dist/index.js"></script></head><body><divid="app"></div></body></html>You can see more examples on StackBlitz.
For routing needs, you can use webjsx-router, a minimal type-safe pattern matching router designed specifically for WebJSX applications.
npm install webjsx-routerimport*aswebjsxfrom"webjsx";import{match,goto,initRouter}from"webjsx-router";// Initialize router with routing logicconstcontainer=document.getElementById("app")!;initRouter(container,()=>match("/users/:id",(params)=><user-detailsid={params.id}/>)||match("/users",()=><user-list/>)||<not-found/>);// Navigation with gotogoto("/users/123");// With query parametersgoto("/search",{q: "test",sort: "desc"});// Static routesmatch("/about",()=><about-page/>);// Routes with parametersmatch("/users/:id",(params)=><user-detailsid={params.id}/>);// Query parameters// URL: /search?q=test&sort=descmatch("/search",(params,query)=>(<search-resultsquery={query.q}sort={query.sort}/>));The router includes TypeScript support with automatic type inference for parameters and query strings. For more details, check out the webjsx-router documentation.
Contributions are welcome! Whether it's reporting bugs, suggesting features, or submitting pull requests, your help is appreciated. Please ensure that your contributions adhere to the project's coding standards and include appropriate tests.
To run the tests:
npm testWebJSX is open-source software licensed as MIT.
If you encounter any issues or have questions, feel free to open an issue on GitHub or reach out via Twitter @jeswin.