The workshop
My Brave new tab

Void

The new tab page I actually wanted.

A custom new tab page for Brave: a clock with something to say, search with bangs, the links I use and a small dock of tools — over a slow aurora and a field of particles that drifts on its own and reacts to the cursor. Settings, notes and tasks stay in the browser. No accounts, no tracking.

Void at 21:47: a large thin clock with the date and a ‘Standing by’ greeting, a search bar with a shell prompt, a row of letter-tile links, and a two-column dock of browser shortcuts, a picture, progress bars, notes, tasks and a focus timer, over a dark blue starfield
Manifest V3 · no permissions
Role
Design & development, solo
Built for
Me, and my browser
Built
August–September 2026
Stack
HTML · CSS · JS, no dependencies

01Why it exists

It’s a nice-to-have, honestly. But I see it every time I open a tab.

Void is my own homepage for Brave. It doesn’t solve a big problem. I wanted nice backgrounds, some effects and particles, and my shortcuts one keystroke away — on the page every new tab opens to. It makes browsing a little better.

The first version was a single 4,274-line HTML file. The same day, I moved the script into its own file and added a manifest, so Brave loads it as an extension and the new tab opens with an empty address bar, ready to type into.

I’m a developer, so it leans that way. Developer style is on by default: monospace chrome, a ❯ prompt in the search bar, an ISO date if you want one, and greetings like “Working directory clean”.

One later change: the small hours got their own voice. At 2 am a joke about caffeine isn’t what lands, so between midnight and four the greeting gets quieter — “It’s late, not final.” “Rest counts as progress.”

02The particle field

Move the cursor. The field leans with it.

Every particle gets a depth, z, between 0.2 and 1. It sets the size, the brightness and how far that particle shifts as the cursor moves — near ones more, far ones less. That one number is all the 3D there is: plain Canvas 2D, no WebGL.

A live particle field drawn on a canvas, with Void’s clock, date and greeting on top. Use the controls below to change it.

Field

—

Theme

Ported from void.js: the same draw routines and knobs, with the browser window swapped for this box. The real page has 15 modes. When your cursor isn’t over the field, an invisible one drifts in a slow loop so there’s something to follow. The clock and greeting lines are the real ones; this greeting just doesn’t know about your weather, tasks or visits.

03In the browser

Eighteen themes. One click each.

A theme sets the background, accent, aurora, particle mode, grain and vignette together. It can also follow the clock, with its own pick for dawn, day, dusk and night.

The Terminal theme: green accent, a constellation of linked points, and the Segment clock face reading 23:08 with faint unlit eights behind the digits
Terminal, with the Segment face and its unlit 88:88.
The Ember theme: a warm brown-black background with glowing orange fireflies, a serif clock reading 19:12 and the greeting ‘Good evening’
Ember: fireflies, film grain and the Serif face.
The settings drawer on the Look tab: eighteen theme cards in a three-column grid with Nebula selected, then a ‘Follow the time of day’ switch with a theme chosen for dawn, day, dusk and night
Settings: eighteen themes, and one for each part of the day.
The command palette with the letter n typed: Notion, Linear and Calendar links mixed with commands like ‘New greeting’, ‘Copy Extensions’ and ‘Add a link’
⌘ K: one fuzzy search over links and commands.

Real screenshots of the app, filled with demo data: invented links, notes and tasks, and a staged time of day.

04What it does

Small things, a keystroke away.

A clock with something to say

Twelve faces, from ultra-light to a seven-segment LED. Under it, a greeting drawn from 33 weighted rules that notice the hour, the weekday, the weather, open tasks and how long you’ve been away.

Search with bangs

Eleven engines, each with a prefix — !g, !gh and the rest. Type an address instead and it just goes there.

Links, in groups

Drag to reorder, right-click to edit. Letter tiles get a colour from the hostname. It imports Brave’s bookmarks: folders become groups, duplicates are skipped.

Your own backdrop

Eighteen themes and twelve accents — or any colour, or your own image, fitted, darkened and blurred to taste.

A dock of small tools

Notes, tasks with undo, a focus timer with a ring, day / week / year progress, and a canvas for your own images and GIFs.

Keyboard first

⌘ K for the palette, ⌘ , for settings, / to search, ⌥ 1–9 to open a link. Start typing anywhere and it lands in search.

05How it’s built

No build step. No dependencies.

Two files and a manifest. No libraries, no web fonts — every clock face uses fonts that ship with macOS. Manifest V3 forbids inline scripts on extension pages, so the script lives in its own file.

Filesindex.html, 1,439 lines of markup and CSS. void.js, 3,059 lines in one IIFE. A manifest that overrides the new tab and asks for no permissions.
StorageSettings in one localStorage record; images in IndexedDB. Export, import, and reset with undo.
NetworkLink tiles load each site’s favicon, then DuckDuckGo’s icon service, then fall back to a letter. Weather calls Open-Meteo — off by default.
ClockWrites to the page only when a value changes, and skips its tick in hidden tabs.
void.js — FIELD_DRAW.stars()
stars(f, t, sp, par){
  for (const p of parts){
    p.y -= sp * .28 * p.z * (p.r + .4);
    if (p.y < -4){ p.y = innerHeight + 4; p.x = Math.random() * innerWidth; }
    const x = p.x + tx * p.z * 26 * par;
    const y = p.y + ty * p.z * 20 * par;
    ctx.globalAlpha = (.45 + .55 * Math.abs(Math.sin(t / 1400 + p.p))) * p.z * .8;
    ctx.fillStyle = partColor(f, p);
    ctx.beginPath(); ctx.arc(x, y, p.r * p.z, 0, Math.PI * 2); ctx.fill();
  }
},

06Build notes

Pretty, but not at any price.

01

Asking about the GPU without waiting for it

Lite mode drops the blur, glow, grain and particles when hardware acceleration is off. No browser says that directly, so Void asks WebGL twice: is the renderer a software one, like SwiftShader or llvmpipe — and is a context requested with failIfMajorPerformanceCaveat refused? Brave’s fingerprinting protection can hide the first answer, but not the second. The check runs when the browser is idle, so first paint uses the saved setting, and it hands its WebGL contexts back afterwards.

02

Particles priced per pixel

The canvas never renders above 1.5× the screen’s pixels. Particle count scales with screen area, with caps for the expensive modes. Bokeh stamps one pre-rendered soft sprite instead of building a gradient for every particle. When the field is off, lite mode is on or the tab is hidden, the canvas memory is handed back — and the mousemove handler does nothing at all.

03

A dock that checks its own layout

On wide screens the dock wraps into two columns. CSS column-wrap can’t be told “at most two”, so on a short window a third column would quietly swallow cards. The code turns wrapping on, measures, and turns it off again if any card spilled out.

04

Living inside five megabytes

Images go to IndexedDB, with localStorage as the fallback when the page runs from a file. GIFs and WebPs are stored untouched so their animation survives; background photos are scaled down to 2,560 px. When a save fails, the message says whether storage is full or blocked — they have different fixes.