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

floki-elements

v0.3.1

Published

Flexoki × Notion — framework-agnostic styled primitives as web components.

Downloads

674

Readme

floki-elements

Framework-agnostic web components in the Flexoki palette, styled in the calm-document design language: warm neutrals, hairline dividers, hover-as-background-wash, semibold-not-black headings, gentle radii.

The library ships styled primitives, not pre-composed layouts. It lends the look; it never imposes a structure. There is no pre-built sidebar, no workspace switcher, no breadcrumb chrome — there is a styled rail shell and styled row/label/spacer primitives that you arrange however you like.

npm install floki-elements

Usage as custom elements

Two files and no build step. The stylesheet themes the page; the module registers the elements:

<html data-mode="dark" data-accent="purple">
  <head>
    <link rel="stylesheet" href="node_modules/floki-elements/dist/tokens.css" />
    <script type="module" src="node_modules/floki-elements/dist/index.js"></script>
  </head>
  <body>
    <fx-callout>
      <span slot="leading">🪶</span>
      <span>That is the whole setup.</span>
    </fx-callout>
  </body>
</html>

With a bundler, the same two lines are imports:

import 'floki-elements';
import 'floki-elements/tokens.css';

Importing the package registers every element and adopts the theme into the document. Linking the stylesheet as well is not redundant: it is what makes the first paint correct, before any module has been fetched.

There are no per-element entry points — the package exports ., ./react, ./tokens.css and ./custom-elements.json, and the root entry pulls in all of the primitives on purpose. Registration is a side effect a bundler cannot see through, so a partial import would hand you unupgraded markup: text with no styling and no shadow root.

Attributes and properties

Strings and booleans are attributes, and they work from markup alone:

<fx-button variant="primary" size="lg">Publish</fx-button>
<fx-rail-item as="a" href="/blog" active>Blog</fx-rail-item>

Anything that is an array or an object is a property only — there is no attribute form to parse. <fx-combobox> and <fx-command-menu> are the two that matter:

document.querySelector('fx-combobox').options = [
  { value: 'flexoki', label: 'Flexoki' },
  { value: 'notion', label: 'Notion' },
];

Events

Every fx-* event is dispatched with bubbles: true, composed: true, so it crosses the shadow boundary and you can listen from anywhere above it — including on document:

