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

@ondrotoman/lektor

v1.1.1

Published

Interactive step-by-step tutorials for web apps

Downloads

605

Readme

@ondrotoman/lektor

A lightweight, framework-agnostic, TypeScript-first library for building step-by-step tutorials, guided tours, and precisely positioned dialogs.

npm version license

Overview

Lektor is a lightweight, framework-agnostic library for creating interactive step-by-step tutorials and guided experiences. It is written with TypeScript in mind and provides flexible dialog positioning, lifecycle hooks, and runtime step management.

Lektor is intentionally unopinionated about styling, allowing you to integrate it into an existing application's visual language.

Installation

Install Lektor using your preferred package manager:

# npm
npm install @ondrotoman/lektor

# pnpm
pnpm add @ondrotoman/lektor

# yarn
yarn add @ondrotoman/lektor

⚠️ Security Notice

Be careful with body and header content.

Lektor and Step body and headers can contain HTML, not just plain text.

If you use user-provided or untrusted content, unsanitized HTML can introduce XSS vulnerabilities.

🔒 Recommendation

Never pass untrusted content directly into body and header.

If HTML is required, make sure it is properly sanitized first.

new Lektor({
  header: safeContent,
})
new Step({
  header: safeContent,
  body: safeContent,
})

Treat dynamic HTML as untrusted by default.

Usage

Lektor supports both static step definitions and dynamically added steps.

1. Import Lektor

import Lektor, { Position, Step, type LektorCallbacks } from '@ondrotoman/lektor'

2. Create a tour with steps

You can define steps when creating a Lektor instance:

const siteTour = new Lektor({
  steps: [
    new Step({
      element: document.getElementById('form'),
      body: 'This can be simple text or rich HTML!',
    }),
  ],
})

Add steps dynamically

Steps can also be added after the Lektor instance has been created:

siteTour.addStep(
  new Step({
    element: document.getElementById('form'),
    body: 'This can be simple text or rich HTML!',
  }),
)

Create a centered message

A step does not require an associated element. This is useful for displaying a message in the center of the screen:

siteTour.addStep(
  new Step({
    body: 'This will be shown in center of the screen!',
  }),
)

Assign an element dynamically

You can create a step without an element and assign one later:

const newItemStep = new Step({
  body: 'This is your new item in the list!',
})

siteTour.addStep(newItemStep)

newItemStep.setElement(document.getElementById('#item-' + id))

Start the tour

Once your steps have been configured, start the tour with start():

siteTour.start()

Hooks

Lektor provides lifecycle hooks that allow you to control the tour based on application state.

For example, you can prevent the user from continuing until an input contains a value:

let keyupHandler: ((e: KeyboardEvent) => void) | null = null

const validateInput = (value: string): boolean => {
  return value.length >= 1
}

new Step({
  element: document.getElementById('input'),
  body: 'Type whatever you want right here',

  onMounted: (step: Step | null, callbacks: LektorCallbacks) => {
    if (!validateInput(step.element.value)) {
      callbacks.disableNext()
    }

    keyupHandler = () => {
      if (validateInput(step.element.value)) {
        callbacks.enableNext()
      } else {
        callbacks.disableNext()
      }
    }

    step.element.addEventListener('keyup', keyupHandler)
    step.element.focus()
  },

  onUnmounted: (step: Step | null, callbacks: LektorCallbacks) => {
    if (keyupHandler) {
      callbacks.enableNext()
      step.element.removeEventListener('keyup', keyupHandler)
    }
  },
})

When the step is mounted, the Next action remains disabled until the input contains at least one character. The event listener is removed when the step is unmounted.

API

Lektor

Methods

interface Lektor {
  isActive: () => boolean
  start: () => void
  end: () => void
  close: () => void
  previous: () => Lektor
  next: () => Lektor
  disablePrevious: () => Lektor
  enablePrevious: () => Lektor
  disableNext: () => Lektor
  enableNext: () => Lektor
  addStep: (step: Step) => Lektor
  removeStep: (step: Step) => Lektor
}

