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

@spranclabs/flowmark

v1.0.0

Published

Flowmark - Zero-dependency highlighting library with cross-element selection

Readme

@spranclabs/flowmark

npm version npm downloads License: MIT Build Status codecov

Flowmark - Zero-dependency highlighting library that flows across boundaries.

Seamless cross-element text highlighting with smart normalization.

Features

  • Cross-element highlighting - Select text across multiple DOM elements
  • Precise text matching - Context-aware text positioning with prefix/suffix validation
  • Zero dependencies - Pure vanilla JavaScript/TypeScript
  • Tiny bundle size - ~10KB minified
  • Framework agnostic - Works with React, Vue, Angular, or plain HTML
  • Customizable UI - Fully customizable highlight colors and styles
  • Storage adapters - Support for LocalStorage, PostMessage (iframes), or custom backends
  • Mobile friendly - Touch selection support

Installation

npm install @spranclabs/flowmark
# or
pnpm add @spranclabs/flowmark
# or
yarn add @spranclabs/flowmark

Quick Start

import { Highlighter, LocalStorageAdapter } from '@spranclabs/flowmark'

// 1. Create storage adapter
const storage = new LocalStorageAdapter('my-highlights')

// 2. Initialize highlighter
const highlighter = new Highlighter(document.body, {
  storage: storage,
  defaultColor: 'rgba(255, 235, 59, 0.4)',
  enableCrossElement: true,
  showSelectionUI: true,

  // Event callbacks
  onHighlightClick: (highlightId, event) => {
    console.log('Highlight clicked:', highlightId)
    // Show delete confirmation, etc.
  },
  onHighlight: (highlight) => {
    console.log('New highlight created:', highlight)
  },
  onRemove: (highlightId) => {
    console.log('Highlight removed:', highlightId)
  }
})

// 3. Initialize (loads highlights and sets up event listeners)
await highlighter.init()

// Now users can select text and create highlights!

Highlighter Class

Constructor

new Highlighter(container: HTMLElement, config: HighlighterConfig)

Parameters:

  • container: HTMLElement - Container element to enable highlighting within (e.g., document.body)
  • config: HighlighterConfig - Configuration options (see below)

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | storage | StorageAdapter | - | Required. Storage adapter for persisting highlights | | defaultColor | string | 'rgba(255, 235, 59, 0.4)' | Default highlight color (CSS color value) | | enableCrossElement | boolean | true | Allow text selections across multiple DOM elements | | showSelectionUI | boolean | true | Show action toolbar when text is selected | | highlightClassName | string | 'highlight' | CSS class name applied to highlight <mark> elements | | onHighlightClick | (id: string, event: MouseEvent) => void | - | Called when a highlight is clicked | | onHighlight | (highlight: Highlight) => void | - | Called when a new highlight is created | | onRemove | (highlightId: string) => void | - | Called when a highlight is removed | | onUpdate | (highlight: Highlight) => void | - | Called when a highlight is updated | | selectionUI | SelectionUIComponent | - | Custom UI component for selection actions |

Example with all callbacks:

const highlighter = new Highlighter(document.body, {
  storage: new LocalStorageAdapter(),
  defaultColor: '#fef08a',

  onHighlightClick: (highlightId, event) => {
    // Handle click (e.g., show delete button, open notes panel)
    if (confirm('Delete this highlight?')) {
      highlighter.removeHighlight(highlightId)
    }
  },

  onHighlight: (highlight) => {
    // Handle creation (e.g., analytics, toast notification)
    console.log('Highlighted:', highlight.text)
  },

  onRemove: (highlightId) => {
    // Handle deletion (e.g., update UI, sync to server)
    console.log('Removed highlight:', highlightId)
  },

  onUpdate: (highlight) => {
    // Handle updates (e.g., color change, note added)
    console.log('Updated highlight:', highlight)
  }
})

Methods

init(): Promise<void>

Initializes the highlighter by:

  1. Loading existing highlights from storage
  2. Rendering highlights on the page
  3. Setting up event listeners for text selection
