From Notepad Tables to Rich Text Editor: Building a Lightweight Electron Editor with Table Support
Build a compact Electron editor with table editing, Markdown export, and a plugin API—practical steps, code, and 2026 best practices.
Hook — Your editor should ship features, not friction
As developers and IT professionals, we spend hours inside editors that promise simplicity or power — but rarely both. You want a small, fast, cross-platform app that supports real-world tasks: writing notes, pasting data, and turning quick tables into sharable Markdown. Microsoft’s 2025 rollout of tables in Notepad proved one thing: simple apps can gain meaningful, practical features without ballooning into full IDEs. This tutorial shows how to build a lightweight Electron-based editor with table editing, Markdown export, and a minimal plugin architecture so you can iterate fast and ship features your team will actually use.
What you'll build (in brief)
By the end of this walkthrough you’ll have a compact, cross-platform rich text editor with:
- Contenteditable rich text area with inline formatting controls (bold, italic, lists)
- Table insertion and editing (add/remove rows & columns, paste CSV)
- Markdown export that converts tables into GitHub-style pipe tables
- A tiny plugin hook API so extensions can manipulate the editor or add commands
- Secure Electron setup (preload, contextIsolation) and packaging notes
Why Electron in 2026 (and when to consider alternatives)
In late 2025 and early 2026 we saw sustained interest in lighter native-web runtimes (Tauri, Neutralino). However, Electron remains a dominant choice for dev tools due to ecosystem maturity, debugging ergonomics, and broad native API support. Use Electron when you need:
- Stable, battle-tested APIs and a large plugin/package ecosystem
- Fast development cycles with Node-based tooling
- Out-of-the-box cross-platform desktop integrations
If you prioritize binary size and lower memory usage, evaluate Tauri; if you prioritize speed-to-market and interoperability with Node modules, Electron is still compelling in 2026.
Project structure — minimal and focused
Keep the app structure tiny to reduce cognitive load and onboarding friction (a core pain point for teams). Here’s the recommended layout:
my-editor/
├─ package.json
├─ src/
│ ├─ main.js // Electron main process
│ ├─ preload.js // secure API bridge
│ └─ renderer/
│ ├─ index.html
│ ├─ renderer.js
│ └─ styles.css
└─ plugins/ // optional local plugins
Step 1 — Minimal Electron app (secure defaults)
Start with these essentials in main.js. We enable contextIsolation and use a preload script to expose a controlled API to the renderer.
// src/main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 900,
height: 680,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
}
})
win.loadFile(path.join(__dirname, 'renderer', 'index.html'))
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() })
ipcMain.handle('show-save-dialog', async (_, defaultPath) => {
const { filePath } = await dialog.showSaveDialog({ defaultPath })
return filePath || null
})
ipcMain.handle('write-file', async (_, { filePath, content }) => {
const fs = require('fs').promises
await fs.writeFile(filePath, content, 'utf8')
return true
})
Step 2 — Expose a tiny API via preload
Use the preload script to expose safe, intentional APIs to the renderer. This avoids enabling nodeIntegration and improves security.
// src/preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('appApi', {
saveFile: async (defaultPath, content) => {
const filePath = await ipcRenderer.invoke('show-save-dialog', defaultPath)
if (!filePath) return null
await ipcRenderer.invoke('write-file', { filePath, content })
return filePath
}
})
Step 3 — Renderer: minimal rich text editor
The renderer uses a contenteditable div for compactness and predictability. We add an inline toolbar for common actions and a set of table controls.
Core editor logic
// src/renderer/renderer.js
const editor = document.getElementById('editor')
document.getElementById('boldBtn').addEventListener('click', () => document.execCommand('bold'))
document.getElementById('italicBtn').addEventListener('click', () => document.execCommand('italic'))
document.getElementById('ulBtn').addEventListener('click', () => document.execCommand('insertUnorderedList'))
function insertTable(rows = 2, cols = 2) {
const table = document.createElement('table')
table.className = 'cw-table'
for (let r = 0; r < rows; r++) {
const tr = table.insertRow()
for (let c = 0; c < cols; c++) {
const td = tr.insertCell()
td.contentEditable = 'true'
td.textContent = r === 0 ? 'Header' : ''
}
}
editor.appendChild(table)
}
document.getElementById('insertTableBtn').addEventListener('click', () => insertTable(2, 3))
// Simple plugin hooks container
const plugins = []
function registerPlugin(p) { plugins.push(p); p.onInit?.({ editor, registerCommand }) }
function registerCommand(name, fn) { window[name] = fn }
// Export markdown button
document.getElementById('exportMdBtn').addEventListener('click', async () => {
const html = editor.innerHTML
const md = htmlToMarkdown(html)
const saved = await window.appApi.saveFile('document.md', md)
if (saved) alert('Saved ' + saved)
})
// Basic HTML->Markdown conversion focused on tables and basic elements
function htmlToMarkdown(html) {
const container = document.createElement('div')
container.innerHTML = html
// Convert tables
const tables = container.querySelectorAll('table')
tables.forEach(table => {
const md = tableToMarkdown(table)
const pre = document.createElement('pre')
pre.textContent = md
table.replaceWith(pre)
})
// Replace simple tags
let md = container.innerHTML
md = md.replace(/(.*?)<\/strong>/g, '**$1**')
md = md.replace(/(.*?)<\/b>/g, '**$1**')
md = md.replace(/(.*?)<\/em>/g, '*$1*')
md = md.replace(/(.*?)<\/i>/g, '*$1*')
md = md.replace(/\s*- (.*?)<\/li>\s*<\/ul>/g, '- $1')
// strip remaining tags conservatively
md = md.replace(/<[^>]+>/g, '')
// collapse multiple newlines
md = md.replace(/\n{3,}/g, '\n\n')
return md.trim()
}
function tableToMarkdown(table) {
const rows = Array.from(table.rows).map(row => Array.from(row.cells).map(c => c.textContent.trim()))
if (rows.length === 0) return ''
const header = rows[0]
const body = rows.slice(1)
const headerLine = '| ' + header.join(' | ') + ' |'
const delim = '| ' + header.map(() => '---').join(' | ') + ' |'
const bodyLines = body.map(r => '| ' + r.join(' | ') + ' |')
return [headerLine, delim, ...bodyLines].join('\n')
}
// Expose plugin registration to window for simple local plugins
window.registerEditorPlugin = registerPlugin
// Example: auto word count plugin loaded if present in global scope
if (window.__autoLoadPlugins) window.__autoLoadPlugins.forEach(p => registerPlugin(p))
Step 4 — Plugin architecture (keep it tiny and composable)
Design the plugin API so that plugins can:
- Register commands
- Attach UI controls or context menu items
- Listen to export events and augment output
We already added registerEditorPlugin which calls onInit. A plugin can register commands by calling registerCommand. Here’s a simple plugin that adds a CSV import command and a small UI button.
// plugins/csvImporter.js (loaded by bundler or injected in index.html)
const csvImporter = {
onInit({ editor, registerCommand }) {
// Add a button to the DOM — lightweight approach
const btn = document.createElement('button')
btn.textContent = 'Import CSV'
btn.addEventListener('click', async () => {
const csv = prompt('Paste CSV here')
if (!csv) return
const table = csvToTable(csv)
editor.appendChild(table)
})
document.querySelector('.toolbar').appendChild(btn)
registerCommand('importCsv', (csv) => {
const table = csvToTable(csv)
editor.appendChild(table)
})
}
}
function csvToTable(csv) {
const rows = csv.split(/\r?\n/).filter(Boolean).map(r => r.split(','))
const table = document.createElement('table')
rows.forEach((r, i) => {
const tr = table.insertRow()
r.forEach(cell => {
const td = tr.insertCell()
td.contentEditable = true
td.textContent = cell.trim()
})
})
return table
}
window.__autoLoadPlugins = window.__autoLoadPlugins || []
window.__autoLoadPlugins.push(csvImporter)
Step 5 — Table UX details (tiny features that matter)
Table support isn't just HTML — it’s about the editing experience. Add these affordances early:
- Quick row/column controls: small handles at the top-left to add/remove rows and columns
- CSV paste: detect pasted CSV and convert to table
- Smart header detection: if first row contains mostly non-numeric values, treat as header
- Keyboard navigation: Tab to move cells, Shift+Tab backward
Below is a compact example of handling CSV paste and Tab navigation.
// Add to renderer.js
editor.addEventListener('paste', (ev) => {
const text = (ev.clipboardData || window.clipboardData).getData('text')
if (text.includes(',') || text.includes('\t')) {
// rough CSV/TSV detection
ev.preventDefault()
const delim = text.includes('\t') ? '\t' : ','
const rows = text.split(/\r?\n/).filter(Boolean).map(r => r.split(delim))
const table = document.createElement('table')
rows.forEach(r => {
const tr = table.insertRow()
r.forEach(cell => { const td = tr.insertCell(); td.contentEditable = 'true'; td.textContent = cell.trim() })
})
editor.appendChild(table)
}
})
// Tab navigation inside table cells
editor.addEventListener('keydown', (ev) => {
if (ev.key === 'Tab') {
const sel = window.getSelection()
const td = sel.anchorNode && sel.anchorNode.closest && sel.anchorNode.closest('td')
if (td) {
ev.preventDefault()
const cell = ev.shiftKey ? prevTableCell(td) : nextTableCell(td)
if (cell) {
cell.focus()
placeCaretAtStart(cell)
}
}
}
})
Step 6 — Markdown export: rules & edge cases
Tables are the trickiest part of HTML -> Markdown. The minimal converter included above handles most simple cases: headers, rows, and cell content. Here are practical tips for real projects:
- Escape pipes (|) inside cells — replace with | or wrap with code spans if content is complex
- Truncate or warn on very wide tables (more than 10 columns) — consider CSV export instead
- Provide an option to export HTML alongside Markdown for fidelity
For production-grade conversion, integrate turndown or a purpose-built HTML-to-markdown library, but keep your app resilient to library changes by writing focused table conversion tests.
Packaging & distribution (quick notes)
In 2026, common packaging choices for Electron are still electron-builder and electron-forge. For small apps, electron-forge gives a fast dev loop; electron-builder provides more control for signing and cross-platform artifacts.
- Mac: notarize and sign — 2026 Apple policies still require notarization for smoother installs
- Windows: set up AutoUpdater (Squirrel or electron-updater) for seamless updates
- Linux: AppImage and Snap remain useful for broad compatibility
Security checklist
- Keep contextIsolation enabled and use a preload bridge
- Do not enable nodeIntegration in the renderer
- Validate and sanitize pasted content and plugin input
- Code-sign binaries before distribution
Performance & UX: small optimizations that feel big
Small apps win by being snappy. Consider:
- Lazy-load heavy plugins only when invoked
- Use requestAnimationFrame for DOM updates during large paste events
- Offer a focused reading mode with typography scaling for long notes
- Support OS-native shortcuts: Cmd/Ctrl+S, Cmd/Ctrl+P, Cmd/Ctrl+F
Testing & CI
Include unit tests for critical conversions (table -> markdown, CSV import). For end-to-end checks, Playwright works well with Electron to validate UI flows like paste->table->export. In 2026 CI pipelines, cache node_modules and artifact builds to speed up releases.
Future predictions & Trends (2026)
Looking ahead, here are trends to watch and how to adapt your lightweight editor:
- Composable runtimes: expect easier migration paths between Electron and leaner runtimes — design your code to isolate native integrations
- AI-assisted editing: plugins that summarize table data or generate CSVs from natural language will be common; design plugin hooks to pass structured table data instead of raw HTML
- Collaboration vs. privacy: lightweight editors will offer ephemeral collaboration (websocket-based) while keeping local-first defaults
"The future of small dev tools is not in packing every feature, but in composing a few robust primitives and letting plugins add the rest."
Real-world case study (quick)
One small DevOps team I mentored in late 2025 replaced ad-hoc Excel paste workflows with a compact Electron editor similar to this approach. They used CSV import, table paste, and Markdown exports to sync runbooks into Git. The result: fewer context switches, standardized documentation, and measurable time savings during incident postmortems.
Actionable checklist before shipping
- Lock down preload API and validate inputs
- Add tests for table->markdown edge cases
- Implement a minimal plugin loader and document the hook surface
- Set up packaging for target OSes and configure auto-update
- Collect telemetry opt-in to learn how users use tables vs. text
Wrap-up: Why this approach wins
This project is purpose-built for developer workflows: it’s small, predictable, and extendable. Instead of shipping a monolith, you create a core that does a few things very well (editing, tables, Markdown export) and a plugin surface for future features. That pattern reduces onboarding friction and makes incremental improvements — the exact pain points we started with.
Next steps (get this running locally)
Clone the skeleton, implement the components above, and iterate quickly on the table UX. If you want to go deeper, add:
- Undo/redo stacks for table edits
- Cell type detection (numeric, date) with formatting rules
- Export templates for different Markdown flavors
Call to action
Try building the minimal editor today: scaffold the repository, wire the preload API, and implement the table conversion function. Share your plugin ideas or a repository link in the comments — I’ll review the best ones and publish a follow-up showing real plugin code and automated tests. If you want a starter repo, I can generate an initial template you can clone and extend.
Related Reading
- The Evolution of Smoke‑Free Homes in 2026: Practical Toxin‑Reduction and Air Strategies for Families
- Freelancing for Government and Regulated AI Contracts: What Creators Should Know from BigBear.ai’s Pivot
- Use a Mini PC (Mac mini M4) to Run Your Cellar Inventory and Tasting Notes: Setup and App Recommendations
- Hands‑On Review: Total Gym X1 (2026) — Studio‑Grade Features for the Pro Home Trainer
- Should You Buy Flood or Wildfire Insurance in Retirement? A Practical Decision Guide
Related Topics
Unknown
Contributor
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you
The Future of Transportation Management: Integrating Autonomy
Will Apple's AI Chatbot Transform Development on iOS?
Navigating AI in Cloud Infrastructure: What Railway's Rise Means for Developers
Apple's AI Wearable: Potential for Developers to Build Revolutionary Applications
The Hidden Costs of Anti-Rollback Measures: A Developer's Perspective
From Our Network
Trending stories across our publication group