Constructor parameters

interface LektorParams {
  steps?: Step[]
  header?: string
  previousButtonText?: string
  nextButtonText?: string
  endButtonText?: string
  classPrefix?: string
  dialogOffset?: number
  dialogPositon?: Position
  enableKeyboardNavigation?: boolean
  onStart?: () => void
  onEnd?: () => void
  onStepChange?: () => void
  onClose?: () => void
}

Lifecycle hooks

onStart: () => void
onEnd: () => void
onStepChange: () => void
onClose: () => void

Step

Methods

interface Step {
  setHeader: () => Step
  setBody: () => Step
  setElement: () => Step
}

Constructor parameters

interface StepParams {
  body: string
  element?: HTMLElement | null
  header?: string
  dialogPosition?: Position
  onMounted?: (step: Step | null, callbacks: LektorCallbacks) => void | null
  onUnmounted?: (step: Step | null, callbacks: LektorCallbacks) => void | null
}

Hooks

onMounted: (step: Step | null, callbacks: LektorCallbacks) => void

onUnmounted: (step: Step | null, callbacks: LektorCallbacks) => void

Lektor callbacks

Each Step hook receives a set of callbacks that provide access to the active Lektor instance:

interface LektorCallbacks {
  previous: () => Lektor
  next: () => Lektor
  disablePrevious: () => Lektor
  enablePrevious: () => Lektor
  disableNext: () => Lektor
  enableNext: () => Lektor
  addStep: (step: Step) => Lektor
  removeStep: (step: Step) => Lektor
}

Styling

Lektor does not impose a styling system, so you can customize its appearance to match your application.

The following example provides a clean starting point:

.lektor-dialog {
  display: flex;
  flex-direction: column;
  background-color: #ffffff;
  color: #0f172a;
  width: 100%;
  max-width: 420px;
  padding: 20px;
  border-radius: 16px;
  border: 1px solid #e2e8f0;
  box-shadow:
    0 10px 25px -5px rgba(0, 0, 0, 0.08),
    0 8px 10px -6px rgba(0, 0, 0, 0.04);
  box-sizing: border-box;
  transform: translateY(-30px);
  animation: slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.lektor-curtain {
  background-color: rgba(15, 23, 42, 0.45);
  backdrop-filter: blur(1px);
}

.lektor-active-element {
  box-shadow: 0 0 25px 15px rgba(107, 243, 175, 0.8);
}

.lektor-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.lektor-text {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  color: #0f172a;
}

.lektor-close-button {
  background: transparent;
  border: none;
  color: #64748b;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.lektor-close-button:hover {
  background-color: #f1f5f9;
  color: #0f172a;
}

.lektor-body {
  font-size: 0.95rem;
  line-height: 1.6;
  color: #475569;
  margin-bottom: 24px;
}

.lektor-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.lektor-previous-button,
.lektor-next-button,
.lektor-end-button {
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  padding: 10px 20px;
  border-radius: 9999px;
  cursor: pointer;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.lektor-previous-button {
  background-color: #ffffff;
  color: #0f172a;
  border: 1px solid #e2e8f0;
}

.lektor-previous-button:hover:not(:disabled) {
  background-color: #f8fafc;
  border-color: #cbd5e1;
}

.lektor-previous-button:disabled {
  border-color: #e2e8f0;
}

.lektor-next-button {
  background-color: #0f172a;
  color: #ffffff;
  border: 1px solid #0f172a;
}

.lektor-next-button:hover {
  background-color: #1e293b;
  border-color: #1e293b;
}

.lektor-previous-button:disabled,
.lektor-next-button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.lektor-end-button {
  background-color: #6bf3af;
  color: #0f172a;
  border: 1px solid #6bf3af;
}

.lektor-end-button:hover {
  background-color: #63eba7;
  border-color: #63eba7;
}

.lektor-next-button:active,
.lektor-end-button:active {
}