await highlighter.init()

createHighlight(range: Range, color?: string): Promise<Highlight>

Programmatically create a highlight from a DOM Range.

const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
  const range = selection.getRangeAt(0)
  const highlight = await highlighter.createHighlight(range, '#86efac')
  console.log('Created:', highlight)
}

removeHighlight(highlightId: string): Promise<void>

Remove a highlight by ID.

await highlighter.removeHighlight('highlight_123')

destroy(): void

Clean up event listeners and remove highlights from DOM. Call this when unmounting the highlighter.

highlighter.destroy()

Storage Adapters

Flowmark uses storage adapters to persist highlights. You can use a built-in adapter or create your own.


LocalStorageAdapter (Browser)

Persists highlights in browser localStorage. Data survives page reloads.

import { LocalStorageAdapter } from '@spranclabs/flowmark'

const storage = new LocalStorageAdapter('my-app-highlights')

Parameters:

  • storageKey?: string - LocalStorage key (default: 'text-annotator-highlights')

Use case: Single-page apps, browser extensions, offline-first apps


PostMessageAdapter (Iframes)

Sends highlight operations to parent window via postMessage. Use this when highlighting content in iframes.

import { PostMessageAdapter } from '@spranclabs/flowmark'

const storage = new PostMessageAdapter(window.parent, 'https://parent-domain.com')

Parameters:

  • targetWindow?: Window - Target window to send messages (default: window.parent)
  • targetOrigin?: string - Target origin for security (default: '*')

Message protocol:

// Sent from iframe to parent
{
  type: 'load_highlights' | 'save_highlight' | 'remove_highlight' | ...,
  requestId: string,
  data: any
}

// Parent responds with
{
  type: '<same-as-request>',
  requestId: string,
  data: any,
  error?: string
}

Use case: Highlighting content in iframed web pages, browser extension content scripts

Parent window handler example:

window.addEventListener('message', async (event) => {
  if (event.data.type === 'save_highlight') {
    const { requestId, data } = event.data

    // Save to your backend
    const savedHighlight = await saveToDatabase(data)

    // Respond to iframe
    event.source.postMessage({
      type: 'save_highlight',
      requestId,
      data: savedHighlight
    }, event.origin)
  }
})

MemoryStorageAdapter (Testing)

In-memory storage for testing. Data is lost on page reload.

import { MemoryStorageAdapter } from '@spranclabs/flowmark'

const storage = new MemoryStorageAdapter()

Use case: Unit tests, demos, temporary highlighting


Custom Adapter

Create a custom adapter by implementing the StorageAdapter interface:

import { StorageAdapter, StoredHighlight } from '@spranclabs/flowmark'

class MyCustomAdapter implements StorageAdapter {
  async load(): Promise<StoredHighlight[]> {
    const response = await fetch('/api/highlights')
    return response.json()
  }

  async save(highlight: StoredHighlight): Promise<void> {
    await fetch('/api/highlights', {
      method: 'POST',
      body: JSON.stringify(highlight)
    })
  }

