
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@zeix/ui-element
Advanced tools
Version 0.14.0
UIElement - a HTML-first library for reactive Web Components
UIElement is a set of functions to build reusable, loosely coupled Web Components with reactive properties. It provides structure through components and simplifies state management and DOM synchronization using declarative signals and effects, leading to more organized and maintainable code without a steep learning curve.
Unlike SPA frameworks (React, Vue, Svelte, etc.) UIElement takes a HTML-first approach, progressively enhancing sever-rendered HTML rather than recreating (rendering) it using JavaScript. UIElement achieves the same result as SPA frameworks with SSR, but with a simpler, more efficient approach. It works with a backend written in any language or with any static site generator.
UIElement uses Cause & Effect internally for state management with signals and for scheduled DOM updates. But you could easily rewrite the component() function to use a signals library of your choice or to produce something else than Web Components.
# with npm
npm install @zeix/ui-element
# or with bun
bun add @zeix/ui-element
The full documentation is still work in progress. The following chapters are already reasonably complete:
Server-rendered markup:
<basic-counter count="5">
<button type="button">💐 <span>5</span></button>
</basic-counter>
UIElement component:
import { asInteger, component, on, setText } from '@zeix/ui-element'
export default component(
'basic-counter',
{
// Get initial value from count attribute
count: asInteger(),
},
(el, { first }) => [
// Update count display when state changes
first('span', setText('count')),
// Handle click events to change state
first(
'button',
on('click', () => {
el.count++
}),
),
],
)
Example styles:
basic-counter {
& button {
border: 1px solid var(--color-border);
border-radius: var(--space-xs);
background-color: var(--color-secondary);
padding: var(--space-xs) var(--space-s);
cursor: pointer;
color: var(--color-text);
font-size: var(--font-size-m);
line-height: var(--line-height-xs);
transition: background-color var(--transition-short) var(--easing-inout);
&:hover {
background-color: var(--color-secondary-hover);
}
&:active {
background-color: var(--color-secondary-active);
}
}
}
An example demonstrating how to create a fully accessible tab navigation.
Server-rendered markup:
<module-tabgroup>
<div role="tablist">
<button
type="button"
role="tab"
id="trigger1"
aria-controls="panel1"
aria-selected="true"
tabindex="0"
>
Tab 1
</button>
<button
type="button"
role="tab"
id="trigger2"
aria-controls="panel2"
aria-selected="false"
tabindex="-1"
>
Tab 2
</button>
<button
type="button"
role="tab"
id="trigger3"
aria-controls="panel3"
aria-selected="false"
tabindex="-1"
>
Tab 3
</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="trigger1">
Tab 1 content
</div>
<div role="tabpanel" id="panel2" aria-labelledby="trigger2" hidden>
Tab 2 content
</div>
<div role="tabpanel" id="panel3" aria-labelledby="trigger3" hidden>
Tab 3 content
</div>
</module-tabgroup>
UIElement component:
import { component, on, setProperty, show } from '@zeix/ui-element'
import { manageArrowKeyFocus } from './manage-arrow-key-focus'
export default component('module-tabgroup', {
selected: '',
},
(el, { all, first }) => {
el.selected =
el.querySelector('[role="tab"][aria-selected="true"]')
?.getAttribute('aria-controls') ?? ''
const isSelected = target => el.selected === target.getAttribute('aria-controls')
const tabs = Array.from(el.querySelectorAll<HTMLButtonElement>('[role="tab"]'))
let focusIndex = 0
return [
first('[role="tablist"]',
on('keydown', manageArrowKeyFocus(tabs, focusIndex)),
),
all('[role="tab"]',
on('click', e => {
el.selected =
e.currentTarget.getAttribute('aria-controls') ?? ''
focusIndex = tabs.findIndex(tab => isSelected(tab))
}),
setProperty('ariaSelected', target => String(isSelected(target))),
setProperty('tabIndex', target => isSelected(target) ? 0 : -1),
),
all('[role="tabpanel"]',
show(target => el.selected === target.id),
),
]
})
Auxiliary function:
```js
export const manageArrowKeyFocus = (elements, index) => e => {
if (!(e instanceof KeyboardEvent))
throw new TypeError('Event is not a KeyboardEvent')
const handledKeys = [
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
'Home',
'End',
]
if (handledKeys.includes(e.key)) {
e.preventDefault()
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
index = index < 1 ? elements.length - 1 : index - 1
break
case 'ArrowRight':
case 'ArrowDown':
index = index >= elements.length - 1 ? 0 : index + 1
break
case 'Home':
index = 0
break
case 'End':
index = elements.length - 1
break
}
if (elements[index]) elements[index].focus()
}
}
Example styles:
module-tabgroup {
display: block;
margin-bottom: var(--space-l);
> [role='tablist'] {
display: flex;
border-bottom: 1px solid var(--color-gray-50);
padding: 0;
margin-bottom: 0;
& button {
border: 0;
border-top: 2px solid transparent;
border-bottom-width: 0;
font-family: var(--font-family-sans);
font-size: var(--font-size-s);
font-weight: var(--font-weight-bold);
padding: var(--space-s) var(--space-m);
color: var(--color-text-soft);
background-color: var(--color-secondary);
cursor: pointer;
transition: all var(--transition-short) var(--easing-inout);
&:hover,
&:focus {
color: var(--color-text);
background-color: var(--color-secondary-hover);
}
&:active {
color: var(--color-text);
background-color: var(--color-secondary-active);
}
&[aria-selected='true'] {
color: var(--color-primary-active);
border-top: 3px solid var(--color-primary);
background-color: var(--color-background);
margin-bottom: -1px;
}
}
}
> [role='tabpanel'] {
font-family: sans-serif;
font-size: var(--font-size-m);
background: var(--color-background);
margin-block: var(--space-l);
}
}
An example demonstrating how to use a custom attribute parser (sanitize an URL) and a signal producer (async fetch) to implement lazy loading.
<module-lazy src="/module-lazy/snippet.html">
<card-callout>
<div class="loading" role="status">Loading...</div>
<div class="error" role="alert" aria-live="polite"></div>
</card-callout>
</module-lazy>
UIElement component:
import {
UNSET,
component,
computed,
dangerouslySetInnerHTML,
setText,
show,
state,
toggleClass,
} from '@zeix/ui-element'
import { asURL } from './as-url'
export default component(
'module-lazy',
{
src: asURL,
},
(el, { first }) => {
const error = state('')
const content = computed(async abort => {
const url = el.src.value
if (el.src.error || !url) {
error.set(el.src.error ?? 'No URL provided')
return ''
}
try {
error.set('')
el.querySelector('.loading')?.remove()
const response = await fetch(url, { signal: abort })
if (response.ok) return response.text()
else error.set(response.statusText)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
error.set(errorMessage)
return ''
}
})
return [
dangerouslySetInnerHTML(content),
first(
'card-callout',
[
show(() => !!error.get() || content.get() === UNSET),
toggleClass('danger', () => !error.get()),
],
'Needed to display loading state and error messages.',
),
first('.error', setText(error), 'Needed to display error messages.'),
]
},
)
Custom attribute parser:
// Attribute Parser uses current element to detect recursion and set error message
export const asURL = (el, v) => {
let value = ''
let error = ''
if (!v) {
error = 'No URL provided'
} else if (
(el.parentElement || el.getRootNode().host)?.closest(
`${el.localName}[src="${v}"]`,
)
) {
error = 'Recursive loading detected'
} else {
try {
// Ensure 'src' attribute is a valid URL
const url = new URL(v, location.href)
// Sanity check for cross-origin URLs
if (url.origin === location.origin) value = String(url)
else error = 'Invalid URL origin'
} catch (err) {
error = String(err)
}
}
return { value, error }
}
Feel free to contribute, report issues, or suggest improvements.
License: MIT
(c) 2025 Zeix AG
FAQs
UIElement - a HTML-first library for reactive Web Components
We found that @zeix/ui-element demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.