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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lofi-ui/lofi

v0.1.4

Published

Simple, unstyled, web elements for common UI components

Downloads

5

Readme

Lofi

The web's missing UI primitives.

Install it

Lofi was written in vanilla JavaScript modules with no build process whatsoever. Simply include index.js on your page and you're off:

<head>
    <!-- ... -->

    <script src="./index.js" type="module"></script>
</head>

Use it

To get a glimpse of how lofi works, paste the following HTML into a page and watch it sing...

<ui-invoke anchor>
    <button type="button">Open</button>

    <ui-menu popover>
        <ui-item>Foo</ui-item>
        <ui-item>Bar</ui-item>
        <ui-item>Baz</ui-item>
    </ui-menu>
</ui-invoke>

Components

Lofi ships with the following components:

Radio Group

<ui-radio-group>
    <ui-radio value="foo" checked>Foo</ui-radio>
    <ui-radio value="bar">Bar</ui-radio>
    <ui-radio value="baz">Baz</ui-radio>
</ui-radio-group>

Disclosure

<ui-disclosure>
    <button>...</button>

    <div class="hidden data-[open]:block">
        <!-- ... -->
    </div>
</ui-disclosure>

Combobox

<ui-combobox>
    <input type="text">

    <ui-listbox>
        <ui-option value="foo"> <button>Foo</button> </ui-option>
        <ui-option value="bar"> <button>Bar</button> </ui-option>
        <ui-option value="baz"> <button>Baz</button> </ui-option>
    </ui-listbox>
</ui-combobox>

Tooltip

<ui-tooltip>
    <button>Tooltip</button>

    <div popover>
        <!-- ... -->
    </div>
</ui-tooltip>

Listbox

<ui-listbox>
    <ui-option value="foo">Foo</ui-option>
    <ui-option value="bar">Bar</ui-option>
    <ui-option value="baz">Baz</ui-option>
</ui-listbox>

Switch

<ui-switch></ui-switch>

Dropdown

<ui-dropdown>
    <button>Open</button>

    <div popover>
        <!-- ... -->
    </div>
</ui-dropdown>

Modal

<ui-modal>
    <button>Open</button>

    <dialog>
        <!-- ... -->
    </dialog>
</ui-modal>

Field

<ui-field>
    <ui-label>Something</ui-label>
    <ui-description>Some description</ui-description>

    <input type="text">
</ui-field>

Tabs

<ui-tabs>
    <ui-tab-group>
        <ui-tab panel="foo">
            <button>Foo</button>
        </ui-tab>

        <ui-tab panel="bar">
            <button>Bar</button>
        </ui-tab>

        <ui-tab panel="baz">
            <button>Baz</button>
        </ui-tab>
    </ui-tab-group>

    <ui-tab-panel name="foo">
        <div>...</div>
    </ui-tab-panel>

    <ui-tab-panel name="bar">
        <div>...</div>
    </ui-tab-panel>

    <ui-tab-panel name="baz">
        <div>...</div>
    </ui-tab-panel>
</ui-tabs>

Menu

<ui-menu>
    <ui-item>
        <button>Foo</button>
    </ui-item>

    <ui-item>
        <button>Bar</button>
    </ui-item>

    <ui-item>
        <button>Baz</button>
    </ui-item>
</ui-menu>
<ui-combobox>
    <input />

    <ui-listbox>
        <ui-option value="foo">
            <button>Foo</button>
        </ui-option>

        <ui-option value="bar">
            <button>Bar</button>
        </ui-option>

        <ui-option value="baz">
            <button>Baz</button>
        </ui-option>
    </ui-listbox>
</ui-combobox>

Most basic example

Let's start with the most basic Lofi element. A simple switch that can be pressed to toggle it on and off:

<ui-switch>
    <!-- ... -->
</ui-switch>

Lofi will handle all the accessibility features like labels, aria attributes, focusability, & keyboard interactivity.

You can style it in Tailwind using its data attributes:

<ui-switch class="group bg-white data-[checked]:bg-black">
    <!-- ... -->
</ui-switch>

You can bind to its state from any framework as if it were a native input element:

<ui-switch x-model="notifications"> <!-- Alpine -->
<ui-switch v-model="notifications"> <!-- Vue -->
<ui-switch :value="notifications" onChange={setNotifications}> <!-- React, etc... -->

Here are some other elements of a similar nature.

A custom radio group component

<ui-radio-group>
  <ui-radio value="table">Table</ui-radio>
  <ui-radio value="grid">Grid</ui-radio>
  <ui-radio value="list">List</ui-radio>
</ui-radio-group>

A tabs component

<ui-tabs>
  <ui-tab-group>
    <ui-tab panel="foo">Foo</ui-tab>
    <ui-tab panel="bar">Bar</ui-tab>
    <ui-tab panel="baz">Baz</ui-tab>
  </ui-tab-group>

  <ui-tab-panel name="foo">...</ui-tab-panel>
  <ui-tab-panel name="bar">...</ui-tab-panel>
  <ui-tab-panel name="baz">...</ui-tab-panel>
</ui-tabs>

Dialogs and popovers

When perusing lofi, you might notice an obvious lack of <ui-dialog> or <ui-popover> elements. This is because browsers now ship with native popovers and dialogs.

A key principle behind Lofi is: prefer native solutions; you get more for free.

However, there are still some gaps that Lofi can fill to make using them a more pleasant experience.

Let's first look at a truly native HTML popover:

<button popovertarget="dropdown">Open...</button>

<div popover id="dropdown">
    <!-- Dropdown contents... -->
</div>

As you can see, the popover attribute is simple. Attach it to an element, invoke it with a button using popovertarget, and everything works like magic.

Briefly, here's what you get out of the box with a native popover:

  • Light dismissal (press escape to close and focus invoke element)
  • Top-layer rendering (no need for portals or z-index hacks)
  • No need for extra aria attributes

However, there are two key hangups with going full native:

  • You need to provide non-conflicting hardcoded IDs (this can be a problem in re-usable server-side templates)
  • You have to "anchor" the popover's positioning to the button with JavaScript (Using something like Floating-UI)

Now let's look at a native dialog element:

<button onclick="this.nextElementSibling.showModal()">Open...</button>

<dialog>
    <!-- Modal contents -->
</dialog>

Pros of the <dialog> element:

  • Automatically makes page "inert" while open
  • Traps focus inside dialog
  • Renders in the top-layer (no need for z-index hacks and what not)

This is great, but you can only invoke a modal dialog using element.showModal() in JavaScript. There is no "dialogtarget" attribute like there is for popovers. (There is a proposal for a more universal API like invoketarget to solve this)

Now that the stage is set, let me introduce you to Lofi's most powerful element: <ui-invoke>

The <ui-invoke> element provides a way to declaratively link a invoke (often a button) with an invokeable target (often a popover or dialog).

Here's a native button/dialog combo declaratively linked by <ui-invoke>:

<ui-invoke>
  <button>Open...</button>

  <dialog>
    <!-- ... -->
  </dialog>
</ui-invoke>

This API is an elegant alternative to a more traditional—albeit more agressive and less-composable—API like the following:

<!-- A dystopian vingette into what Lofi almost become... -->
<ui-dialog>
  <ui-dialog-invoke>
    <button>Open...</button>
  </ui-dialog-invoke>

  <ui-dialog-panel>
    <dialog>
      <!-- ... -->
    </dialog>
  </ui-dialog-panel>
</ui-dialog>

Let's tour more examples of <ui-invoke>:

Here's a simple navigation menu using the semantic <nav> element provided by browsers, in combination with the popover attribute:

<ui-invoke>
  <button>Open...</button>

  <nav popover>
    <a href="..."></a>
    <a href="..."></a>
    <a href="..."></a>
  </nav>
</ui-invoke>

<ui-invoke> even works with non-overlay elements. For example, you can use it in combination with Lofi's <ui-disclosure> element to combose an accordion widget:

<ui-invoke>
  <button>Do you accept refunds?</button>

  <ui-disclosure class="hidden data-[open]:block">
    Sure, if you ask nicely...
  </ui-disclosure>
</ui-invoke>

Here's an example of using popover in combination with Lofi's <ui-menu> element to construct a robust action menu:

<ui-invoke>
  <button>Open...</button>

  <ui-menu popover>
    <ui-item>Edit</ui-item>
    <ui-item>Archive</ui-item>
    <ui-item>Delete</ui-item>
  </ui-menu>
</ui-invoke>

The beauty of the <ui-invoke> and popover attribute combo, is that you can "opt-out" of using components as overlays by placeing them outside <ui-invoke>.

Here is the action menu from above rendered as a static action list on the page:

<ui-menu>
  <ui-item onclick="...">Edit</ui-item>
  <ui-item onclick="...">Archive</ui-item>
  <ui-item onclick="...">Delete</ui-item>
</ui-menu>

Another component you might use this pattern with is: <ui-listbox> (a stylable select menu of options). Here is a listbox statically displayed on a page:

<ui-listbox x-model="role" multiple>
  <ui-option value="read">Read-only</ui-option>
  <ui-option value="write">Edit</ui-option>
  <ui-option value="admin">Admin</ui-option>
</ui-listbox>

Now we can make that a common select menu dropdown like so:

<ui-invoke>
  <button>Open...</button>

  <ui-listbox popover x-model="role" multiple>
    <ui-option value="read">Read-only</ui-option>
    <ui-option value="write">Edit</ui-option>
    <ui-option value="admin">Admin</ui-option>
  </ui-listbox>
</ui-invoke>

And of course, in addition to controlling it's value both externally and internally, you can style it with tailwind:

<ui-invoke>
  <button>Open...</button>

  <ui-listbox popover x-model="role" multiple>
    <ui-option value="read" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Read-only</ui-option>
    <ui-option value="write" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Edit</ui-option>
    <ui-option value="admin" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Admin</ui-option>
  </ui-listbox>
</ui-invoke>

To take it even further, we can compose a full autocomplete component by combining the listbox with a combobox text input like so:

<ui-combobox>
  <input type="text" placeholder="Type to search...">

  <ui-listbox popover x-model="role" multiple>
    <ui-option value="read" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Read-only</ui-option>
    <ui-option value="write" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Edit</ui-option>
    <ui-option value="admin" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Admin</ui-option>
  </ui-listbox>
</ui-combobox>

Or maybe you want to compose a command pallet in a dialog instead, you can do that too:

<ui-invoke>
  <button>Open command...</button>

  <dialog>
    <ui-combobox>
      <input type="text" placeholder="Type to search..." autofocus>
    </ui-combobox>

    <ui-listbox>
      <ui-option>Dashboard</ui-option>
      <ui-option>New Post</ui-option>
      <ui-option>Log out</ui-option>
    </ui-listbox>
  </dialog>
</ui-invoke>

APIs:

For Sebastian

  • ui-invoke -> limit it? (only popovers/dialogs) or expand (breaks down with combobox)
  • ui-trigger vs ui-invoke?
  • Abandon ui-invoke? in favor of ui-dialog and ui-popover
    • What about non-modal dialogs? are those in ui-popover? maybe it should just be ui-disclosure? idk...
    • Now what about nested inside ui-dialog? to close the dialog?
  • What about x-combobox in combination with popover?
  • Use x-model on non-form-controls like ui-tabs
  • ui-tabs/tablist using independantly with a tags
    • Other things like menu items and options etc...
  • ui-dialog/popover be used with plain divs and no popover attribute?
    • redundant aria attributes with something like popover?

points:

  • lint for "dialog" and "popover" native stuff at runtime
  • If I'm relying on dedicated children, I should error out when it's missing

Fundamental things:

  • Should ui-checkbox still be x-modealable when inside x-checkbox-group?
    • If so, should it still dispatch bubbling change events?
      • If so, should they be intercepted by ui-checkbox-group to prevent double handling by x-model?
  • Which attributes/getter/setters should exist on wrappers, and which on child buttons, and which duplicated?
  • How should "disabled" behavior be handled? What get's "disabled"
    • Potentially disabled things: aria, interaction handlers, x-model/selectability?
  • When to use data-active and when to use data-focus (and is data-focus the same as data-focus-visible)
  • How is "morphing" handled (particularly IDs)
  • How does it react when things like data-checked and checked are changed in the DOM?