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

@oeltkit/components

v0.1.0

Published

OELT interaction components — accessible, themeable, auto-tracked custom elements (<oelt-*>).

Readme

@oeltkit/components

Accessible, themeable, auto-tracked custom elements (<oelt-*>) for OELT courses. Vanilla custom elements — no framework, zero runtime dependencies. Each is authored as plain HTML; it emits an oelt-interaction event that the runtime maps to SCORM/cmi5/web tracking — the author writes no tracking code.

Status: beta (pending manual NVDA/VoiceOver passes). Contracts: specs/components/. Base contract: base.md.

Loading the bundle (or importing the ESM entry) registers the elements:

<script src="oelt.min.js"></script>
<!-- or: import "@oeltkit/components"; -->

All three are light DOM (author CSS/themes reach the content), styled via --oelt-* tokens, themeable via ::part(), fully keyboard-operable, and screen-reader documented in their specs. They persist UI state through oelt.state (quota-enforced) and round-trip through suspend/resume.

<oelt-mcq> — multiple choice / multiple response

Single-answer (radio) or multiple-answer (checkbox), built on a native <fieldset>.

<!-- single answer -->
<oelt-mcq id="q1" mode="single" key="b">
  <p slot="prompt">Which standard does cmi5 build on?</p>
  <oelt-option value="a">SCORM 1.2</oelt-option>
  <oelt-option value="b">xAPI</oelt-option>
  <oelt-option value="c">AICC</oelt-option>
  <p slot="correct">Right — cmi5 is an xAPI profile.</p>
  <p slot="incorrect">Not quite — cmi5 is built on xAPI.</p>
</oelt-mcq>

<!-- multiple response: key lists every correct value -->
<oelt-mcq id="q2" mode="multiple" key="a c">
  <p slot="prompt">Select every accessibility requirement (choose all that apply).</p>
  <oelt-option value="a">Keyboard operable</oelt-option>
  <oelt-option value="b">Mouse only</oelt-option>
  <oelt-option value="c">Visible focus</oelt-option>
</oelt-mcq>

<!-- ungraded poll: omit key, add manual-grade -->
<oelt-mcq id="poll1" manual-grade>
  <p slot="prompt">How confident are you?</p>
  <oelt-option value="low">Low</oelt-option>
  <oelt-option value="high">High</oelt-option>
</oelt-mcq>

Emits oelt-interaction on submit (type: "choice"). Spec: mcq.md.

<oelt-branching> — branching scenario

A decision graph; each choice emits an interaction, resume restores position by stored path (not content).

<oelt-branching id="scenario1" start="n1">
  <script type="application/json">
    {
      "nodes": {
        "n1": {
          "text": "<p>A learner shares a password with you. You…</p>",
          "choices": [
            { "label": "Use it", "to": "bad", "value": "use" },
            { "label": "Report it", "to": "good", "value": "report" }
          ]
        },
        "good": { "text": "<p>Correct — report and rotate it.</p>", "end": "passed" },
        "bad": {
          "text": "<p>That's a breach.</p>",
          "choices": [{ "label": "Back", "to": "n1", "value": "back" }]
        }
      }
    }
  </script>
</oelt-branching>

<!-- or load the graph from a file -->
<oelt-branching id="scenario2" src="scenarios/onboarding.json" start="intro"></oelt-branching>

Emits oelt-interaction per branch and at the terminal node (type: "sequencing"). Spec: branching.md.

<oelt-media> — accessible media

Wraps native <video>/<audio>; refuses to render without captions or a transcript; adds a transcript disclosure; reports completion at a watched threshold.

<oelt-media id="intro-video" threshold="0.9">
  <video controls preload="metadata" width="480">
    <source src="media/intro.mp4" type="video/mp4" />
    <track kind="captions" src="media/intro.en.vtt" srclang="en" label="English" default />
  </video>
  <div slot="transcript">
    <p>Full transcript of the introduction…</p>
  </div>
</oelt-media>

A <video> without a captions track (and no transcript) renders a visible error instead of the player — this also fails oelt validate a11y. Emits oelt-interaction (type: "media", result: "completed") once past the threshold. Spec: media.md.

<oelt-text-entry> — short text / numeric response

A single-line free-response question. Enhances the authored prompt into a native <label> + <input>; grades on submit and reports one oelt-interaction. Text mode matches case-insensitively after trimming/collapsing whitespace; numeric mode matches within an absolute tolerance.

<!-- text: one accepted answer, or several via | -->
<oelt-text-entry id="capital" answer="Paris">
  <p slot="prompt">What is the capital of France?</p>
  <p slot="correct">Correct — Paris.</p>
  <p slot="incorrect">Not quite — it's Paris.</p>
</oelt-text-entry>

<!-- numeric with tolerance -->
<oelt-text-entry id="pi" mode="numeric" answer="3.14" tolerance="0.01">
  <p slot="prompt">Estimate π to two decimal places.</p>
