stdlib-browser
@valentinkolb/stdlib/browser
Browser utility modules. All exports require a DOM environment (document, navigator, window).
Import
import {
files,
images,
cookies,
clipboard,
notifications,
kvStore,
theme,
} from "@valentinkolb/stdlib/browser";
The images namespace is also exported as img for brevity:
import { img } from "@valentinkolb/stdlib/browser";
Individual functions and types can also be imported directly (they are re-exported at the top level):
import {
downloadFileFromContent,
createZip,
showFileDialog,
OPFS,
copyToClipboard,
readJsonCookie,
} from "@valentinkolb/stdlib/browser";
files
File downloads, ZIP archives, file/folder picker dialogs, path building, MIME type utilities, and OPFS access.
ZIP and compression operations use native CompressionStream/DecompressionStream (no external dependencies).
files.downloadFileFromContent
Triggers a browser file download from in-memory content.
files.downloadFileFromContent(
content: string | Uint8Array | ArrayBuffer | Blob,
filename: string,
mimeType?: string // default: "text/plain"
): void
files.downloadFileFromContent("hello world", "greeting.txt");
files.downloadFileFromContent(pngBytes, "photo.png", "image/png");
files.createZip
Creates an in-memory ZIP archive from an array of file entries. Returns Uint8Array.
files.createZip(
files: ZipFile[],
compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 // default: 6
): Promise<Uint8Array>
ZipFile shape:
type ZipFile = {
filename: string;
source: string | Blob | ArrayBuffer | Uint8Array;
mimeType?: string; // informational only, not used by ZIP format
};
const zipData = await files.createZip([
{ filename: "hello.txt", source: "Hello World" },
{ filename: "data.json", source: JSON.stringify({ foo: "bar" }) },
]);
files.downloadAsZip
Creates a ZIP and triggers a browser download. Combines createZip and downloadFileFromContent.
files.downloadAsZip(
files: ZipFile[],
zipFilename?: string, // default: "download.zip"
compressionLevel?: 0-9 // default: 6
): Promise<void>
await files.downloadAsZip(
[
{ filename: "image.webp", source: imageBlob },
{ filename: "data.json", source: jsonString },
],
"my-files.zip",
);
files.createDownloadLink
Creates an <a> element with a download link without appending it to the DOM. The object URL is auto-revoked 100ms after click or after 60 seconds if never clicked.
files.createDownloadLink(
content: BlobPart,
filename: string,
mimeType?: string, // default: "text/plain"
linkText?: string, // default: "Download"
className?: string // default: "hover-text"
): HTMLAnchorElement
const link = files.createDownloadLink(csvString, "report.csv", "text/csv");
document.getElementById("downloads")!.appendChild(link);
files.showFileDialog
Opens the native file picker. Returns a single File or an array of File objects.
// Single file
files.showFileDialog(conf: { accept?: string; multiple?: false }): Promise<File>
// Multiple files
files.showFileDialog(conf: { accept?: string; multiple: true }): Promise<File[]>
Throws if the user cancels or selects nothing.
const file = await files.showFileDialog({ accept: ".pdf" });
const images = await files.showFileDialog({ accept: ".jpg,.png", multiple: true });
files.showFolderDialog
Opens a native folder picker. Returns all files in the selected directory recursively. Optionally filters by MIME type.
files.showFolderDialog(accept?: string): Promise<File[]>
const allFiles = await files.showFolderDialog();
const imageFiles = await files.showFolderDialog("image/*");
files.path
Tagged template literal that builds sanitized file paths. Interpolated values are sanitized per segment (non-word characters replaced with hyphens, lowercased, truncated to 20 chars, hash suffix appended to reduce collisions). Static parts pass through unchanged.
files.path(strings: TemplateStringsArray, ...values: unknown[]): string
const p = files.path`uploads/${userName}/${fileName}.txt`;
// e.g. "uploads/john-doe-a3f2b1/my-file-c8d4e9.txt"
files.mimeTypesToAccept
Converts comma-separated MIME types to an HTML <input accept> value. Resolves specific MIME types to file extensions via the mime package.
files.mimeTypesToAccept(mimeTypes: string): string
files.mimeTypesToAccept("image/*,application/pdf");
// "image/*,.pdf,application/pdf"
files.mimeTypesToAccept("image/jpeg,image/png");
// ".jpg,.jpeg,.png,image/jpeg,image/png"
files.getMimeType
Looks up the MIME type for a filename or file extension. Returns undefined if unknown.
files.getMimeType(filenameOrExt: string): string | undefined
files.getMimeType("photo.jpg"); // "image/jpeg"
files.getMimeType(".png"); // "image/png"
files.getMimeType("report.pdf"); // "application/pdf"
files.getMimeType("unknown.xyz"); // undefined
files.getExtension
Looks up the primary file extension for a MIME type. Returns undefined if unknown.
files.getExtension(mimeType: string): string | undefined
files.getExtension("image/jpeg"); // ".jpg"
files.getExtension("application/pdf"); // ".pdf"
files.getExtension("text/html"); // ".html"
files.getExtension("made/up"); // undefined
files.checkMimeType
Checks whether a file or MIME type string matches an accept filter. Supports extension (.pdf), wildcard (image/*), and exact (application/pdf) formats.
files.checkMimeType(fileOrType: File | string, accept: string): boolean
Returns true if accept is empty.
files.checkMimeType(file, "image/*"); // true for any image
files.checkMimeType(file, ".pdf,image/*"); // true for PDFs or images
files.checkMimeType("application/pdf", ".pdf"); // true
files.OPFS
Wrapper around the Origin Private File System API. Requires a secure context and a browser supporting navigator.storage.getDirectory().
files.OPFS.getDirHandle(segments: string[], create?: boolean): Promise<FileSystemDirectoryHandle>
files.OPFS.write(path: string, data: Uint8Array): Promise<void>
files.OPFS.read(path: string): Promise<Uint8Array | undefined>
files.OPFS.delete(path: string): Promise<void>
files.OPFS.ls(dirPath?: string): Promise<string[]>
writecreates parent directories automatically. Overwrites existing files.readreturnsundefined(does not throw) if the file or directory does not exist.deleteremoves files or directories recursively.lsreturns entry names sorted, with/suffix on directories. Returns[]if directory does not exist.
await files.OPFS.write("data/images/photo.bin", uint8Array);
const data = await files.OPFS.read("data/images/photo.bin");
await files.OPFS.ls("data/images"); // ["photo.bin", "thumbnails/"]
await files.OPFS.delete("data/images/photo.bin");
images (also exported as img)
Functional, chainable image processing API. All operations produce new ImgData containers -- the original is never mutated. Transforms return functions of type (ImgData | Promise<ImgData>) => Promise<T>, so they compose with Promise.then.
Types
type ImgData = Readonly<{
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
width: number;
height: number;
}>;
type Source = File | Blob | HTMLImageElement | HTMLCanvasElement | string;
type Fit = "cover" | "contain" | "fill";
type Format = "jpeg" | "webp" | "png";
images.create
Loads an image from various sources and produces an ImgData.
images.create(source: Source): Promise<ImgData>
Accepted sources: File, Blob, HTMLImageElement, HTMLCanvasElement, or a URL string. Sets crossOrigin: "anonymous" for URL strings. Revokes object URLs automatically for Blob/File inputs.
const data = await images.create(file);
const data2 = await images.create("https://example.com/photo.jpg");
images.resize
Resizes an image. If only one dimension is given, the other is calculated from the aspect ratio.
images.resize(
width?: number,
height?: number,
fit?: Fit, // default: "fill"
letterboxColor?: string // default: "#000" (only used for "contain")
): Transform
Fit modes:
"fill"-- stretches to exact dimensions, ignoring aspect ratio."cover"-- scales and center-crops to fill the area (may clip)."contain"-- scales to fit within dimensions, fills empty space withletterboxColor.
const resized = await images.create(file)
.then(images.resize(800, 600, "cover"));
images.crop
Crops to a rectangle at pixel coordinates from the top-left corner.
images.crop(x: number, y: number, w: number, h: number): Transform
const cropped = await images.create(file)
.then(images.crop(100, 50, 400, 300));
images.filter
Applies a CSS filter string (same syntax as the CSS filter property).
images.filter(filterStr: string): Transform
const filtered = await images.create(file)
.then(images.filter("grayscale(1) brightness(1.1)"));
// Using a preset:
const vintage = await images.create(file)
.then(images.filter(images.filters.vintage));
images.rotate
Rotates by 90, 180, or 270 degrees. Swaps dimensions for 90/270.
images.rotate(deg: 90 | 180 | 270): Transform
images.flip
Flips along one or both axes.
images.flip(horizontal?: boolean, vertical?: boolean): Transform
// defaults: horizontal = true, vertical = false
const mirrored = await images.create(file).then(images.flip());
const flippedVertical = await images.create(file).then(images.flip(false, true));
images.apply
Applies a custom drawing function to a copy of the canvas. Use for watermarks, overlays, or custom pixel manipulation.
images.apply(fn: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void): Transform
const watermarked = await images.create(file)
.then(images.apply((ctx, canvas) => {
ctx.font = "24px sans-serif";
ctx.fillStyle = "rgba(255,255,255,0.5)";
ctx.fillText("Watermark", 10, canvas.height - 30);
}));
Output converters
images.toBlob(format?: Format, quality?: number): Transform<Blob>
// defaults: format = "webp", quality = 0.9
images.toBase64(format?: Format, quality?: number): Transform<string>
// defaults: format = "webp", quality = 0.9
// Returns a data URL: "data:image/webp;base64,..."
images.toFile(name: string, format?: Format, quality?: number): Transform<File>
// defaults: format = "webp", quality = 0.9
images.toCanvas: (data: ImgData | Promise<ImgData>) => Promise<HTMLCanvasElement>
// Note: toCanvas is a direct function, not a factory
const blob = await images.create(file)
.then(images.resize(800, 600, "cover"))
.then(images.filter(images.filters.vintage))
.then(images.toBlob("webp"));
const dataUrl = await images.create(file)
.then(images.resize(200, 200, "cover"))
.then(images.toBase64("jpeg", 0.8));
const outputFile = await images.create(file)
.then(images.resize(1024))
.then(images.toFile("processed.webp"));
images.batch
Processes multiple images through the same pipeline sequentially. Reports progress via callback.
images.batch<T>(
sources: Source[],
transform: (data: Promise<ImgData>) => Promise<T>,
opts?: { onProgress?: (progress: { current: number; total: number; percent: number }) => void }
): Promise<T[]>
Images are processed one at a time to avoid excessive memory pressure.
const blobs = await images.batch(
fileList,
(data) => data
.then(images.resize(800, 600, "cover"))
.then(images.filter(images.filters.vintage))
.then(images.toBlob("webp")),
{ onProgress: ({ percent }) => console.log(`${Math.round(percent * 100)}%`) },
);
images.filters
Predefined CSS filter presets and generator functions.
// Static presets (strings)
images.filters.vintage // "sepia(0.3) contrast(1.1) brightness(1.1) saturate(1.3)"
images.filters.grayscale // "grayscale(1) contrast(1.1)"
images.filters.dramatic // "contrast(1.4) brightness(0.9) saturate(1.2)"
images.filters.soft // "brightness(1.05) saturate(0.9) blur(0.5px)"
// Generator functions
images.filters.blur(px: number): string // e.g. "blur(5px)"
images.filters.brightness(v: number): string // e.g. "brightness(1.2)"
images.filters.contrast(v: number): string // e.g. "contrast(1.4)"
images.filters.saturate(v: number): string // e.g. "saturate(1.5)"
images.filters.hue(deg: number): string // e.g. "hue-rotate(90deg)"
images.presets
Ready-to-use processing presets combining multiple transforms.
images.presets.avatar(
src: Source,
size?: number, // default: 512
quality?: number, // default: 0.8
format?: Format // default: "webp"
): Promise<string> // returns base64 data URL
// Center-crops to square, applies subtle contrast/saturation boost
images.presets.thumbnail(
src: Source,
maxSize?: number, // default: 300
letterboxColor?: string, // default: "#000"
format?: Format // default: "webp"
): Promise<string> // returns base64 data URL
// Fits within square, fills empty space with letterboxColor
const avatarDataUrl = await images.presets.avatar(file, 256);
const thumbDataUrl = await images.presets.thumbnail(file, 150, "#fff");
cookies
Read, write, and delete browser cookies. Supports both raw string and JSON-encoded values.
All writes default to: path=/, SameSite=Lax, 1-year max-age, auto-detect HTTPS for Secure flag.
cookies.readJsonCookie
Reads and parses a JSON-encoded cookie. Returns defaultValue on missing/corrupt cookies. When both defaultValue and parsed value are plain objects (not arrays), the result is shallow-merged so newly added fields in defaultValue are preserved.
cookies.readJsonCookie<T>(name: string, defaultValue: T): T
const prefs = cookies.readJsonCookie("prefs", { theme: "light", lang: "en" });
// If cookie has { theme: "dark" }, returns { theme: "dark", lang: "en" }
cookies.writeJsonCookie
Writes a value as a JSON-encoded, URI-encoded cookie.
cookies.writeJsonCookie<T>(
name: string,
data: T,
maxAge?: number, // default: 31536000 (1 year)
secure?: boolean // default: auto-detect HTTPS
): void
cookies.writeJsonCookie("prefs", { theme: "dark", lang: "en" });
cookies.writeJsonCookie("session", { token: "abc" }, 3600); // 1 hour
cookies.readCookie
Reads a raw string cookie. Returns null if not found.
cookies.readCookie(name: string): string | null
const token = cookies.readCookie("auth_token");
cookies.writeCookie
Writes a raw string cookie (URI-encoded).
cookies.writeCookie(
name: string,
value: string,
maxAge?: number, // default: 31536000 (1 year)
secure?: boolean // default: auto-detect HTTPS
): void
cookies.writeCookie("auth_token", "abc123");
cookies.writeCookie("temp", "value", 300); // 5 minutes
cookies.deleteCookie
Deletes a cookie by setting max-age to 0.
cookies.deleteCookie(name: string): void
cookies.deleteCookie("auth_token");
clipboard
Clipboard utilities. Requires a secure context (HTTPS or localhost).
For SolidJS apps, use clipboard.create() from @valentinkolb/stdlib/solid which adds a reactive wasCopied signal that auto-resets after a timeout.
clipboard.copy
Copies text to the system clipboard using the Clipboard API. The browser may prompt for permission on first use.
clipboard.copy(text: string): Promise<void>
await clipboard.copy("Hello, world!");
await clipboard.copy(JSON.stringify(data, null, 2));
notifications
Native browser notification permission management and display. SSR-safe: returns null/false in non-browser environments.
Types
type NotificationPermissionState = "granted" | "denied" | "default";
type ShowNotificationOptions = {
title: string;
body: string;
icon?: string; // Small icon URL, typically 128x128 px
image?: string; // Large preview image (Chromium only)
tag?: string; // Deduplication tag -- same tag replaces previous
autoCloseMs?: number; // Auto-close after N ms (disabled when omitted)
onClick?: () => void; // Called on user click
onClose?: () => void; // Called on close (manual, auto, or OS)
};
type NotificationHandle = {
close: () => void; // Safe to call multiple times
};
notifications.isSupported
Check whether the Notification API is available. Returns false during SSR or in unsupported browsers.
notifications.isSupported(): boolean
notifications.getPermission
Get the current permission state without prompting. Falls back to "denied" when the API is unavailable.
notifications.getPermission(): NotificationPermissionState
notifications.requestPermission
Request permission from the user. Triggers the browser's native permission dialog. Returns false in non-browser environments or if already denied.
notifications.requestPermission(): Promise<boolean>
const allowed = await notifications.requestPermission();
if (allowed) {
notifications.show({ title: "Enabled", body: "You will now receive notifications." });
}
notifications.show
Display a native browser notification. Returns a handle with close(), or null if unsupported/not permitted.
notifications.show(options: ShowNotificationOptions): NotificationHandle | null
const handle = notifications.show({
title: "New message",
body: "Alice: Hey, are you there?",
icon: "/avatar.png",
tag: "chat-42",
autoCloseMs: 5000,
onClick: () => window.location.assign("/chat/42"),
onClose: () => console.log("notification dismissed"),
});
// Close programmatically if needed
handle?.close();
kvStore
Persistent key-value store backed by the Origin Private File System (OPFS). Think of it as localStorage, but async and without the 5 MB size limit. Requires a secure context (HTTPS or localhost).
Key characteristics:
- No size limit -- bounded only by available disk space.
- Binary support -- store raw
Uint8Arraydata without serialization overhead viasetBytes/getBytes. - O(1) lookups --
has,keys,meta, andsizeresolve from an in-memory index and never touch disk after initial load. - Concurrency-safe -- Web Locks serialize writes; reads are lock-free.
- Cross-tab sync -- a
BroadcastChannelkeeps every tab's in-memory cache in sync. - Namespace-friendly -- use key prefixes (e.g.
"user:","cache:") and query withkeys(prefix).
Small values (4 KB or less) are stored inline in the index file. Larger values spill to individual blob files.
Types
type KVEntryMeta = {
key: string;
size: number;
timestamp: number;
type: "json" | "bin";
};
type KVEvent = {
type: "set" | "delete" | "clear";
key: string; // empty string for "clear" events
};
kvStore.set
Store a JSON-serializable value. Overwrites any previous entry under the same key.
kvStore.set(key: string, value: unknown): Promise<void>
await kvStore.set("user:1", { name: "Alice", age: 30 });
await kvStore.set("prefs", { theme: "dark" });
await kvStore.set("counter", 42);
kvStore.get
Retrieve a JSON value. Returns undefined for missing keys or keys stored with setBytes.
kvStore.get<T = unknown>(key: string): Promise<T | undefined>
const user = await kvStore.get<{ name: string; age: number }>("user:1");
if (user) console.log(user.name);
kvStore.setBytes
Store raw binary data. Data 4 KB or less is inlined (base64) in the index; larger data gets its own blob file.
kvStore.setBytes(key: string, data: Uint8Array): Promise<void>
const response = await fetch("/large-dataset.bin");
await kvStore.setBytes("data:raw", new Uint8Array(await response.arrayBuffer()));
kvStore.getBytes
Retrieve raw binary data. Returns undefined for missing keys or keys stored with set.
kvStore.getBytes(key: string): Promise<Uint8Array | undefined>
const photo = await kvStore.getBytes("files:photo.raw");
if (photo) processImage(photo);
kvStore.has
Check whether a key exists. O(1) from in-memory index.
kvStore.has(key: string): Promise<boolean>
kvStore.keys
List all keys, optionally filtered by prefix. O(1) from in-memory index. Results are sorted alphabetically.
kvStore.keys(prefix?: string): Promise<string[]>
const allKeys = await kvStore.keys();
const userKeys = await kvStore.keys("user:"); // ["user:1", "user:2"]
const cacheKeys = await kvStore.keys("cache:"); // ["cache:api:token"]
kvStore.meta
Read entry metadata without loading the value. O(1) from in-memory index.
kvStore.meta(key: string): Promise<KVEntryMeta | undefined>
const info = await kvStore.meta("files:photo.raw");
if (info) console.log(`${info.key}: ${info.size} bytes, type=${info.type}`);
kvStore.size
Return the total number of entries in the store. O(1) from in-memory index.
kvStore.size(): Promise<number>
kvStore.delete
Delete a key and its data. No-op if the key does not exist.
kvStore.delete(key: string): Promise<void>
kvStore.clear
Delete all entries and remove the store directory. The store is immediately usable again after clearing.
kvStore.clear(): Promise<void>
kvStore.watch
Watch for store mutations, optionally filtered by key prefix. Fires for changes in the current tab and from other tabs (via BroadcastChannel). Returns an unsubscribe function. The event does not contain the value -- call get/getBytes inside the callback if needed.
kvStore.watch(
callback: (event: KVEvent) => void,
prefix?: string
): () => void // returns unsubscribe function
const unwatch = kvStore.watch((e) => {
console.log(`${e.type}: ${e.key}`);
if (e.type === "set") refreshUI();
}, "user:");
// Later: stop watching
unwatch();
theme
Light/dark mode toggle with cookie persistence. SSR-safe.
theme.getCurrent
Returns current theme by checking document.documentElement for dark class.
Returns "light" in SSR.
theme.getCurrent(): "light" | "dark"
theme.set
Applies theme class to document root and persists to cookie.
theme.set(mode: "light" | "dark"): "light" | "dark"
theme.toggle
Switches to opposite theme and persists.
theme.toggle(): "light" | "dark"
Key patterns
- Browser-only: All modules require a DOM environment. Do not import in Node.js/server-side code.
- Secure context:
clipboard,notifications,kvStore, andfiles.OPFSrequire HTTPS or localhost. - SSR safety:
notificationsfunctions returnnull/falsegracefully in non-browser environments. Other modules will throw if DOM APIs are missing. - Chainable images: The
imagesAPI uses a functional pipeline pattern -- each transform returns a function compatible withPromise.then. Chain transforms freely:const result = await images.create(file) .then(images.resize(800, 600, "cover")) .then(images.crop(0, 0, 400, 300)) .then(images.filter(images.filters.vintage)) .then(images.rotate(90)) .then(images.flip()) .then(images.toBlob("webp", 0.85)); - kvStore vs localStorage: Use
kvStorewhen you need more than 5 MB, binary storage, cross-tab reactivity, or structured namespacing. All operations are async (return Promises). - Key prefixes for namespacing: kvStore supports namespace-style keys like
"user:","cache:","files:". Usekeys(prefix)andwatch(cb, prefix)to scope operations.