I love web components! Even better now with all the improvements they received over the last few years. However, there are still many limitations, often requiring verbose setups, that make dealing with components painful.
React 19 + WebComponent *I'm super excited about the React 19 release for one reason: web components. Mainly because I created Markup which exposes a reactive DOM…*medium.com
There are many amazing libraries that simplify this, but I hate libraries that require you to build or compile your code to get something while adding additional syntax that just does not need to be there.
This is how a simple count button would look like using native Web APIs, Vanilla JavaScript and while complying with the correct HTML tag web standards.
class CountButton extends HTMLElement {
static observedAttributes = ['count'];
#root;
get count() {
return Number(this.getAttribute('count') ?? 0);
}
set count(newCount) {
this.setAttribute('count', String(newCount));
}
constructor() {
super();
this.#root = this.attachShadow({mode: 'open'});
this.#root.innerHTML = `
<button type="button">Count: ${this.count}</button>
`
const btn = this.#root.children[0];
btn.addEventListener('click', () => {
this.setAttribute('count', String(this.count + 1));
})
}
attributeChangedCallback(name, oldValue, newValue) {
const btn = this.#root.children[0];
btn.textContent = `Count: ${this.count}`;
this.dispatchEvent(new CustomEvent('change', {detail: this.count}))
}
}
customElements.define('count-button', CountButton)
It is just too verbose for something so simple. If we increase the number of observed attributes, it gets worse.
Now this is what it could look like:
class CountButton extends WebComponent {
static observedAttributes = ['count'];
count = 0;
updateCount = () => {
this.count += 1;
this.dispatch('change', {value: this.count})
}
render() {
return html`
<button type="button" onclick="${this.updateCount}">
Count: ${this.props.count}
</button>
`
}
}
customElements.define('count-button', CountButton)
This is the exact same thing but I don’t have to concern myself with setters/getters, following the web standards, manipulating the DOM, or tracking values. It’s just JavaScript as we know it.
What you are seeing above is enhanced Web Component APIS with Markup by Before Semicolon.
Markup is a reactive templating system and all it does is enhance the native web component APIs by handling things like getters and setters, Shadow Root, DOM rendering, and tracking value updates to update the internal DOM where and when needed.
The best part is that you dont need to build/compile this code unless you are already in an environment that requires it like a TypeScript project.
You can just add the following script to the top of your HTML file and roughly 11kb of JavaScript will give you all the reactivity you need for your web components.
<script src="https://unpkg.com/@beforesemicolon/web-component/dist/client.js"></script>
How does it work?
Markup is just a reactive templating system. The following is what Markup is in a nutshell:
const [count, setCount] = state(0);
const updateCount = () => {
setCount(prev => prev + 1)
}
const temp = html`
<button type="button" onclick="${updateCount}">
Count: ${count}
</button>
`;
temp.render(document.body);
It is just a way to define reactive data and render HTML using JavaScript template literal.
What it also exposes is a WebComponent class that does the following:
- It extends
HTMLElement; - It sets the shadow root for the component in
openmode by default and exposes theconfigproperty for you to customize or opt out; - It sets all setters and getters for the attributes you defined in the
observedAttributesarray and link them to Markupstateand exposes them via thepropsproperty; - It exposes
onMount,onUpdate,onDestroy,onAdoption, andonErrorlifecycle functions you can use instead of the web component callbacks with some improvements. You also have access to themountedproperty to check; - It exposes a
renderfunction that is only called once after the component is mounted where you can perform any render type logic and return anything to render: a string, DOM Node/Element, Markup*html*template, etc; - It exposes a
dispatchmethod which is just a shortcut fordispatchEventwith already aCustomEventset by default; - It exposes
stylesheetfor you to define the component style andupdateStylesheetfor you to dynamically update it; - It exposes the
internalsproperty for you to use when creating Form Control Elements; - It exposes
contentRoot(shadowRoot),root(closest document or ShadowRoot), andrefs(Markup template Element references), for you to tap into the DOM safely as needed; - It exposes
initialStatefor you to define internal Markupstateproperty andupdateStatemethod for you to update them;
And that’s it! Everything else is just a web component APIs and JavaScript as you might already know it!
See the docs:
Web Component - Markup by Before Semicolon Enhance Web Component APIs with Markup by Before Semicolonmarkup.beforesemicolon.com
Form control elements
Form control web components come with additional setups and Markup does not block you in any way. Again, it's just web standards and JavaScript as you know it.
Take a look at this simple example you can read more about in the docs:
class TextField extends WebComponent {
static observedAttributes = [
'value',
'placeholder',
'disabled',
'pattern',
'error',
'required',
]
static formAssociated = true
stylesheet = `
input {
border: 1px solid #444;
padding: 8px 10px;
border-radius: 3px;
min-width: 150px;
}
input:user-valid {
border-color: #090;
}
input:user-invalid {
border-color: #900;
}
input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`
placeholder = ''
value = ''
pattern = ''
disabled = false
required = false
error = 'Invalid field value'
formAssociatedCallback(form) {
this.handleChange(this.props.value(), false)
}
formDisabledCallback(disabled) {
this.disabled = disabled
}
formResetCallback() {
this.handleChange('', false)
}
formStateRestoreCallback(state, mode) {
if (mode == 'restore') {
const [controlMode, value] = state.split('/')
this.handleChange(value, false)
}
}
handleChange = (value, report = true) => {
this.internals.setFormValue(value)
this.value = value;
const [inputField] = this.refs['input'];
const validity = inputField.validity
this.internals.setValidity(
validity,
validity.valid ? undefined : this.props.error(),
inputField
)
report && this.internals.reportValidity()
this.dispatch('change', { value })
}
render() {
const {error, ...inputAttrs} = this.props;
return html`
<input
${inputAttrs}
part="text-input"
type="text"
ref="input"
onchange="${(event) => this.handleChange(event.target.value)}"
/>
`
}
}
customElements.define('text-field', TextField)
Advanced Forms with Custom Form Elements *Many situations call for custom form elements, which can be easily created with web components. The problem is that…*medium.com
Take away
Web component APIs are already powerful on their own. We don’t need another fancy framework to make things complicated and obstruct us from what's going on at the native level.
Markup here is just an enhancement, and at any time you can just override any behavior while staying as close as possible to the native web APIs and JavaScript.
Markup is just offering its amazing reactive templating capabilities to remove some of the existing pains in creating and maintaining web components. The rest is up to you.
Read more about Markup and how it can help you:
Markup by Before Semicolon *Reactive HTML Templating System to create Web User Interfaces.*markup.beforesemicolon.com

By Elson Correia