</oelt-text-entry>

<!-- open response (no key): captured, marked completed -->
<oelt-text-entry id="why" manual-grade>
  <p slot="prompt">In a word, how did this lesson feel?</p>
</oelt-text-entry>

Enter in the input submits. Emits oelt-interaction with type: "fill-in" (text) or type: "numeric", result passed/failed (or completed for manual-grade), and response set to the raw input. case-sensitive forces exact-case text matching; retry allows re-answering. Spec: text-entry.md.

Screen-reader: the prompt is the input's <label>; on submit, focus moves to the aria-live="polite" feedback and correctness is conveyed in text (visually-hidden "Correct"/"Incorrect" prefix), never colour alone. Numeric mode uses inputmode="decimal" on a text input (not type="number") for a numeric soft keyboard without spinbutton/locale pitfalls.

<oelt-quiz> — question container

Wraps question components, aggregates their results into one weighted score, and reports a single interaction. Children (<oelt-mcq>, <oelt-text-entry>, …) still report their own item-level interactions; the quiz adds the aggregate the score rule consumes.

<oelt-quiz id="final" mastery="0.7">
  <oelt-mcq id="q1" mode="single" key="b" weight="1">
    <p slot="prompt">Which standard is recommended for new content?</p>
    <oelt-option value="a">SCORM 1.2</oelt-option>
    <oelt-option value="b">cmi5</oelt-option>
  </oelt-mcq>
  <oelt-text-entry id="q2" answer="cmi5" weight="2">
    <p slot="prompt">Name that standard (one word).</p>
  </oelt-text-entry>
</oelt-quiz>

Each question may carry a weight (default 1); the quiz score is Σ(weightᵢ·scoreᵢ)/Σ(weightᵢ). mastery (0–1) makes the result passed/failed (else completed). pool="N" shows a random N of the questions (a question bank); shuffle randomizes order. The quiz emits oelt-interaction (type: "performance") once every active question is answered.

Screen-reader: a ::part(status) live region announces progress ("Answered 1 of 2 questions") and the final outcome ("Quiz complete. Score 75%."); pooled-out questions get hidden. Spec: quiz.md.

<oelt-likert> — rating-scale / survey item

A single-select rating scale. Survey semantics: no correct answer, so it emits result: "completed" with no score (and the quiz ignores it). Give explicit labels per point, or generate a numeric scale with end anchors.

<!-- explicit labels (recommended) -->
<oelt-likert id="confidence">
  <p slot="prompt">I feel confident applying what I learned.</p>
  <oelt-option value="1">Strongly disagree</oelt-option>
  <oelt-option value="3">Neutral</oelt-option>
  <oelt-option value="5">Strongly agree</oelt-option>
</oelt-likert>

<!-- generated N-point scale with end anchors -->
<oelt-likert id="ease" scale="5" low-label="Very hard" high-label="Very easy">
  <p slot="prompt">How easy was this lesson?</p>
</oelt-likert>

Emits oelt-interaction (type: "likert", result: "completed", response = chosen value). Built on a native <fieldset> of radios — arrow keys move/select, the prompt is the <legend>. In generated mode the end anchors fold into the first/last radio labels ("1 — Very hard"). Spec: likert.md.

<oelt-ordering> — sequence / ranking

Reorder items into the correct sequence. Keyboard-first drag-and-drop (the family model — see dnd-family.md); pointer drag is an enhancement.

<oelt-ordering id="lifecycle">
  <p slot="prompt">Put the steps in order, first to last.</p>
  <oelt-item value="plan">Plan</oelt-item>
  <oelt-item value="build">Build</oelt-item>
  <oelt-item value="test">Test</oelt-item>
  <oelt-item value="ship">Ship</oelt-item>
</oelt-ordering>

Authored order is correct; items start shuffled. Keyboard: focus an item, Space to pick up, / to move, Space to drop, Esc to cancel — every step announced via an assertive live region. On Check emits oelt-interaction (type: "sequencing", score = fraction in the correct position, passed iff exact, response = ordered values). Spec: ordering.md.

<oelt-matching> — match values to prompts

Assign each value to its correct prompt. Same keyboard-first model as ordering.

<oelt-matching id="capitals">
  <p slot="prompt">Match each country to its capital.</p>
  <oelt-pair prompt="France" value="paris">Paris</oelt-pair>
  <oelt-pair prompt="Japan" value="tokyo">Tokyo</oelt-pair>
  <oelt-pair prompt="Egypt" value="cairo">Cairo</oelt-pair>
</oelt-matching>

Values start shuffled in a bank. Keyboard: focus a value, Space to pick up, / to move it across the prompt targets (and the bank), Space to drop, Esc to cancel — every step announced. One value per target (dropping displaces the previous one to the bank). On Check emits oelt-interaction (type: "matching", score = fraction of targets holding the correct value, passed iff all, response = prompt=value pairs). Spec: matching.md.

