
The text editor uses contenteditable elements, the Compression Streams API, and debounced auto-save to create a functional note-taking experience. It’s great for developers building shareable text tools, browser-based documentation, or offline-first applications.
Features:
- URL-based storage: Compresses content with deflate-raw and encodes it in the URL hash.
- Automatic saving: Debounces input events to 500ms before triggering save operations.
- LocalStorage fallback: Maintains a backup copy in localStorage for persistence across sessions.
- Dark mode: Respects the user’s system color scheme preference via CSS media queries.
- Style persistence: Saves inline styles applied to the editor element in the URL.
- Dynamic title updates: Parses markdown-style headers to set the page title.
Use Cases:
- Quick note sharing: Share code snippets or technical notes by copying the URL. The recipient gets the full content loaded instantly.
- Offline documentation: Create browser-bookmarkable reference documents that load from the URL hash.
- Zero-infrastructure prototyping: Build text-based features during the design phase before backend APIs exist.
How To Use It:
1. Create a contenteditable element in your HTML. The plaintext-only attribute limits formatting to plain text.
<!-- The main editor element --> <article contenteditable="plaintext-only" spellcheck></article>
2. The core JavaScript logic handles compression, state management, and event listeners.
// Select the contenteditable element
const article = document.querySelector('article')
// Listen for user input and trigger debounced save
article.addEventListener('input', debounce(500, save))
// Load content when the page loads
addEventListener('DOMContentLoaded', load)
// Load content when the URL hash changes
addEventListener('hashchange', load)
// Load function: retrieves content from URL hash or localStorage
async function load() {
try {
// Check if URL has a hash (shared content)
if (location.hash !== '') {
await set(location.hash)
} else {
// Fall back to localStorage
await set(localStorage.getItem('hash') ?? '')
// If content exists, update URL without creating history entry
if (article.textContent) {
history.replaceState({}, '', await get())
}
// Focus the editor for immediate typing
article.focus()
}
} catch (e) {
// Handle decompression errors by resetting content
article.textContent = ''
article.removeAttribute('style')
}
// Update page title based on content
updateTitle()
}
// Save function: compresses content and updates URL + localStorage
async function save() {
const hash = await get()
// Update URL only if hash has changed
if (location.hash !== hash) {
history.replaceState({}, '', hash)
}
// Attempt to save to localStorage (may fail if quota exceeded)
try {
localStorage.setItem('hash', hash)
} catch (e) {
// Silent fail - URL storage remains functional
}
// Update page title after save
updateTitle()
}
// Set function: decompresses hash and populates editor
async function set(hash) {
// Decompress the base64-encoded hash
const [content, style] = (await decompress(hash.slice(1))).split('x00')
// Set the text content
article.textContent = content
// Apply saved styles if they exist
if (style) {
article.setAttribute('style', style)
}
}
// Get function: compresses current content into hash format
async function get() {
const style = article.getAttribute('style')
// Combine content and style with null separator
const content = article.textContent + (style !== null ? 'x00' + style : '')
// Return compressed hash with # prefix
return '#' + await compress(content)
}
// Update page title from markdown-style header
function updateTitle() {
// Match first line starting with # (markdown header)
const match = article.textContent.match(/^n*#(.+)n/)
// Set title to header text or default
document.title = match?.[1] ?? 'Textarea'
}
// Compress function: converts string to base64-encoded deflate data
async function compress(string) {
// Encode string to byte array
const byteArray = new TextEncoder().encode(string)
// Create deflate-raw compression stream
const stream = new CompressionStream('deflate-raw')
const writer = stream.writable.getWriter()
// Write data and close stream
writer.write(byteArray)
writer.close()
// Read compressed output
const buffer = await new Response(stream.readable).arrayBuffer()
// Convert to URL-safe base64 (replace + and / characters)
return new Uint8Array(buffer)
.toBase64()
.replace(/+/g, "-")
.replace(///g, "_")
}
// Decompress function: converts base64 to original string
async function decompress(b64) {
// Convert URL-safe base64 back to standard format
const byteArray = Uint8Array.fromBase64(
b64.replace(/-/g, "+").replace(/_/g, "/")
)
// Create decompression stream
const stream = new DecompressionStream('deflate-raw')
const writer = stream.writable.getWriter()
// Write compressed data and close
writer.write(byteArray)
writer.close()
// Read decompressed output
const buffer = await new Response(stream.readable).arrayBuffer()
// Decode byte array back to string
return new TextDecoder().decode(buffer)
}
// Debounce function: delays execution until user stops typing
function debounce(ms, fn) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), ms)
}
}
3. You can set a custom page title by starting your document with a markdown-style header. The editor automatically extracts the first line after `#` and uses it as the page title.
# CSSScript.com Content goes here
4. To add custom styling that persists in the URL, open DevTools and add inline styles to the article element:
document.querySelector('article').setAttribute('style',
'font-family: monospace; color: #00ff00; background: #000;'
)
FAQs:
Q: What happens when the compressed URL exceeds browser length limits?
A: Most browsers support URLs up to 2,000 characters. The deflate compression typically achieves 40-60% size reduction. This means you can store roughly 3,000-5,000 characters of plain text before hitting limits.
Q: Why does localStorage fail sometimes?
A: Browsers enforce storage quotas, typically 5-10MB per origin. If other scripts have filled localStorage, the save attempt throws an exception.
Q: Can I encrypt the content before compression?
A: Yes, but you’ll need to add encryption logic before the compression step. The Web Crypto API provides AES-GCM encryption. You’d encrypt the plaintext, then compress the encrypted bytes, then base64 encode. The recipient would need the decryption key to read the content.
The post Build a Serverless Text Editor with URL Storage – textarea.my appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
