npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zeix/le-truc

v0.15.0

Published

Le Truc - the thing for type-safe reactive web components

Readme

Le Truc

Version 0.15.0

Le Truc - the thing for type-safe reactive web components

Le Truc helps you create reusable, interactive web components that work with any backend or static site generator. Build once, use everywhere.

Le Truc 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 signals and effects, leading to more organized and maintainable code without a steep learning curve.

Unlike SPA frameworks (React, Vue, Svelte, etc.) Le Truc takes a HTML-first approach, progressively enhancing server-rendered HTML rather than recreating (rendering) it using JavaScript. Le Truc achieves the same result as SPA frameworks with SSR, with a simpler, more efficient approach.

Quick Start

Add interactivity to your HTML in three steps:

  1. Start with HTML:
<basic-hello>
  <label for="name">Your name</label>
  <input id="name" name="name" type="text" autocomplete="given-name" />
  <p>Hello, <output for="name">World</output>!</p>
</basic-hello>
  1. Define the component:
import { asString, defineComponent, on, setText } from '@zeix/le-truc'

defineComponent(
  'basic-hello',               // 1. Component name
  { name: asString('World') }, // 2. Reactive property
  q => ({                      // 3. Find DOM elements
    input: q.first('input'),
    output: q.first('output'),
  }),
  ({ host, input }) => ({      // 4. Define behavior
    input: on('input', () => { host.name = input.value }),
    output: setText('name'),
  }),
)
  1. Import and watch it work!

Key Features

  • 🧱 HTML Web Components: Build on standard HTML and enhance it with reusable Web Components. No Virtual DOM – Le Truc works directly with the real DOM.
  • 🚦 Reactive Properties: Get and set values like with normal element properties, but they automatically track reads and notify on changes (signals).
  • ⚡️ Fine-grained Effects: Pinpoint updates to the parts of the DOM that need updating, avoiding unnecessary re-renders.
  • 🧩 Function Composition: Declare component behavior by composing small, reusable functions (parsers and effects).
  • 🛠️ Customizable: Le Truc is designed to be easily customizable and extensible. Create your own custom parsers and effects to suit your specific needs.
  • 🌐 Context Support: Share global states across components without prop drilling or tightly coupling logic.
  • 🪶 Tiny footprint: Minimal core (~8kB gzipped) with tree-shaking support, minimizing JavaScript bundle size.
  • 🛡️ Type Safety: Early warnings when types don't match improve code quality and reduce bugs.

Le Truc uses Cause & Effect internally for state management with signals and glitch-free DOM updates. If wanted, you could fork Le Truc and replace Cause & Effect with a different state management library without changes to the user-facing createComponent() API.

Installation

# with npm
npm install @zeix/le-truc

# or with bun
bun add @zeix/le-truc

Documentation

The full documentation is still work in progress. The following chapters are already reasonably complete:

Basic Usage

  1. Start with HTML:
<basic-counter>
  <button type="button">💐 <span>5</span></button>
</basic-counter>
  1. Define the component:
import { asInteger, defineComponent, on, read, setText } from '@zeix/le-truc'

