One line to start. 10 annotation tools. Everything runs client-side β no server uploads, total privacy.
- Sound Familiar?
- What is Shotmark?
- Three Steps to Annotate
- 10 Built-in Tools
- Six Core Highlights
- Architecture
- Quick Start
- Full API Reference
- FAQ
- Use Cases
- More Resources
Before diving into the API, ask yourself:
- π Bug reports: Your tester screenshots a bug and wants to circle the problem β but there's no annotation tool in the browser. Time to switch to Snipaste, alt-tab back and forth...
- π± Privacy leaks: You share a screenshot only to realize a phone number or order ID is visible. Recall message... too late, someone saved it.
- π Pixel measuring: UI review means opening DevTools and clicking elements one by one. A full page later, your wrist is dead.
- π Tutorial creation: Writing docs with step numbers and arrows? Spend more time fighting your screenshot tool than writing the actual guide.
- π Window-switching hell: Screenshot β open annotation app β annotate β save β switch back β upload. That's 4 context switches for one image.
If any of these hit home, Shotmark is for you.
In one sentence: capture, annotate, and export screenshots β all inside the browser.
No desktop app install. No leaving the browser. No complex config. Import Shotmark, wire up a button, and your users can enter screenshot mode β draw rectangles, arrows, text, mosaic, then copy or download in one click.
Core traits:
- π¦ Lightweight β ~113 kB (gzip β 35 kB), only 2 runtime dependencies
- π Private β Everything runs client-side, images never leave the browser
- π¨ Rich β 10 annotation tools covering every scenario
- β‘ Simple β One line to start, 5 minutes to integrate
- π Polished β Light/dark themes, English & Chinese i18n
- π§© Flexible β Tools, buttons, and colors are fully configurable
Whether you're a developer, tester, designer, or PM β the workflow is the same:
Add a trigger button that calls Shotmark.start(). Click it β the page enters capture mode.
Pick a tool (rectangle, arrow, text, mosaicβ¦) and draw freely. Made a mistake? Ctrl+Z to undo, unlimited.
Three ways out:
- π Copy to clipboard β Ctrl+V straight into chat or docs
- πΎ Download file β Save as PNG or JPEG locally
- π base64 callback β Your code gets the image data to upload programmatically
"So many tools β what does each one do?" Here's the cheat sheet:
| Tool | Icon | Purpose | Pro tip |
|---|---|---|---|
| Rectangle | β‘ | Highlight a region | Hold Shift for a perfect square |
| Ellipse | β | Circle irregular areas | Hold Shift for a perfect circle |
| Arrow | β | "Look here!" pointer | Drag endpoints to adjust |
| Brush | β | Freehand drawing | Strokes auto-smooth |
| Text | T | Add text labels | Multi-line, resizable font & color |
| Mosaic | β¦ | Redact private info | Adjustable granularity & softness |
| Highlight | β | Fluorescent marker | Semi-transparent, doesn't obscure |
| Number | β | Step numbering | Auto-increment, configurable start |
| Line | -- | Underline / divider | Shift for horizontal or vertical |
| Measure | β | Pixel distance | Shows px value β UI review superpower |
Universal actions:
- π Undo / Redo β
Ctrl+Z/Ctrl+Shift+Z(Mac:Cmd) - π― Select & adjust β Click a shape, drag anchors to resize/move
- π¨ Restyle live β Change color, stroke width, font size on selected shapes
- π±οΈ Scroll wheel β Quickly adjust line width or font size
Master these and your annotation speed doubles:
| Shortcut | Action |
|---|---|
1 β 0 |
Switch to Nth tool (toolbar order) |
Ctrl/Cmd + Z |
Undo |
Ctrl/Cmd + Shift + Z |
Redo |
Ctrl/Cmd + C |
Copy screenshot to clipboard |
Delete / Backspace |
Delete selected shape |
Enter |
Confirm and export |
Escape |
Close annotation |
Shift + draw |
Constrain axis (square / circle / H / V line) |
| Scroll wheel | Adjust line width or font size |
π‘ Number keys work on both the main keyboard and numpad, and are IME-safe β they work even with Chinese/Japanese input active.
Shotmark.start({ region: document.getElementById('target') });No new, no config file, no lifecycle hooks. One function call does it all. On close, Shotmark auto-cleans its DOM β zero leftovers.
From capture to export, everything stays in browser memory:
π₯οΈ Page DOM β πΈ Canvas pixel grab β βοΈ SVG annotation β π¨ Canvas composite β πΌοΈ Image output
Zero network requests. No third-party services. The screenshot only exists in the onShot callback parameter β if you don't upload it, it stays in memory until garbage collected.
- Bundle β 113 kB (gzip β 35 kB)
- Runtime deps: only
@emotion/css+clsx - No dependency on any UI library (antd / element / material β none)
- React β₯ 17.0.0 (supports 17 and 18)
Built-in light / dark theme switching. Plus zh-CN / en-US i18n with custom text overrides for full localization control.
// Bug reporting: just 4 tools
Shotmark.start({
tools: ['rectangle', 'arrow', 'text', 'mosaic'],
actions: ['cancel', 'copy'],
});
// UI review: measurement first
Shotmark.start({
tools: ['measure', 'rectangle', 'arrow', 'text'],
defaultTool: 'measure',
actions: ['cancel', 'download'],
});The toolbar shows only what you configure β clean and focused.
Most web screenshot solutions break on background-image β mosaic renders blank, or backgrounds vanish on export.
Shotmark handles full background image rendering during rasterization: cover, contain, repeat, multi-backgrounds, gradient + image combos β all preserved in the exported image.
π‘ Curious about the internals? This section covers how Shotmark works under the hood. If you just want to integrate, skip to Quick Start.
Shotmark uses a five-layer separation architecture:
| Layer | Module | Responsibility |
|---|---|---|
| L1 | Controller | Config validation, lifecycle management |
| L2 | Selection Layer | Region picking, overlay mask, drag-to-adjust |
| L3 | Draw Board | SVG drawing, shape management, undo/redo |
| L4 | Toolbar | Tool switching, color/width/font config |
| L5 | Rasterizer | DOM β SVG β Canvas β Image |
Interesting technical details:
-
Plugin-based shapes β Each tool is an independent
GraphPluginimplementing adown β move β upthree-phase protocol. Adding a new shape means registering one file β no other code touched. -
True pixel mosaic β Not a CSS
filter: blur()hack. Raw pixels are grabbed from Canvas, block-averaged, and painted back β identical to Photoshop's pixelate. -
Double-rAF export β On submit, a double
requestAnimationFramestrategy lets the UI animation render one frame before the heavy rasterization task runs β users feel zero jank. -
Config memory β Colors, stroke widths, and font sizes persist across sessions β next time you open Shotmark, your preferences are restored.
5 minutes to integrate.
pnpm add shotmark
# or: npm i shotmark / yarn add shotmark
reactandreact-domare peer dependencies (β₯17). Install them in your host project.
import Shotmark from 'shotmark';
function ScreenshotButton() {
const handleClick = () => {
const el = document.getElementById('target-area');
if (!el) return;
Shotmark.start({
region: el, // what to capture
autoAnnotate: true, // enter annotation mode immediately
onShot: (result) => {
// result.image is the annotated base64 image
console.log('Done:', result.width, 'Γ', result.height);
},
});
};
return <button onClick={handleClick}>πΈ Screenshot</button>;
}Seriously. Shotmark.start() opens it, the user annotates, and it auto-closes with DOM cleanup. Need programmatic close? Call Shotmark.close().
Free selection mode (no region β user drags their own area):
Shotmark.start({
onShot: (result) => uploadToServer(result.image),
});Custom tools + dark theme:
Shotmark.start({
region: el,
tools: ['rectangle', 'arrow', 'text', 'mosaic'],
actions: ['cancel', 'copy', 'download'],
theme: 'dark',
defaultColor: '#ff4d4f',
locale: 'en-US',
});Copy to clipboard with callbacks:
Shotmark.start({
region: el,
onCopy: (blob) => {
showToast('Screenshot copied!');
},
onDownload: (fileName) => {
showToast(`Saved as ${fileName}`);
},
});Launch the screenshot annotation overlay. All options are optional β calling with no arguments enters full-page free selection mode.
| Field | Type | Default | Description |
|---|---|---|---|
region |
HTMLElement | Rect |
β | Capture area; pass an element or coordinate rect. Omit for free selection |
regionPadding |
number |
0 |
Extra pixels to expand the region |
autoAnnotate |
boolean |
true |
Enter annotation mode immediately after capture |
trigger |
HTMLElement |
β | Element to temporarily hide during capture |
| Field | Type | Default | Description |
|---|---|---|---|
tools |
GraphType[] |
all 10 | Which tools to show |
actions |
ActionType[] |
all 4 | Action buttons subset |
defaultTool |
GraphType |
β | Pre-selected tool |
defaultColor |
string |
'#FF3B30' |
Default annotation color |
defaultLineWidth |
number |
β | Default line width |
theme |
'light' | 'dark' |
'light' |
Theme mode |
locale |
'zh-CN' | 'en-US' |
'zh-CN' |
Language |
localeText |
LocaleTextOverrides |
β | Override built-in text |
zIndex |
number |
9998 |
Overlay z-index |
| Field | Type | Default | Description |
|---|---|---|---|
mosaicSize |
number |
2 |
Block size (larger = stronger) |
mosaicSoftness |
number |
36 |
Softness (0~100) |
numberStart |
number |
1 |
Starting number value |
| Field | Type | Default | Description |
|---|---|---|---|
fileName |
string |
shotmark_timestamp |
Download filename (no extension) |
format |
'png' | 'jpeg' |
'png' |
Export format |
| Field | Type | Description |
|---|---|---|
onShot |
(result: ShotmarkResult) => void |
Capture complete |
onShotStart |
() => void |
Rasterization started (show loading) |
onCancel |
() => void |
Cancelled (β or Esc) |
onCopy |
(blob: Blob) => void |
Copy succeeded |
onCopyError |
(error: unknown) => void |
Copy failed |
onDownload |
(fileName: string) => void |
Download succeeded |
onDownloadError |
(error: unknown) => void |
Download failed |
onAnnotationChange |
(graphs: GraphPath[]) => void |
Annotation data changed |
Programmatically close the overlay and clean up all DOM. Usually unnecessary β it auto-closes when the user finishes.
| Field | Type | Description |
|---|---|---|
image |
string |
base64 dataURL |
width |
number |
Selection width (CSS pixels) |
height |
number |
Selection height (CSS pixels) |
pixWidth |
number |
Actual output width (2x retina) |
pixHeight |
number |
Actual output height (2x retina) |
Type note:
Rect = { left, top, width, height }(viewport coordinate rectangle)
Q: Browser support? Chrome 90+, Edge 90+, Firefox 90+, Safari 15+. All modern browsers.
Q: Will it conflict with my UI library?
No. Shotmark uses @emotion/css for style isolation β class names are unique hashes, no global pollution. Works alongside any framework.
Q: Does screenshot data get uploaded? Never. Zero network requests at runtime. The image only exists in the callback parameter β upload is entirely your decision.
Q: React version requirement? React β₯ 17.0.0. Both 17 and 18 are supported.
Q: Export quality? 2x pixel density by default. A 300Γ200 selection outputs a 600Γ400 image β crisp on Retina displays.
Q: Can I use only 3 tools?
Absolutely: tools: ['rectangle', 'arrow', 'text'] β the toolbar shows just those three.
Q: What happens if I call start() twice? Safe no-op. If the overlay is already open, the second call is ignored.
| Scenario | Who | How |
|---|---|---|
| π Bug reporting | Testers / Users | Screenshot β circle the bug β add text β paste into ticket |
| πΌοΈ Privacy redaction | Everyone | Phone/ID numbers β mosaic β safe to share |
| π UI review | Design / Frontend | Measure tool for spacing β annotate diffs β export comparison |
| π Tutorial creation | Everyone | Step screenshots β numbers + arrows β instant guide |
| π Data masking | Analytics / Ops | Report screenshot β mosaic sensitive numbers β safe distribution |
| π Quality inspection | QA / Operations | Compare images β annotate issues β upload records |
- π User Guide β Full features, shortcuts, scenarios
- ποΈ Architecture β Directory structure, internals, design decisions
- π Changelog
- π€ Contributing
pnpm storybook # dev server
pnpm test # unit tests
pnpm build # production build
pnpm format # format code