<oelt-categorize> — sort items into categories

Sort tokens into category buckets. A bucket holds many tokens (unlike matching's 1:1).

<oelt-categorize id="animals">
  <p slot="prompt">Sort each animal into its group.</p>
  <oelt-bucket value="mammals">Mammals</oelt-bucket>
  <oelt-bucket value="birds">Birds</oelt-bucket>
  <oelt-token bucket="mammals" value="dog">Dog</oelt-token>
  <oelt-token bucket="birds" value="eagle">Eagle</oelt-token>
  <oelt-token bucket="mammals" value="cat">Cat</oelt-token>
</oelt-categorize>

Tokens start shuffled in a bank. Keyboard: focus a token, Space to pick up, / across buckets (and the bank), Space to drop, Esc to cancel — every step announced. On Check emits oelt-interaction (type: "matching", score = fraction of tokens in their correct bucket, passed iff all, response = token=bucket pairs). Spec: categorize.md.

The drag-and-drop family (<oelt-ordering>, <oelt-matching>, <oelt-categorize>) shares one keyboard pick-up/move/drop model, documented once in dnd-family.md and implemented by a shared GrabController. Native <button> items, no deprecated aria-grabbed, SR announcement on every state change, prefers-reduced-motion honored.

Presentation — <oelt-tabs>, <oelt-accordion>, <oelt-flip-cards>

Accessible content-organizing widgets. Presentation only — they don't track or score (engagement is the course's pages-viewed progress) and are stateless. Spec: presentation.md.

<oelt-tabs id="topics">
  <oelt-tab label="Overview"><p>…</p></oelt-tab>
  <oelt-tab label="Details"><p>…</p></oelt-tab>
</oelt-tabs>

<oelt-accordion single>
  <!-- `single` = one section open at a time -->
  <oelt-panel label="What is SCORM?"><p>…</p></oelt-panel>
  <oelt-panel label="What is cmi5?"><p>…</p></oelt-panel>
</oelt-accordion>

<oelt-flip-cards>
  <oelt-card front="Mercury"><p>Closest planet to the Sun.</p></oelt-card>
  <oelt-card front="Venus"><p>Hottest planet.</p></oelt-card>
</oelt-flip-cards>
  • Tabs follow the WAI-ARIA Tabs pattern: / (wrapping), Home/End, roving tabindex, automatic activation.
  • Accordion upgrades each panel to a native <details>/<summary> (accessible by construction); single uses the native shared-name mechanism for exclusive open.
  • Flip-cards are <button>s with aria-pressed; the hidden face is hidden so SR reads only the visible side; the flip animation is suppressed under prefers-reduced-motion.

<oelt-hotspot> — image hotspot selection

Select the correct region(s) on an image. Accessible by design: hotspots are labelled, keyboard-operable <button>s positioned over the image (percentage coords) — never pixel-hunting, so screen-reader users answer from the labels alone.

<oelt-hotspot id="cell" mode="multiple" src="images/cell.svg" alt="Diagram of an animal cell">
  <p slot="prompt">Select every structure that stores genetic material.</p>
  <oelt-area value="nucleus" x="10" y="18" w="26" h="30" label="Nucleus" correct></oelt-area>
  <oelt-area value="mito" x="55" y="50" w="22" h="20" label="Mitochondria"></oelt-area>
  <oelt-area value="dna" x="16" y="24" w="10" h="10" label="DNA" correct></oelt-area>
</oelt-hotspot>

<oelt-area> places a labelled hotspot by percentage of the image box (x/y/w/h, 0–100), marking answers with correct. The image needs alt. Hotspots are toggle buttons in a role="group" labelled by the prompt; graded exactly like <oelt-mcq> (type: "choice", single or multiple with partial credit). Spec: hotspot.md.

<oelt-reflection> — free-text reflection

An open-ended written response. Not auto-graded — the text is captured and recorded as a completed interaction.

<oelt-reflection id="takeaway" maxlength="500">
  <p slot="prompt">What is one thing you will apply from this lesson?</p>
</oelt-reflection>

A native <textarea> (prompt is its <label>, live character counter); Enter inserts newlines (it doesn't submit). On Save it emits oelt-interaction (type: "fill-in", result: "completed", no score) and fires the evaluation hook — an oelt-reflection CustomEvent { id, text, provideFeedback({ message }) } so a future cloud LLM evaluator can assess the text and surface feedback. v0 ships no evaluator (the contract exists; evaluation is a Tier-3 cloud concern). Editable/revisable; resume-safe. Spec: reflection.md.

Develop

npm run build -w @oeltkit/components   # ESM + IIFE (dist/oelt.min.js)
npm test                               # unit (grading logic)
npm run test:a11y                      # Playwright: axe-clean, keyboard, tracking-in-harness
npm run harness -- examples/components-demo   # drive all three in the fake LMS

Demos live in harness/demos/; the in-harness tracking course is examples/components-demo/.