export default defineComponent(
  // 1. Component name
  'basic-counter',

  // 2. Reactive properties (signals)
  {
    // Count property is read from the DOM (ui.count) and converted to an integer
    count: read(ui => ui.count.textContent, asInteger()),
  },

  // 3. Find DOM elements
  ({ first }) => ({
    // first() returns the first element matching the selector
    increment: first(
      'button',
      'Add a native button element to increment the count.',
    ),
    count: first('span', 'Add a span to display the count.'),
  }),

  // 4. Define behavior (effects)
  ({ host }) => ({ // host is the component element with reactive properties
    // Add a click event listener to the increment button
    increment: on('click', () => {
      host.count++
    }),
    // Set the text of the count element to the count property whenever it changes
    count: setText('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);
    }
  }
}
  1. Import and watch it work!

Advanced Examples

Tab Group

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>

Le Truc component:

import { createSensor, defineComponent, read, setProperty, show } from '@zeix/le-truc'

const getAriaControls = element => element.getAttribute('aria-controls') ?? ''

const getSelected = (elements, isCurrent, offset = 0) => {
  const tabs = elements.get()
  const currentIndex = tabs.findIndex(isCurrent)
  const newIndex = (currentIndex + offset + tabs.length) % tabs.length
  return getAriaControls(tabs[newIndex])
}

export default defineComponent(
  // 1. Component name
  'module-tabgroup',

  // 2. Reactive properties (signals)
  {
    // Sensors are read-only signals that update on user interaction only (events)
    selected: createSensor(
      // Initial value from aria-selected attribute
      read(ui => getSelected(ui.tabs, tab => tab.ariaSelected === 'true'), ''),
      // Target element(s) key
      'tabs',
      // Event handlers return a value to update the signal
      {
        click: ({ target }) => getAriaControls(target),
        keyup: ({ event, ui, target }) => {
          const key = event.key
          if (
            [
              'ArrowLeft',
              'ArrowRight',
              'ArrowUp',
              'ArrowDown',
              'Home',
              'End',
            ].includes(key)
          ) {
            event.preventDefault()
            event.stopPropagation()
            const tabs = ui.tabs.get()
            const next =
              key === 'Home'
                ? getAriaControls(tabs[0])
                : key === 'End'
                  ? getAriaControls(tabs[tabs.length - 1])
                  : getSelected(
                      ui.tabs,
                      tab => tab === target,
                      key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1,
                    )
            tabs.filter(tab => getAriaControls(tab) === next)[0].focus()
            return next
          }
        },
      },
    ),
  },

  // 3. Find DOM elements
  ({ all }) => ({
    // all() returns a Collection signal that holds all elements matching the selector,
    // dynamically updating when the DOM changes
    tabs: all(
      'button[role="tab"]',
      'At least 2 tabs as children of a <[role="tablist"]> element are needed. Each tab must reference a unique id of a <[role="tabpanel"]> element.',
    ),
    panels: all(
      '[role="tabpanel"]',
      'At least 2 tabpanels are needed. Each tabpanel must have a unique id.',
    ),
  }),

  // 4. Define behavior (effects)
  ({ host }) => {
    // Extracted function to check if a tab is the current selected tab
    const isCurrentTab = tab => host.selected === getAriaControls(tab)

    return {
      // Set properties on tabs based on their selection status
      tabs: [
        setProperty('ariaSelected', target => String(isCurrentTab(target))),
        setProperty('tabIndex', target => (isCurrentTab(target) ? 0 : -1)),
      ],
      // Toggle visibility of panels based on the selected tab
      panels: show(target => host.selected === target.id),
    }
  },
)

Example styles:

module-tabgroup {
  display: block;
  margin-bottom: var(--space-l);

  > [role="tablist"] {
    display: flex;
    border-bottom: 1px solid var(--color-border);
    padding: 0;
    margin-bottom: 0;

    > [role="tab"] {
      border: 0;
      border-top: 2px solid transparent;
      border-bottom-width: 0;
      border-radius: var(--space-xs) var(--space-xs) 0 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);
      }

      &:focus {
        z-index: 1;
      }

      &: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);
  }
}

Lazy Load

An example demonstrating how to use a custom attribute parser (sanitize an URL) and a signal producer (async fetch) to implement lazy loading.

<module-lazyload src="/module-lazyload/snippet.html">
  <card-callout>
    <p class="loading" role="status">Loading...</p>
    <p class="error" role="alert" aria-live="assertive" hidden></p>
  </card-callout>
  <div class="content" hidden></div>
</module-lazyload>

Le Truc component:

import {
  asString,
  type Component,
  createComputed,
  dangerouslySetInnerHTML,
  defineComponent,
  setText,
  show,
  toggleClass,
} from '@zeix/le-truc'
import { isRecursiveURL, isValidURL } from '../_common/fetch'

export default defineComponent(
  // 1. Component name
  'module-lazyload',

  // 2. Reactive properties (signals)
  {
    src: asString(),
  },

  // 3. Find DOM elements
  ({ first }) => ({
    callout: first(
      'card-callout',
      'Needed to display loading state and error messages.',
    ),
    loading: first('.loading', 'Needed to display loading state.'),
    error: first('.error', 'Needed to display error messages.'),
    content: first('.content', 'Needed to display content.'),
  }),

  // 4. Define behavior (effects)
  ui => {
    const { host } = ui

    // Private async computed signal to fetch content from the provided URL
    const result = createComputed(
      async (_prev, abort) => {
        const url = host.src
        const error = !url
          ? 'No URL provided'
          : !isValidURL(url)
            ? 'Invalid URL'
            : isRecursiveURL(url, host)
              ? 'Recursive URL detected'
              : ''
        if (error) return { ok: false, value: '', error, pending: false }

        try {
          const response = await fetch(url, abort)
          if (!response.ok) throw new Error(`HTTP error: ${response.statusText}`)
          const content = await response.text()
          return { ok: true, value: content, error: '', pending: false }
        } catch (error) {
          return {
            ok: false,
            value: '',
            error: `Failed to fetch content for "${url}": ${String(error)}`,
            pending: false,
          }
        }
      },
      // Initial value of the signal before the Promise is resolved
      { ok: false, value: '', error: '', pending: true },
    )

    // Extracted function to check if an error occurred
    const hasError = () => !!result.get().error

    return {
      callout: [show(() => !result.get().ok), toggleClass('danger', hasError)],
      loading: show(() => !!result.get().pending),
      error: [show(hasError), setText(() => result.get().error ?? '')],
      content: [
        show(() => result.get().ok),
        // Set inner HTML to the fetched content (use only for trusted sources)
        dangerouslySetInnerHTML(() => result.get().value ?? '', {
          allowScripts: host.hasAttribute('allow-scripts'),
        }),
      ],
    }
  },
)

Contributing & License

Feel free to contribute, report issues, or suggest improvements.

License: MIT

(c) 2025 Zeix AG