  async update(id: string, data: Partial<StoredHighlight>): Promise<void> {
    await fetch(`/api/highlights/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data)
    })
  }

  async remove(id: string): Promise<void> {
    await fetch(`/api/highlights/${id}`, {
      method: 'DELETE'
    })
  }

  async clear(): Promise<void> {
    await fetch('/api/highlights', {
      method: 'DELETE'
    })
  }
}

const storage = new MyCustomAdapter()

TypeScript

Flowmark is written in TypeScript. All types are exported:

import type {
  Highlight,               // Highlight with Date objects
  StoredHighlight,         // Serializable highlight (with ISO date strings)
  HighlighterConfig,       // Configuration options
  StorageAdapter,          // Storage interface
  SelectionData,           // Browser selection data
  CreateHighlightInput,    // Input for creating highlights
  SelectionTooltipOptions, // Tooltip configuration
  TooltipStyles,           // Tooltip container styles
  ButtonStyles,            // Button styles
} from '@spranclabs/flowmark'

See src/types.ts for full type definitions.


Advanced Usage

For advanced use cases, Flowmark exports low-level utilities:

Text processing:

  • normalizeText(text, options?) - Normalize text for consistent matching
  • computeSimilarity(str1, str2) - Compute similarity score (0-1)
  • getTextContext(range, before?, after?) - Extract text context around a range

DOM manipulation:

  • renderHighlightMarks(range, id, options) - Render highlight marks in DOM
  • unwrapHighlight(container, highlightId) - Remove highlight marks
  • getHighlightElements(container, highlightId) - Get all <mark> elements for a highlight
  • updateHighlightColor(container, highlightId, color) - Change highlight color

Selection handling:

  • captureSelection() - Get current browser selection as SelectionData
  • validateSelection(selection) - Validate selection is suitable for highlighting
  • clearSelection() - Clear browser selection

Highlight restoration:

  • restoreHighlight(container, highlight) - Restore a highlight to the DOM
  • restoreHighlights(container, highlights) - Restore multiple highlights

For detailed documentation on these utilities, see src/ directory.


Styling Highlights

Flowmark renders highlights as <mark> elements with inline background-color styles. You can customize the appearance with CSS:

/* Basic styling */
mark.highlight {
  cursor: pointer;
  transition: background-color 0.2s;
}

mark.highlight:hover {
  opacity: 0.8;
}

/* Custom class for specific highlights */
mark.my-custom-class {
  background-color: #fef08a;
  border-bottom: 2px solid #fbbf24;
}

Pass custom class via config:

const highlighter = new Highlighter(document.body, {
  highlightClassName: 'my-custom-class',
  // ...
})

Customizing Selection Tooltip

The built-in SelectionTooltip can be fully customized:

import { Highlighter, SelectionTooltip } from '@spranclabs/flowmark'

const customTooltip = new SelectionTooltip({
  buttonText: 'Highlight',
  offsetY: 35, // Distance above selection (default: 60)

  // Custom icon (SVG string, or null to hide)
  icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',

  styles: {
    tooltip: {
      background: 'white',
      border: '1px solid #e5e7eb',
      borderRadius: '8px',
      padding: '4px',
      boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
    },
    button: {
      background: 'white',
      color: '#374151',
      border: '1px solid #d1d5db',
      borderRadius: '6px',
      padding: '0 8px',
      fontSize: '12px',
      height: '24px',
      hoverBackground: '#f3f4f6',
      activeBackground: '#e5e7eb',
    },
  },
})

const highlighter = new Highlighter(document.body, {
  storage: storage,
  showSelectionUI: false,    // Disable default UI
  selectionUI: customTooltip, // Use custom tooltip
})

SelectionTooltipOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | buttonText | string | 'Highlight' | Button text | | className | string | 'flowmark-tooltip' | CSS class for tooltip | | offsetY | number | 60 | Vertical offset from selection (px) | | icon | string \| null | Default pencil | SVG icon string, or null to hide | | styles.tooltip | TooltipStyles | - | Tooltip container styles | | styles.button | ButtonStyles | - | Button styles |

TooltipStyles

| Property | Type | Default | |----------|------|---------| | background | string | 'white' | | border | string | '1px solid #ddd' | | borderRadius | string | '6px' | | padding | string | '4px' | | boxShadow | string | '0 2px 8px rgba(0,0,0,0.1)' | | fontFamily | string | System font stack | | fontSize | string | '14px' |

ButtonStyles

| Property | Type | Default | |----------|------|---------| | background | string | '#333' | | color | string | 'white' | | border | string | 'none' | | borderRadius | string | '4px' | | padding | string | '8px 16px' | | fontSize | string | '14px' | | height | string | 'auto' | | hoverBackground | string | '#555' | | activeBackground | string | '#222' |


Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+

License

MIT © Spranc Labs


Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.


Links