@spranclabs/flowmark
v1.0.0
Published
Flowmark - Zero-dependency highlighting library with cross-element selection
Maintainers
Readme
@spranclabs/flowmark
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/flowmarkQuick 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:
- Loading existing highlights from storage
- Rendering highlights on the page
- 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 matchingcomputeSimilarity(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 DOMunwrapHighlight(container, highlightId)- Remove highlight marksgetHighlightElements(container, highlightId)- Get all<mark>elements for a highlightupdateHighlightColor(container, highlightId, color)- Change highlight color
Selection handling:
captureSelection()- Get current browser selection asSelectionDatavalidateSelection(selection)- Validate selection is suitable for highlightingclearSelection()- Clear browser selection
Highlight restoration:
restoreHighlight(container, highlight)- Restore a highlight to the DOMrestoreHighlights(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.
