A new business app starts with a question: React, Vue or Angular? The question that rarely gets asked is whether it needs one at all. For a page that lists orders, edits a customer and shows a few figures, the browser now does most of what we once picked a framework for.
This article walks through what frameworks were for, what the browser has taken over, and what is still missing. Then it builds a working order page in one HTML file, with no framework and no build step.
The habit
You start a project. You pick a framework, then a component library for that framework, then a state library, a router and a form library that fit both. By the time the first page shows a table, you have five dependencies that must agree with each other, and each will ship a breaking major version on its own schedule.
None of that is wrong. A framework gives a team one way to do things, and every developer you hire has used one. But it is a choice made by reflex, and the reasons it was the right reflex ten years ago have mostly gone away.
What a framework was for
Frameworks filled holes in the browser. Look at what those holes were, and what fills them today:
| You needed a framework for | The browser has |
|---|---|
| Your own reusable tags | Custom elements |
| Knowing when a component appears and disappears |
connectedCallback and disconnectedCallback
|
| Modals that trap focus and close on Escape | <dialog> |
| Dropdowns that float above everything | The Popover API |
| Form validation | Constraint validation: required, min, pattern, :invalid
|
| Numbers, amounts and dates per locale | Intl |
| Code split into modules | ES modules, loaded straight from a URL |
The big remaining selling point is reactivity: change a variable and the page follows. That solves a real problem in an app where a hundred things depend on each other. A business page is rarely that app. It shows records, lets you change one, and shows the result. The places that change are few, and you know which they are.
What is still missing
The browser has no table that sorts and pages. No tabs, no tree view, no chart. That is what a business page is made of, and it is the real reason teams reach for a framework and its component library.
So that is the gap I filled with @relax.js/components: tables, tree views, tabs, menus, dialogs, charts and meters for business pages. Not a framework. Each one is a plain custom element, and where the browser already has the part, it uses it: the table is a native <table>, menus use the Popover API, the dialog is a <dialog>.
A page with no framework
Save this as a file and open it. No npm install, no bundler:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Orders</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@relax.js/components@2.0.0/dist/theme.css">
<style>
body { font-family: system-ui; background: var(--surface-bg); color: var(--text-primary); }
</style>
</head>
<body>
<r-stat-tile label="Orders"></r-stat-tile>
<button id="add">New order</button>
<r-table page-size="10">
<r-column field="number" sortable>Order</r-column>
<r-column field="customer" sortable>Customer</r-column>
<r-column field="total" sortable>Total</r-column>
</r-table>
<r-dialog id="new-order" heading="New order">
<template>
<form method="dialog">
<label>Customer <input name="customer" required></label>
<label>Total <input name="total" type="number" min="1" required></label>
<button value="save">Save</button>
</form>
</template>
</r-dialog>
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@relax.js/components@2.0.0/+esm';
const orders = [
{ number: 'SO-10001', customer: 'Fjällbo AB', total: 12400 },
{ number: 'SO-10002', customer: 'Kvarnen HB', total: 860 },
{ number: 'SO-10003', customer: 'Ekdahl & Son', total: 3150 },
];
const table = document.querySelector('r-table');
const tile = document.querySelector('r-stat-tile');
const dialog = document.querySelector('#new-order');
function show() {
table.rows = orders;
tile.setAttribute('value', orders.length);
}
dialog.addEventListener('submit', (event) => {
const form = new FormData(event.target);
orders.push({
number: `SO-${10001 + orders.length}`,
customer: form.get('customer'),
total: Number(form.get('total')),
});
});
document.querySelector('#add').addEventListener('click', async () => {
if (await dialog.open() === 'save') {
show();
}
});
show();
</script>
</body>
</html>
You get a figure, a sortable table with grouped numbers and a pager, and a modal form. The form refuses to save without a customer or with a total below 1. That is the browser, not my code and not a library.
Now count what is not there. No state store. No virtual DOM. No compiler. No component tree to keep in your head. The data is an array, and show() is the whole rendering strategy: when the data changed, you tell the page.
"But who keeps the page in sync?"
You do, and that is the point. A framework re-renders when it decides something changed. When it gets it wrong, you debug the framework: why did this render twice, why did that not render at all.
Each component here redraws when you change its attributes, its properties or its content, and at no other time. There is no base class, no reactivity and no hidden scheduler. The lifecycle is the one on MDN. If the table shows old rows, you forgot to set rows, and a breakpoint on that line tells you so.
One rule trips everybody up: records go in a property, not an attribute. Attributes are strings, and a list in an attribute is a JSON string the component would have to parse and keep in sync with the property. So rows, columns and chart data are set from code, and scalars like page-size are attributes.
"But it needs Relax.js core"
Yes. The components have one peer dependency, @relax.js/core, and the jsDelivr link pulls it in for you.
That does not sneak a framework back in. A framework calls your code: it owns the lifecycle, the rendering and the state. Core is a set of libraries your code calls, and you use only the parts you need: typed form reading, routing, dependency injection, translations. The components themselves call it for four things:
- translating the words they show (
No data,Next) - reporting errors through one channel
- formatting values
- letting the router open a dialog, and only for a dialog you give a
name
When the page above grows into an app with several pages, those same libraries give you a router and typed forms. You add them when you need them, not on day one.
Already on React or Vue?
Then the components still work, because every framework renders HTML and a custom element is HTML. The property rule above is all you need to know. React 19 assigns rows={orders} as a property. In Vue you write :rows.prop="orders" and tell the compiler that r- tags are not Vue components. Events are plain DOM events.
It is a way out as much as a way in. The table you use in React today is the same table in the page that replaces it.
The takeaway
Frameworks were the right answer to a browser that could not do much. The browser caught up. For a page that lists, edits and summarizes records, what is missing is the business components, not a framework. Add those as plain custom elements, write the updates yourself, and you have nothing left to migrate away from.
Next in the series: server-side paging and sorting, with one async function.
Top comments (0)