| Event | From | | ----------- | --------------------------------------------------------------------- | | fx-input | form controls, on each keystroke or while dragging | | fx-change | form controls, on commit — plus <fx-tabs>, <fx-segmented>, <fx-pagination>, <fx-accordion>, <fx-toggle>, <fx-toggle-group> | | fx-select | <fx-menu>, <fx-context-menu>, <fx-command-menu>, <fx-tree>, <fx-menubar> | | fx-open / fx-close | <fx-dialog>, <fx-drawer>, <fx-popover>, <fx-alert-dialog>, <fx-navigation-menu-item>; fx-close also from <fx-toast>, and carries a reason (<fx-alert-dialog>'s is cancel / action / escape / api) | | fx-toggle | <fx-disclosure> | | fx-complete | <fx-input-otp>, when the last cell is filled |

fx-input and fx-change are the one contract every form control shares, which is why a single listener on the form covers all of them.

form.addEventListener('fx-change', (e) => console.log(e.detail.value));

Form controls dispatch native input and change alongside their fx- counterparts, so existing form code keeps working unchanged. The value also reaches FormData and constraint validation runs — see Forms under The primitives.

A few things are imperative rather than declarative: dialog.show() and dialog.hide(reason), and the toast() function shown under Overlays in the same section.

Editor support

custom-elements.json ships in the package, and most editors read it automatically for attribute, slot and part autocomplete. Each primitive also declares itself in HTMLElementTagNameMap, so document.querySelector('fx-button') is typed as FxButton in TypeScript with no cast.

Usage in React

Typed wrappers live at floki-elements/react. They are built with @lit/react, which is an optional peer dependency alongside react >= 18 — install both:

npm install floki-elements @lit/react

Import the stylesheet yourself. The React subpath registers the elements it wraps but deliberately does not touch the document, so it never adopts the theme the way the root entry does. Load the tokens once, at the app root:

import 'floki-elements/tokens.css';

Then the components are ordinary React components. Props are set as properties, so arrays and objects pass straight through, and each fx-* event arrives as an onFx* handler with a typed detail:

import { useState } from 'react';
import { FxField, FxInput, FxSwitch, FxButton } from 'floki-elements/react';

function ProfileForm() {
  const [handle, setHandle] = useState('');
  const [notify, setNotify] = useState(false);

  return (
    <form>
      <FxField label="Handle" description="How you appear in mentions.">
        <FxInput
          name="handle"
          value={handle}
          onFxChange={(e) => setHandle(e.detail.value)}
        />
      </FxField>

      <FxSwitch checked={notify} onFxChange={(e) => setNotify(e.detail.checked)}>
        Email me on replies
      </FxSwitch>

      <FxButton variant="primary" onClick={() => save(handle)}>
        <span slot="leading">💾</span>
        Save
      </FxButton>
    </form>
  );
}

Slots are expressed the way they are in HTML — a child carrying a slot attribute — because these are still custom elements underneath. Ordinary DOM events like onClick work as they always have; the onFx* props exist for the custom events, which React has no way to know about on its own.

A ref gives you the element instance, which is how you reach the imperative APIs:

const dialog = useRef(null);

<FxButton onClick={() => dialog.current.show()}>Open</FxButton>
<FxDialog ref={dialog} onFxClose={(e) => console.log(e.detail.reason)} />

toast() has no wrapper — it is a function, not an element — so it comes from the root entry. It appends an <fx-toaster> for you if the page has none:

import { toast } from 'floki-elements';

Note that this pulls in the root entry, which registers the full element set and adopts the theme. That is harmless, but it is also why the tokens import above is the thing to rely on: it works whether or not anything ever imports the root.

Server rendering

Every primitive calls customElements.define when its module is evaluated, and that global does not exist in Node — so these components have to be imported on the client. In Next.js that means a 'use client' boundary, and next/dynamic with ssr: false for the route or component that renders them:

const ProfileForm = dynamic(() => import('./profile-form'), { ssr: false });

TypeScript

The wrappers are the typed path: the package declares no JSX.IntrinsicElements entries, so writing <fx-button> directly in a .tsx file is an error. Import FxButton instead and both props and events are checked.

The theme is static CSS

tokens.css is a plain stylesheet. Correct appearance never depends on JavaScript upgrading — link it and the page is themed at first paint, with no flash and no <flexoki-config> anywhere.

Everything is keyed off four attributes on <html>:

| Attribute | Values | | --------------- | ----------------------------------------------------------------- | | data-mode | light (default) · dark · auto (follows prefers-color-scheme) | | data-accent | blue (default) · green · red · orange · yellow · cyan · purple · magenta | | data-density | comfortable (default) · compact | | data-contrast | normal (default) · high |

Beyond colour, the token layer also ships a stacking scale (--layer-base--layer-toast), breakpoints, icon sizing, control metrics and status colours — the last re-derived per mode rather than reused, like every other alpha in the theme.

Toggling data-mode is the entire dark-mode implementation. For a no-flash restore, set it before first paint:

<script>
  const saved = localStorage.getItem('mode');
  if (saved) document.documentElement.dataset.mode = saved;
</script>

Theming, in increasing power

1 — Attributes. The table above. Covers the common cases with zero CSS.

2 — Token overrides in plain CSS. Override any semantic token on any scope. It cascades to descendants and pierces shadow DOM:

.promo {
  --accent: #7a3e9d;
  --accent-wash: rgba(122, 62, 157, 0.12);
}

The library's own defaults live in a @layer fx.tokens, and unlayered CSS outranks any layer — so your plain rule always wins, with no !important and no specificity war. This is deliberate: the no-JavaScript path has to stay fully sufficient, which means nothing the library does at runtime may outrank what you wrote by hand.

3 — ::part for structure. Every stylable internal node exposes a part:

fx-rail-item::part(base) {
  padding-block: 2px;
}

See custom-elements.json for the full slot/part/attribute surface of every element; most editors read it for autocomplete automatically.

<flexoki-config> (optional)

A convenience layer over the static theme, never a requirement for it. Delete it and everything still looks right; only live retheming stops working.

<flexoki-config mode="dark" accent="green" density="compact"></flexoki-config>
const config = document.querySelector('flexoki-config');
config.toggleMode();                                  // flips the resolved mode
config.overrides = { '--color-bg': '#101010' };       // bulk token overrides
config.root = document.querySelector('#panel');       // scope a second theme

It adopts whatever data-* attributes are already on the root rather than overwriting them — that is what makes the pre-paint script above work — and it writes at author level rather than inline, so your own CSS still wins. It is a singleton per root: a second instance warns and no-ops until the first disconnects, because SPA remounts and hot reload will mount two.

The primitives

The rail — a shell and its parts. None of them name a "workspace", a "search", or a nav order.

  • <fx-rail> — tinted surface, hairline divider, sticky scroll region
  • <fx-rail-item> — a row: leading / label / secondary / meta / trailing
  • <fx-rail-label> — a faint uppercase section label
  • <fx-rail-spacer> — a flex spacer, if you want a bottom group
  • <fx-rail-text> — a muted prose block
  • <fx-disclosure> — optional, built on real <details>/<summary>

Layout<fx-stack>, <fx-grid>, <fx-container>, <fx-box>, <fx-divider>, <fx-aspect>, <fx-scroll-area>, <fx-icon>

Spacing attributes map to the scale — <fx-stack gap="4"> is var(--space-4), and an off-scale value simply doesn't match. The mapping is plain attribute selectors, so it is right before any JavaScript runs.

Forms<fx-field>, <fx-label>, <fx-input>, <fx-input-group>, <fx-input-otp>, <fx-textarea>, <fx-select>, <fx-checkbox>, <fx-radio>

  • <fx-radio-group>, <fx-switch>, <fx-toggle> + <fx-toggle-group>, <fx-slider>, <fx-combobox>, <fx-button> + <fx-button-group>, <fx-icon-button>

<fx-toggle> is a button whose action sticks (bold in a toolbar); <fx-toggle-group> clusters them, single or multiple. <fx-input-otp> is a form-associated one-time-code field, <fx-input-group> wraps a bare control and its addons in one surface, and <fx-button-group> welds buttons into one unit.

Buttons carry one variant vocabulary, shared with <fx-icon-button>: primary (accent fill), secondary (filled neutral), outline (hairline plus hover wash — the button's default), ghost (the wash alone, for toolbars) and danger. outline means the same thing on <fx-badge> and <fx-card>.

Every control is a form-associated custom element. The value reaches FormData, the control resets and restores with its form, and constraint validation runs — so a <form> full of these behaves like a <form>:

new FormData(form).get('handle');   // the value of <fx-input name="handle">
form.checkValidity();               // includes every fx- control

<fx-field> wires aria-labelledby, aria-describedby and aria-invalid onto whatever you slot into it — including a plain <input>.

Overlays<fx-popover>, <fx-tooltip>, <fx-dialog>, <fx-alert-dialog>, <fx-drawer>, <fx-toast> + <fx-toaster>, <fx-hover-card>

<fx-alert-dialog> is the confirm variant: role="alertdialog", described by its body, and non-dismissible by default so a destructive choice is answered rather than dismissed.

Dialog and drawer are the native <dialog>, so the top layer, the inert page behind and focus containment come from the platform. The rest share one in-house positioner — no positioning dependency. Toasts have an imperative entry point:

import { toast } from 'floki-elements';
toast('Moved to trash', { tone: 'neutral' });

Navigation<fx-tabs> + <fx-tab> + <fx-tab-panel>, <fx-segmented>

  • <fx-segment>, <fx-pagination>, <fx-menu> + <fx-menu-item> + <fx-menu-separator>, <fx-context-menu>, <fx-command-menu>, <fx-menubar>
  • <fx-menubar-menu>, <fx-navigation-menu> + <fx-navigation-menu-item>, <fx-breadcrumb> + <fx-breadcrumb-item>

Each composite is a single tab stop with the arrow keys moving inside it. <fx-menubar> is the app-style menu bar and <fx-navigation-menu> the site nav with dropdown panels — both built on the same menu rows and positioner as <fx-menu>. Breadcrumb was originally cut in design review as app chrome; it is back by request, shipped as a styled <nav> and crumbs that assume no particular navigation structure (see docs/spec.md §11).

Content<fx-callout>, <fx-alert>, <fx-eyebrow>, <fx-inline-link>, <fx-list>, <fx-row>, <fx-card>, <fx-avatar>, <fx-badge>, <fx-kbd>, <fx-blockquote>, <fx-code>, <fx-table>, <fx-accordion>, <fx-tree> + <fx-tree-item>, <fx-page-header>

<fx-alert> is the status message — toned on the status ramp, leading with a glyph, and a live region — where <fx-callout> is the editorial aside. <fx-badge> is the Notion marker: near-square, small radius, tight padding.

<fx-table> styles a slotted native <table> and owns no data — no sorting, no columns, no rows. <fx-page-header> is icon, cover and title only.

State & feedback<fx-skeleton>, <fx-spinner>, <fx-progress>, <fx-empty-state>, <fx-inline-status>

<fx-row> and <fx-rail-item> are the same primitive, implemented once and aliased twice; they differ only in metrics.

The full catalog, including what is deliberately out of scope, is in docs/spec.md.

The composability proof

Two unrelated sidebars, same primitives, no shared structure:

<!-- A personal site -->
<fx-rail>
  <fx-rail-item as="div"><span slot="leading">M</span> Mit Vasani</fx-rail-item>
  <fx-rail-text>Software engineer, anime fan, avid reader.</fx-rail-text>
  <fx-rail-item as="a" href="/" active><span slot="leading">🏠</span> Home</fx-rail-item>
  <fx-rail-item as="a" href="/blog"><span slot="leading">✍️</span> Blog</fx-rail-item>
  <fx-rail-spacer></fx-rail-spacer>
  <fx-rail-item as="a" href="https://github.com/…"><span slot="leading">↗</span> GitHub</fx-rail-item>
</fx-rail>

<!-- A docs table of contents -->
<fx-rail>
  <fx-rail-label>Getting started</fx-rail-label>
  <fx-rail-item as="a" href="#install" active>Install</fx-rail-item>
  <fx-rail-item as="a" href="#theming">Theming</fx-rail-item>
</fx-rail>

Neither reads as a reproduction of any particular app's sidebar; both read as the same styling. That is the goal.

Accessibility

  • :focus-visible outline keyed to --accent on every interactive primitive
  • prefers-reduced-motion: reduce disables transitions
  • an active nav row carries aria-current="page"; the wash is visual only
  • the disclosure is a real <details>/<summary>

The look runs low-contrast on purpose — 65%-alpha secondary text, hairline borders. That is fine for decoration and secondary UI, but verify WCAG on actual reading content. data-contrast="high" is the opt-in escape hatch: secondary text moves onto solid ramp stops and hairlines take the stronger alphas.

The rail does not hide itself at a breakpoint we chose. collapsed is a documented state you drive from your own media queries, because that decision belongs to the page. breakpoints, above() and below() are exported so your media queries and the --bp-* tokens cannot drift apart:

import { below } from 'floki-elements';
// `(max-width: 767.98px)` — usable inside a `css` template literal

The usual arrangement on a phone is the rail's own markup inside an <fx-drawer>, opened from a button the page owns. The gallery in demo/ does exactly that, in about fifteen lines of demo.js.

Development

npm install
npm run dev      # the demo gallery at /demo/index.html
npm test         # cascade + component tests, in a real browser
npm run build    # ESM + .d.ts + custom-elements.json

src/styles/tokens.css is generated from src/styles/token-values.ts — the single source of truth for every token — by npm run tokens. Edit the TypeScript, never the CSS.

License

MIT