Repository navigation
Design Language
This page defines how Blinkql looks, moves, behaves and speaks, and explains why each choice was made. It's for anyone who builds or reviews the interface. The implementation side (the CSS tokens, the shadcn/ui setup, and theming the editor, grid and charts) lives in Design Tokens.
Status: version 1, written before the Milestone 0 scaffold. To change something, open a discussion or a pull request, the same as for any other decision.

If you read nothing else:
- Calm grey, colour with meaning. Surfaces are a slightly green "machine grey", taken from the paint used on factory control cabinets. Colour appears only when it means something.
- Signal blue marks what you can act on. That's the Run button, links, focus and selection, and nothing else.
- One living signal, the lamp. The dot on the "i" in the logo is the engine's status lamp. It blinks while a query runs and flashes once when results arrive.
-
Type built to be told apart. Atkinson Hyperlegible Next and Mono make
B10O1andB1001look different, which matters in factory data. - Dense, docked and keyboard-first, built from shadcn/ui components with the compact Mira style.
- Motion only explains a change. Nothing moves for decoration, and every animation respects reduced-motion settings.
- Checked, not guessed. Every colour pair was checked by script against WCAG 2.2 AA and simulated for colour blindness. The numbers are on this page.
- The brief
- Direction: the control room
- Brand mark
- Colour
- Typography
- Layout and space
- Components: shadcn/ui
- Motion
- Interaction
- Writing
- Accessibility checklist
- Marketing surfaces
- Open decisions
- Method and references
What Blinkql is. A SQL workbench that runs entirely in the browser. It's for exports that are too big for a spreadsheet and too sensitive to upload anywhere.
Its primary job. Get someone from "I have a file" to "I have the answer" as fast as possible, and leave them certain that the data never left their computer.
Who uses it, and what each person needs from the interface (drawn from Use Cases):
| Person | Arrives with | Needs from the interface |
|---|---|---|
| Production or process engineer | A 1–2 million row MES export | Speed, and numbers that line up for comparison |
| Maintenance manager | A breakdown log | A chart clear enough to show at the weekly review |
| Shift supervisor or plant HR | Attendance data with personal details | Visible reassurance that nothing is uploaded |
| Stores or inventory controller | Two exports that should agree | Differences that stand out from thousands of matching rows |
| Implementation engineer | A client master file under NDA | Fast profiling and exact values, such as NULL vs empty |
| Data or backend engineer | Remote Parquet, JSON logs | Density, keyboard speed, real SQL |
| Learner or trainer | A shared link | A first screen that says what to do next |
Where it's used, and what that requires:
| Condition | Design consequence |
|---|---|
| Managed Windows laptops at 1366×768, or 1920×1080 at 125% scaling | Desktop-first, works at 1280 CSS px wide, dense but legible. Supports Windows contrast themes (forced colours). |
| Bright offices and shop-floor cabins | A real light theme with true contrast, not pale grey on white |
| Long sessions (reconciliation, month-end) | Calm surfaces, no decorative motion, and a dark theme designed as an equal |
| Review meetings on a projector | Charts that survive a washed-out projector: strong lightness contrast and direct labels |
| Sensitive data: payroll, attendance, client NDAs | Privacy reassurance at the moments people worry, not hidden in a policy page |
Codes like B10O1, B1001, E1042
|
A typeface that can't confuse 0 with O, or 1 with l and I |
Blinkql's maintainer has spent years building MES and IIoT dashboards for factories. The best control-room screens in that world follow ISA-101, the standard for high-performance HMI (human–machine interface). Its core ideas are simple:
- Backgrounds are a calm grey.
- Colour is reserved for abnormal states, so a problem stands out immediately.
- Nothing is decorated, and every value is shown with enough context to act on.
Operators work in front of these screens for twelve-hour shifts and spot a fault in a second.
Blinkql takes that discipline and leaves out the look: no bevels, no 3D gauges, no 1990s grey. Here's why it fits:
- It comes from the product's own world. Blinkql's users already know this visual language from their plants. It feels professional to them, not borrowed.
- It puts the data first. When the interface is quiet, the numbers and charts are the loudest thing on screen.
- It earns trust. People bringing payroll or NDA data want a tool that feels precise and serious. A playful one doesn't feel safe.
- It lasts all day. Low-glare greys and very little motion reduce fatigue during long sessions.
- It stands apart. Most data tools are either SaaS dashboards built from cards or dark developer consoles. A disciplined, grey-first instrument with one living signal doesn't look like either.
What makes it new, not retro, is precision rather than ornament:
- Colours are defined in OKLCH and verified by script.
- One variable type family with true tabular figures.
- Light and dark themes are each tuned separately; neither is an inversion of the other.
- Motion is driven only by real engine events.
- One signature element, the lamp, runs from the logo to the Run button.
- Grey is the resting state; colour is a signal. If something is coloured, it means one of three things: you can act on it, the system needs you, or it's your data.
- Say it three ways. Every state is shown with colour, a shape or icon, and words, never colour alone. This is ISA-101's "redundant coding" and WCAG's rule 1.4.1.
- The data is the loudest thing on screen. Chrome recedes: machine-grey bars, hairline dividers, and no cards wrapped around content.
- Show the machine working. The engine's state is always visible, and every result reports its row count and time. Performance is a feature, so the interface shows it.
- Faults stay until you act. Errors don't disappear on a timer. Like an alarm on a plant HMI, they stay until you deal with them.
- Calm surface, instant response. The visuals are quiet and the feedback is immediate. Motion only explains a change.
- Privacy where people worry. Remind people their data stays local at the moments that matter: dropping a file, opening a URL, sharing a link.
| Avoid | Why |
|---|---|
| Gradients, glass, glows, coloured section backgrounds | Decoration competes with the data and with status colours |
| Content chopped into rounded cards with soft shadows | It's a generic SaaS look and wastes space in a dense tool. Panes are docked and flush. |
| ALL-CAPS labels and letter-spaced eyebrow text | Harder to read, and column names must always appear exactly as they are |
| Emoji and illustrations inside the product | The wrong tone for sensitive data; icons do the job |
| A spinner on everything | The lamp is the one "working" signal |
| Toasts for errors | Errors need action, and toasts disappear |
| Near-black dark mode with a neon accent | Tiring to read, and a cliché. Our dark theme is machine grey at night. |
| Yellow as the brand colour | It's DuckDB's colour, and Blinkql is independent. It also collides with warnings. |
The wordmark is "blinkql" in lowercase Atkinson Hyperlegible Next at weight 720, with the dot of the i replaced by the engine lamp.
- Why a lamp: the name promises a blink, and the lamp is what blinks. In the product, the dot in the top-bar wordmark mirrors the engine's state. It lights while a query runs and goes back to rest when the query is done. The brand and the status indicator are the same object, so the mark does something instead of just sitting there.
- Why lowercase: "QL" reads as a suffix rather than a shout. Lowercase also keeps the word's shape compact at 16–24 px in the top bar.
- Why text rather than an icon: a new tool needs its name remembered first. The lamp on its own becomes the app icon, where there's no room for a word.
| File | Use |
|---|---|
blinkql-wordmark-light.svg, -dark.svg
|
Top bar, docs, slides |
blinkql-wordmark-light-animated.svg, -dark-animated.svg
|
README and landing page. The lamp blinks five times, then stays lit. It stays still if the viewer has reduced motion turned on. |
blinkql-icon.svg |
App icon, favicon, PWA icon and profile pictures: the lamp, lit, on a machine-grey panel. One icon serves both themes, because a lit lamp reads best on a dark panel and one icon is easier to recognise. |
Rules
- Minimum height is 16 px. Leave clear space of at least twice the lamp's diameter on every side.
- The letters always use
--foreground. The lamp is signal blue, or a status colour when it's showing live engine state. - Never stretch, outline or re-letter the mark, and never put ⚡ next to it.
- The SVGs are drawn from the font's outlines, so they don't depend on the font being installed.
-
Production files are in the repository's
brand/folder: the favicon set (including tab icons for "needs attention" and "error"), PWA icons, the web manifest, link-preview and social images, a brand sheet and a slide template. Its README explains why each file exists.
The ⚡ in the wiki's headings is a text shorthand from before this page existed. The product and the README use the wordmark.

Every colour in Blinkql belongs to exactly one of these kinds:
| Kind | Used for | Never used for |
|---|---|---|
| Machine grey | All surfaces, text, borders, icons | — |
| Signal blue | Things you can act on: the primary button, links, focus rings, selection, the running lamp | Headings, decoration, chart series |
| Status (red, amber, green) | System state: error, needs attention, confirmed | Data, categories, decoration |
| Data colours | Charts and data highlights | Interface chrome |
If a new colour doesn't fit one of these four kinds, it doesn't belong in the interface.
Choice: a neutral at OKLCH hue 150 with a whisper of chroma (0.002–0.012).
Why:
- It comes from the factory. The hue is derived from RAL 7035 "light grey", the standard paint on control cabinets, electrical enclosures and machine guards. Measured, RAL 7035 sits at hue ≈ 152° with chroma ≈ 0.008. Blinkql's users stand next to this colour every day.
-
It isn't a default. A trace of green makes the grey read as painted metal rather than
#F5F5F5. It also avoids the cream and beige "paper" look that many generated interfaces fall back on. - Two surfaces, one job each. The chrome (top bar, sidebar, toolbars, grid header) is a step darker than the work surface (editor, grid, charts). Your eye lands on the work, and the chrome recedes, as ISA-101 intends.
-
Dark mode is machine grey at night, not black. The work surface is
#1E211F. Pure black behind light text causes halation (letters appear to glow and smear), especially for people with astigmatism. Dark grey also leaves room to show elevation by making floating layers lighter.
| Token | Use | Light | Dark |
|---|---|---|---|
--chrome |
Top bar, toolbars, sidebar, grid header | #EFF2EF |
#181B19 |
--background |
Work surface: editor, grid, charts | #FBFCFB |
#1E211F |
--popover |
Menus, popovers, dialogs, toasts | #FFFFFF |
#282B28 |
--muted |
Quiet fills: tab lists, kbd, placeholders |
#E7EBE7 |
#292C29 |
--accent |
Hover and highlighted items (shadcn's name; it's a neutral) | #E3E8E3 |
#2F332F |
--border |
Dividers and hairlines | #D6DAD7 |
#363A37 |
--input |
Edges of fields and outline buttons (3:1 or more) | #828883 |
#6F7570 |
--foreground |
Text and icons | #1A201B |
#E7EBE7 |
--muted-foreground |
Secondary text | #585E59 |
#A5ABA6 |
--lamp-off |
The lamp at rest | #B4B9B5 |
#4A4E4B |
shadcn/ui calls its hover surface
--accent. In Blinkql that token is a neutral grey; our action colour lives in--primary. Keep this in mind when reading shadcn's docs.
Choice: #0165AF in light mode with white text, and #68AEF5 in dark mode with near-black text.
Why:
- On the shop floor, blue already means "do this". ISO 3864 and ISO 7010 use blue for mandatory action signs (wear goggles, read the manual). Blinkql borrows that meaning: blue marks what you can act on.
- It doesn't collide with status colours. Red, amber and green are reserved for system state, and blue is the one strong hue left free.
- It holds up for colour-blind people. The common red–green deficiencies (protanopia and deuteranopia) leave blue distinguishable.
- It passes contrast both ways. Light mode reaches 5.86:1 as link text and 5.86:1 behind white button text. Dark mode reaches 6.93:1 and 7.72:1.
Rule: one blue-filled button per view. In the workbench that's almost always Run.
| Token | Use | Light | Dark |
|---|---|---|---|
--primary |
Primary button, links, running lamp | #0165AF |
#68AEF5 |
--primary-foreground |
Text on primary | #FAFCFE |
#071727 |
--ring |
Focus ring (2 px, offset 2 px) | #1577C8 |
#549DE5 |
--selection |
Selected rows, cells and text | #DAECFF |
#1D3954 |
Choice: red, amber and green, taken from safety-sign colours and used only for system state.
Why: in ISA-101, alarm colours are kept exclusively for alarms, so they never lose their urgency. If a chart bar were red just because it was the third category, a real error would stop standing out.
Rules:
- Every status uses an icon or lamp, plus a colour, plus words. "Query failed" is always written out, not just shown in red.
- Amber is too light for text on light backgrounds. Use it as a fill with
--warning-foregroundtext, or use--warning-textfor amber text. - Green means confirmed (saved, exported) and appears briefly. A finished query does not turn green. Done is the normal state, and normal states aren't coloured.
| Token | Use | Light | Dark |
|---|---|---|---|
--destructive |
Errors and destructive buttons | #C32324 |
#EF6661 |
--destructive-foreground |
Text on destructive | #FEFBFA |
#250E0D |
--warning |
Needs attention: lamp and badge fill | #F5AE39 |
#F2AF48 |
--warning-foreground |
Text on warning | #472400 |
#351900 |
--warning-text |
Amber text on surfaces | #965802 |
#F2B95A |
--success |
Confirmed: saved, exported | #13804A |
#4EBE7D |
--success-foreground |
Text on success | #FAFDFB |
#051B0E |

Charts follow the same principle as the interface: grey first, with colour only where it carries meaning.
| Chart situation | Colour rule | Why |
|---|---|---|
| One measure (most charts) | Bars and lines use --data-ink, a blue-tinted graphite. Emphasis comes from de-emphasising everything else with --data-ink-muted. |
One measure doesn't need a hue. In a Pareto chart, the vital few stay dark and the trivial many fade, which is clear to everyone, including on a washed-out projector. |
| Categories (one line per machine) |
--chart-1 to --chart-6, always in that order. If there are more than six categories, group the rest as "Other" in --data-ink-muted. |
The order was chosen to keep the first few colours as far apart as possible (below). Beyond six hues, no palette stays distinguishable for colour-blind viewers while also meeting 3:1 contrast. |
| One quantity, low to high (heatmaps, density) | The sequential blue ramp | Lightness carries the value, and lightness survives every colour-vision deficiency |
| Above and below a midpoint (stock variance, plan vs actual) | The diverging brown–blue ramp with a grey middle | Brown and blue stay distinguishable in all three common colour-vision deficiencies. A red–green ramp would fail for about 1 in 12 men. |
| Targets and thresholds | A dashed --data-reference line with a text label
|
A threshold is information, so it gets words |
| "Below target is bad" | Only an analysis template may use a status colour on data, and always with a labelled threshold | So that red in a chart always means a real, defined problem |
How the categorical palette was chosen. A script searched OKLCH colour space for six colours that pass all of these checks:
- At least 3:1 contrast against the work surface (WCAG 1.4.11 for graphics), in each theme.
- No hue inside the red band used for errors.
- As far apart as possible when seen with normal vision and when simulated for protanopia, deuteranopia and tritanopia (Machado et al., 2009).
Distances are measured in OKLab (×100). As a rough guide, about 2 is just noticeable, and above 10 the colours are clearly different at a glance.
| Token | Role | Light | Dark |
|---|---|---|---|
--data-ink |
The single measure, emphasised items | #46515D |
#A9B9CA |
--data-ink-muted |
De-emphasised items, "Other" | #AAB2BA |
#4D5660 |
--data-reference |
Targets, thresholds, cumulative lines (with labels) | #494F4A |
#B3B9B4 |
--gridline |
Chart gridlines and grid row dividers | #E2E6E2 |
#2B2F2C |
--chart-1 |
Category 1 (sky blue) | #0993E5 |
#96C9FE |
--chart-2 |
Category 2 (brown and orange) | #8A4C04 |
#DA892A |
--chart-3 |
Category 3 (raspberry) | #C13D67 |
#D55078 |
--chart-4 |
Category 4 (teal) | #0A819D |
#0C95B4 |
--chart-5 |
Category 5 (gold and sand) | #B58209 |
#D9C472 |
--chart-6 |
Category 6 (indigo and lavender) | #464DBB |
#9F89FE |
| Check | Light | Dark |
|---|---|---|
| Lowest contrast against the work surface (needs 3:1) | 3.23:1 | 4.06:1 |
| Closest pair, typical colour vision (OKLab ×100) | 11.4 | 14.2 |
| Closest pair, protanopia (OKLab ×100) | 11.6 | 11.0 |
| Closest pair, deuteranopia (OKLab ×100) | 10.4 | 11.0 |
| Closest pair, tritanopia (OKLab ×100) | 9.2 | 10.0 |
shadcn/ui ships
--chart-1to--chart-5. Blinkql adds--chart-6, plus the data-ink, sequential and diverging tokens. See Design Tokens.
Both themes are designed as equals. By default Blinkql follows the operating system setting, and you can override it with Light, Dark or System.
- Not inverted. Dark mode has its own values. Blue gets lighter and less saturated, text drops slightly below white, and chart colours were re-optimised separately for the dark background.
-
Elevation flips. In light mode, floating layers are white with a shadow. In dark mode they're lighter than the surface beneath (
--popoversits above--background), because shadows are hard to see on dark grey. - Same meanings. Every token keeps the same role in both themes. Code never checks which theme is active to choose a colour.
Every pair that carries text or meaning was checked with the WCAG 2.x contrast formula. Requirements: 4.5:1 for text, and 3:1 for user-interface parts and graphics.
| Pair | Needs | Light | Dark |
|---|---|---|---|
Body text: foreground on background
|
4.5:1 | ✅ 16.12:1 | ✅ 13.49:1 |
Text on chrome: foreground on chrome
|
4.5:1 | ✅ 14.70:1 | ✅ 14.41:1 |
Secondary text: muted-foreground on background
|
4.5:1 | ✅ 6.46:1 | ✅ 6.94:1 |
Secondary text on chrome: muted-foreground on chrome
|
4.5:1 | ✅ 5.89:1 | ✅ 7.42:1 |
Secondary text on muted fills: muted-foreground on muted
|
4.5:1 | ✅ 5.52:1 | ✅ 6.04:1 |
Primary button label: primary-foreground on primary
|
4.5:1 | ✅ 5.86:1 | ✅ 7.72:1 |
Link text: primary on background
|
4.5:1 | ✅ 5.86:1 | ✅ 6.93:1 |
Destructive button label: destructive-foreground on destructive
|
4.5:1 | ✅ 5.69:1 | ✅ 5.88:1 |
Error text: destructive on background
|
4.5:1 | ✅ 5.69:1 | ✅ 5.22:1 |
Text on amber: warning-foreground on warning
|
4.5:1 | ✅ 7.23:1 | ✅ 8.53:1 |
Amber text: warning-text on background
|
4.5:1 | ✅ 5.53:1 | ✅ 9.18:1 |
Text on green: success-foreground on success
|
4.5:1 | ✅ 4.87:1 | ✅ 7.68:1 |
Text in a selected row: foreground on selection
|
4.5:1 | ✅ 13.76:1 | ✅ 9.88:1 |
Menu text: foreground on popover
|
4.5:1 | ✅ 16.58:1 | ✅ 11.89:1 |
SQL keywords: syntax-keyword on background
|
4.5:1 | ✅ 5.86:1 | ✅ 7.59:1 |
SQL strings: syntax-string on background
|
4.5:1 | ✅ 6.55:1 | ✅ 9.20:1 |
SQL numbers: syntax-number on background
|
4.5:1 | ✅ 6.85:1 | ✅ 7.84:1 |
SQL functions: syntax-function on background
|
4.5:1 | ✅ 7.93:1 | ✅ 9.05:1 |
SQL comments: syntax-comment on background
|
4.5:1 | ✅ 4.88:1 | ✅ 5.23:1 |
Field edge (1.4.11): input on background
|
3:1 | ✅ 3.52:1 | ✅ 3.45:1 |
Field edge on chrome: input on chrome
|
3:1 | ✅ 3.21:1 | ✅ 3.68:1 |
Focus ring: ring on background
|
3:1 | ✅ 4.53:1 | ✅ 5.67:1 |
Focus ring on chrome: ring on chrome
|
3:1 | ✅ 4.13:1 | ✅ 6.06:1 |
Green icon: success on background
|
3:1 | ✅ 4.85:1 | ✅ 6.95:1 |
Chart marks: data-ink on background
|
3:1 | ✅ 7.87:1 | ✅ 8.11:1 |
Reference line and label: data-reference on background
|
4.5:1 | ✅ 8.17:1 | ✅ 8.14:1 |

Choice: Atkinson Hyperlegible Next for the interface and data, and Atkinson Hyperlegible Mono for SQL and code. Both come from the Braille Institute, were released in 2025 and are free under the OFL.
Why. Blinkql's data is full of codes: part numbers, asset tags, employee IDs, machine names. Misreading one code during a reconciliation is a real error with real cost. We compared three candidates on the same text, the comparison shown above:
| Criterion | Atkinson Hyperlegible Next and Mono | IBM Plex Sans and Mono | Archivo |
|---|---|---|---|
| 0 vs O by default | ✅ The zero is slashed | ❌ Nearly identical | |
| I vs l vs 1 by default | ✅ Serifed I, tailed l | ✅ | ❌ I and l identical |
Tabular figures (tnum) |
✅ | ✅ | ✅ |
| Matching monospace | ✅ Same design family | ✅ | ❌ None |
| Variable font | ✅ Weight 200–800 | ✅ Weight and width | ✅ Weight and width |
| Designed for | Readers with low vision | IBM's brand | General grotesque |
| Download size (Latin, WOFF2) | 34 KB + 18 KB | — | — |
Atkinson wins on the criterion that matters most here, telling characters apart, and it directly supports goal G6 (accessible). It has a recognisably engineered character without being quirky. It also isn't one of the defaults: Inter and Geist appear in most shadcn projects, and using either would make Blinkql look like everyone else.
Trade-off we accept: Atkinson covers Latin scripts only (most of Latin Extended-A, no Cyrillic, Greek or Devanagari). Data in other scripts falls back to the operating system's fonts, such as Segoe UI or Nirmala UI on Windows. The interface copy is English, so this affects only data cells, and the system fonts render those well.
Fonts are self-hosted through Fontsource packages, as the DuckDB engine is (ADR-008). That means there are no third-party requests at runtime, which keeps the privacy promise and works under the strict Content Security Policy and cross-origin isolation headers.
| Role | Face | Where |
|---|---|---|
| Interface and data | Next | Everything by default, including grid cells |
| Code | Mono | SQL editor, inline SQL, engine messages, column type names |
| Numbers | Next with tabular figures | Grid, row counts, timings, axis ticks, timers |
Why the grid uses Next, not Mono. Proportional text fits more characters into a column and is faster to read. Numbers still line up because of tabular figures. Next already tells codes apart, which is the usual reason data tools reach for a monospace.
The scale uses the traditional steps from Robert Bringhurst's The Elements of Typographic Style (…11, 12, 14, 16, 18, 24, 36, 48, 60…). The app uses the small end; the large sizes are for empty states and the landing page.
| Step | Size / line height | Weight | Use |
|---|---|---|---|
| Hero | 60 / 64 | 700 | The landing page headline only, from 1024 px wide |
| Display | 48 / 52 | 700 | Empty states, landing hero below 1024 px |
| Title | 36 / 42 | 700 | Landing sections, onboarding |
| Heading | 24 / 30 | 600 | Dialog and page headings |
| Subheading | 18 / 26 | 600 | Chart titles, panel headings |
| Reading | 16 / 26 | 400 | Descriptions, onboarding text, help |
| Body (default) | 14 / 20 | 400 / 600 | Controls, menus, grid cells, editor (Mono, 14 / 21) |
| Meta | 12 / 16 | 400 / 600 | Secondary details, counts, keyboard hints, section labels |
| Micro | 11 / 14 | 400 | Column type names and chart axis ticks only |
- Why a 14 px body: Atkinson's x-height is 0.496 em, smaller than IBM Plex Sans (0.516) and Archivo (0.526). At 14 px it reads comfortably on a 1366×768 laptop, so 12 px is kept for secondary information and 11 px for single words.
- Why a Hero step: on a wide screen, a 48 px landing headline is barely larger than the 36 px section titles beneath it, so the page has no clear top. Hero is the next step of the same scale, kept to one line of one page so the app's hierarchy is unchanged. Below 1024 px the headline drops to Display, then to Title on phones. Large sizes get slightly tighter tracking, -0.01 em at Display and -0.015 em at Hero, because Atkinson's spacing is tuned for reading sizes.
- Three weights only: 400, 600 and 700. Fewer weights give a clearer hierarchy and less to download, even with a variable font.
-
Line length: running text, such as descriptions, dialogs and error details, stays within 72 characters (
max-width: 72ch).
- Sentence case everywhere. No all-caps labels, and no title case except for proper nouns.
- Names are data. Column, table and file names appear exactly as they are, never re-cased or prettified, because users type them into SQL. If a name is truncated, a tooltip shows it in full.
-
Engine type names are shown as the engine reports them (
VARCHAR,TIMESTAMP), in Mono at 11 px. They're data, not labels. - Tabular figures apply wherever numbers align or update live. A running timer whose digits change width jiggles; with tabular figures it stays still.
- We never re-format your SQL unless you ask, using the Format button. Keywords stay in whatever case you typed.
-
No ligatures in code.
<>must look like<>. Atkinson Hyperlegible Mono has no ligature features, which we verified in the font file.
Large type is used rarely, only for empty states, onboarding and the landing page. When it is, the full stop is the lamp. On the first-run screen, the full stop in "Drop a file to start" is dim while the engine loads and lights up when it's ready. It's the same device as the wordmark: one bold idea, used consistently.

┌──────────────────────────────────────────────────────────────────────────────┐
│ blinkql │ production_log.parquet ▾ Local only [Search… Ctrl K] ◐ ? │ top bar 44
├────────────┬─────────────────────────────────────────────────────────────────┤
│ Tables │ OEE by machine │ Downtime Pareto │ + Format [▶ Run Ctrl↵]│ query tabs 36
│ ▾ prod_log │ 1 WITH daily AS ( │
│ T mach… │ 2 SELECT … SQL editor │
│ # run_… │ │
│ ▸ breakdo… ├─────────────────────────────────────────────────────────────────┤ ← drag to resize
│ │ Results Chart Profile ● 40 rows in 212 ms Export│ results bar 40
│ Saved │ # machine_id shift_date availability quality oee │ grid header 44
│ queries │ 1 PRS-03 2026-10-06 0.612 0.951 0.509 │ rows 28
│ │ 2 CNC-12 2026-10-05 0.688 0.968 0.601 │
│ [Add file] │ … │
└────────────┴─────────────────────────────────────────────────────────────────┘
sidebar 248 (resizable 200–400, collapsible)

- Docked panes, no floating cards. The sidebar, editor and results sit flush and are separated by 1 px borders. Every pixel goes to the data.
- The results stay where they are. Running a query never moves the editor or the results bar, so there's no layout shift (the CLS budget is 0.1 or less).
- The speed readout sits where your eyes already are, in the results bar right next to the rows: "40 rows in 212 ms".
| Element | Compact (default) | Comfortable |
|---|---|---|
| Top bar | 44 | 44 |
| Query tabs, results bar | 36, 40 | 36, 40 |
| Controls (buttons, inputs) | 28 | 36 |
| Grid row | 28 | 36 |
| Grid header (name and type) | 44 | 52 |
| Sidebar | 248 | 280 |
Density is a single setting. Compact fits about 25 rows on a 768 px-high laptop screen. Comfortable suits touch screens and anyone who wants more room. Both keep click targets at 24 × 24 px or larger (WCAG 2.5.8).
A 4 px base grid, using the steps 4, 8, 12, 16, 24, 32, 48, 96. Inside a component, use 4–12. Between groups, use 16–24. Page-level whitespace (empty states, onboarding) uses 48–96. Use only these steps.
| Thing | Radius | Elevation | Why |
|---|---|---|---|
| Docked panes (sidebar, editor, grid) | 0 | Border only | They're the fixed parts of the machine |
| Controls (buttons, inputs, tabs) | 6 px (--radius) |
None | Soft enough to look clickable, crisp enough to look precise |
| Small items (kbd, tooltips) | 4 px | None | Proportional to their size |
| Floating layers (menus, popovers, dialogs, toasts) | 10 px | Shadow (light) or lighter surface (dark) | They're temporary and sit above the work |
| Lamp, status dots, switches | Fully round | None | They represent physical indicators |
No more than two floating layers at once. If you'd need a third, use a dialog instead.
- Left-align text, labels and headings.
- Right-align numbers, and align each column header with its data.
- Empty states are left-aligned at the top third of the work area rather than centred in a box, so the headline reads like the first line of a page.
- Centre things only in icon buttons and lamps.
| Width | Behaviour |
|---|---|
| ≥ 1280 px | Full layout |
| 1024–1279 px | The sidebar collapses to icons |
| 768–1023 px | The sidebar becomes a slide-out sheet; editor and results become tabs |
| < 768 px | Viewing small files only. Mobile isn't a target (see Goals & Non-Goals). |

| Setting | Choice | Why |
|---|---|---|
| Style | Mira | shadcn describes it as "Compact. Made for dense interfaces.", which is exactly what a data workbench is. Of the other seven styles, Nova is also compact (a tightened classic) and is the fallback. Rhea is compact but soft and rounded, from the Luma family. Lyra is boxy with mono fonts, the developer-console direction we didn't choose. Vega, Maia and Luma are roomier. Sera is editorial, with serif headings and uppercase tracking, both against the rules above. Compare Mira and Nova in shadcn/create with our tokens before committing. |
| Component base | Base UI (ADR-009) | Base UI has been shadcn's default since July 2026 and is recommended for new projects. Nothing was built yet, so choosing it cost nothing. Radix remains supported, but don't mix the two. |
| Icons | lucide at 16 px with stroke 1.75 | Already in the stack. Stroke 1.75 matches the stem weight of 14 px Atkinson text; lucide's default of 2 looks heavy next to it. |
| Fonts | Atkinson Hyperlegible Next and Mono | See §5 |
| Radius |
0.375rem (6 px) |
See §6.4 |
| Theme | Blinkql tokens | Replace the generated theme with the CSS in Design Tokens |
Rule: Blinkql doesn't build its own UI components for now. Every control is a shadcn/ui component, or a composition of several, styled only through tokens and the component's own variants.
There are exactly three exceptions, all required by the Architecture:
| Surface | Built with | How it follows this design language |
|---|---|---|
| SQL editor | CodeMirror 6 | Theme built from CSS variables, using the --syntax-* tokens |
| Results grid | TanStack Virtual, later a canvas grid | Reads the same tokens; canvas colours are refreshed when the theme changes |
| Charts | ECharts and uPlot (canvas) | A registered theme built from --chart-*, --data-* and --gridline
|
Don't use shadcn's Chart component. It's built on Recharts, which renders SVG, and that conflicts with ADR-006 (canvas charts). Use its --chart-N token convention only. Likewise, shadcn's Table and data-table recipe are fine for small tables like settings or column statistics, but never for query results, because they put every row into the page.
| Blinkql need | shadcn/ui components | Notes |
|---|---|---|
| App shell, catalog |
Sidebar, Collapsible, ScrollArea
|
Sidebar tokens map to the machine-grey chrome |
| Editor and results split | Resizable |
The sizes are remembered between visits |
| Query tabs, Results / Chart / Profile | Tabs |
|
| Run, Stop, Format, Export |
Button, ButtonGroup, Kbd, Tooltip
|
One default (blue) button per view |
| Engine status |
Badge (outline) with a lamp dot, Tooltip
|
The lamp is a styled dot inside the badge, not a new component. Don't use Spinner. |
| Command palette |
Command in a Dialog
|
Ctrl/⌘ K |
| Column header menu | DropdownMenu |
Sort, filter, profile, copy name, hide |
| Grid right-click | ContextMenu |
Copy, copy as SQL IN (…), filter to value |
| Import options (delimiter, header, types) |
Sheet, Field, Select, RadioGroup, Switch, Input
|
|
| Remote URL |
Dialog, InputGroup, Field
|
Explains CORS in the field description |
| Empty states, first run |
Empty, Button, Item
|
|
| Errors |
Alert (destructive) with actions |
Persistent; never a toast |
| Confirmations | Toast |
Only for background confirmations such as "Query saved", with Undo |
| Irreversible actions | AlertDialog |
Only for deleting a file you chose to keep |
| Progress on large imports | Progress |
Shown only when progress can be measured (bytes read); otherwise the lamp |
| Column statistics |
Popover, HoverCard, Table (small) |
|
| Settings |
Dialog, Field, ToggleGroup (theme, density) |
| Variant | Use | Example |
|---|---|---|
default (signal blue) |
The one main action in a view | Run, Choose file |
outline |
Secondary actions that need a clear edge | Stop, Use shift_date, Open a URL |
secondary |
Supporting actions in toolbars | Save query |
ghost |
Low-emphasis and repeated actions | Format, Export, Open |
destructive |
Removing something you can't get back | Delete file |
link |
Navigation to docs | Learn more |
Run and Stop share one position. While a query runs, the Run button becomes Stop with the elapsed time beside it. Your mouse is already there, and the layout doesn't move.
- Motion explains a change; it never decorates. Nothing animates just because a section appears.
-
Fast and out of the way. Entrances decelerate (
--ease-out), exits accelerate (--ease-in) and use the shorter duration. -
Cheap to render. Animate only
transformandopacity, never anything that forces layout. Nothing animates while the grid scrolls, so the 60 fps scrolling budget belongs to the data. - No layout animation in the results area. It would cost time and add layout shift.
-
Reduced motion is respected everywhere. With
prefers-reduced-motion, durations drop to zero and the lamp holds steady.
| Token | Value | Use |
|---|---|---|
--duration-fast |
100 ms | Hover and press colour changes; all exits |
--duration-base |
160 ms | Menus, popovers, tooltips appearing |
--duration-slow |
240 ms | Dialogs and sheets appearing |
--ease-out |
cubic-bezier(0.2, 0, 0, 1) |
Entering: a fast start and a soft landing |
--ease-in |
cubic-bezier(0.4, 0, 1, 1) |
Leaving: gets out of the way |
--lamp-blink |
1000 ms | Running: 0.5 s on, 0.5 s off |
--lamp-breathe |
1600 ms | Engine starting: a slower rhythm, so it reads differently from "running" |
The lamp is the single piece of "living" motion in Blinkql. It appears in the wordmark, in the results bar and in the status badge, and it always shows the same thing: what the engine is doing.
| State | Lamp | Rhythm | Words shown alongside |
|---|---|---|---|
| Starting the engine | Blue, dim | Slow fade, 1.6 s | "Starting the engine…" |
| Ready | Grey, at rest | Still | "Ready" (tooltip) |
| Running | Blue | Blinks at 1 Hz for at most 5 s, then holds steady | "Running 3.4 s, 412,000 rows so far" |
| Done: the blink | One blue flash with an expanding halo, then back to rest | 400 ms, once | "40 rows in 212 ms" |
| Needs attention | Amber | Still | "Memory 78% used" |
| Error | Red | Still, until you act | "Query failed", plus the error alert |
Why it's designed this way:
- Distinct rhythms, not just colours. Starting fades, running blinks and done flashes. You can tell the states apart without seeing colour.
- Within flash limits. One blink per second is well under the three-flashes-per-second limit (WCAG 2.3.1).
- Blinking stops after five seconds. For long queries, the lamp holds steady and the ticking timer carries the "still working" signal. This meets WCAG 2.2.2 (pause, stop, hide) without needing a pause button.
- Done doesn't turn green. A successful result is the normal outcome. The flash says "arrived" and the lamp goes back to rest, because a normal state isn't coloured.
- For screen readers, a polite live region announces "Query finished, 40 rows in 212 ms" once, not on every tick.
- In Windows contrast themes, the lamp gets an outline in the system text colour, and the words carry the meaning.
| Moves | Doesn't move |
|---|---|
| The lamp (above) | Page sections, cards, headings appearing |
| Menus, popovers, tooltips: fade and 4 px slide, 160 ms | Grid rows as they stream in. They simply appear, and the count updates. |
| Dialogs and sheets: fade and scale or slide, 240 ms | Chart drawing. Charts render in their final state, because animating them delays reading the values. |
| Resizing panes, which follow the pointer directly | Skeleton shimmer. Placeholders are static, because the lamp already says "working". |
| Hover and press feedback: colour, 100 ms | Number count-ups, parallax, scroll-triggered effects |
| Action | Windows / Linux | macOS |
|---|---|---|
| Run the statement at the cursor, or the selection | Ctrl Enter | ⌘ Enter |
| Run everything in the tab | Ctrl Shift Enter | ⌘ Shift Enter |
| Stop the running query | Ctrl . | ⌘ . (the standard macOS “cancel”) |
| Command palette | Ctrl K | ⌘ K |
| Format SQL | Ctrl Shift F | ⌘ Shift F |
| New query tab | Ctrl Alt N | ⌘ Option N |
| Move between sidebar, editor and results | Tab, Shift Tab (in the editor, press Esc first) | Same |
| Copy selected cells (tab-separated, pastes into Excel) | Ctrl C | ⌘ C |
| Show all shortcuts | ? (outside the editor) | ? |
Every shortcut appears in its button's tooltip and in the command palette, so people learn them by using the interface. None of them clash with browser shortcuts or CodeMirror's defaults: Ctrl / toggles a comment and F6 moves to the address bar, so neither is used.
- Run (or Ctrl Enter) starts the query. The button becomes Stop (Ctrl .), and the lamp starts blinking.
- Running again while a query is still going replaces it. The old query is cancelled, never queued (P-06).
- The first rows appear as soon as they stream in. The row count and timer update in place with tabular figures, so nothing shifts.
- When the query finishes, the lamp flashes once (the blink) and the results bar shows "40 rows in 212 ms".
- Sorting and filtering in the grid run as SQL (Architecture rule 4), so they show the same lamp and timing as any other query.
Errors follow the alarm pattern: what happened, where, and what to do. They stay until you act.
- In the editor: a red wavy underline at the failing location, with the message on hover.
-
Above the results: a persistent
Alertin plain language, giving the location, a suggested fix as a button, and the engine's raw message behind "Show engine message". - When memory runs out: say so plainly, restart the engine automatically, and offer the next step: "Add a LIMIT", "Select fewer columns" or "Convert to Parquet".
- Never show a stack trace, a blank screen or an error that disappears on its own.
- You can drop a file anywhere in the window. While a file is dragged over, an overlay says "Release to open production_log.parquet. It stays on this computer." This is the moment people worry about privacy, so it's the moment we answer.
- Large files get an amber warning before loading (thresholds come from the benchmarks), with the Parquet tip.
- Opening a remote URL explains CORS in the field description, before anything can fail.
- Import settings (delimiter, header row, types) are detected automatically and shown, so you can override them, never hidden.
- Selection works like a spreadsheet: click, Shift-click and drag; Ctrl/⌘ A selects all. Copying produces tab-separated text that pastes cleanly into Excel.
- The column header menu offers sort, filter, profile, copy name and hide.
- Long values are truncated with "…" and shown in full in a popover. JSON and lists open in a formatted viewer.
-
Selection is blue (
--selection), and the focused cell has a 2 px focus ring. Hover is a neutral tint. -
There's no zebra striping. Hairline row dividers (
--gridline) guide the eye without adding visual noise.
| Value | Shown as | Why |
|---|---|---|
| Integers and decimals | Browser locale grouping, tabular figures, right-aligned: 1,048,576 (en-US), 10,48,576 (en-IN) |
People read numbers best in their own convention, including Indian lakh grouping |
| Decimals | As stored, up to a user-set precision (default 6 significant digits) | Never round silently. The tooltip shows the full value. |
| Dates |
2026-10-06 (ISO 8601), always |
04/05/2026 means April in one plant and May in another |
| Timestamps |
2026-10-06 06:00:00, plus the zone when the type carries one |
The same ambiguity problem, across time zones |
| NULL |
NULL in muted italic Mono |
Must be clearly different from an empty string |
| Empty string | An empty cell; the tooltip shows ''
|
|
| Booleans |
true / false
|
Words, not ✓ and ✗ icons |
| Column and table names | Exactly as the engine returns them | You type them into SQL |
- Every input gets a visible response within 100 ms (the INP budget is 200 ms).
- Undo beats confirmation. Closing a tab or removing a file from the session can be undone. A confirmation dialog appears only when something truly can't be recovered, such as deleting a file stored in the browser.
-
Focus is always visible: a 2 px
--ringoutline with a 2 px offset, never removed.
Plain, calm and exact, like a good shift handover: what happened, what it means, what to do next. Speak as the product, using "your" for the user's things, and never "I". Use contractions. No exclamation marks.
One word per idea, used everywhere. The button, the tooltip, the shortcut list and the confirmation all use the same word.
| Use | Not | Meaning |
|---|---|---|
| File | dataset, source, upload | What you drop in or open |
| Table | relation, view, dataframe | What you query. Each file becomes a table. |
| Query | statement, script, cell | The SQL in a tab |
| Run / Stop | execute, submit / cancel, abort | Start or end a query |
| Results | output, data, response | What a query returns |
| Rows, columns | records, fields, attributes | Parts of a table |
| Engine | DuckDB, WASM, backend, server | The thing that runs queries. Name DuckDB only in About and attribution. |
| Keep | persist, save to OPFS, cache | Storing a file in the browser between visits |
| Local only | offline, private mode, secure | No data leaves this computer |
- Buttons start with a verb, use one to three words, are in sentence case and have no full stop: "Run", "Choose file", "Use shift_date", "Restart engine".
- Errors say what happened, then what to do, with no "Error:" prefix and no apology: "Column “shift_dt” doesn’t exist in production_log. Did you mean shift_date?"
- Empty states are an invitation: a headline naming the action, one line explaining it, and a verb button. For example: "Drop a file to start", then "Choose file".
- Confirmations use the past tense of the button's verb: Save query → "Query saved"; Export → "Exported 40 rows".
- Numbers in copy use digits and the same formatting as the grid: "18,402 rows", not "eighteen thousand rows".
| Engine or generic message | Blinkql says |
|---|---|
Binder Error: Referenced column "shift_dt" not found in FROM clause! Candidate bindings: "production_log.shift_date" |
Column “shift_dt” doesn’t exist in production_log. Did you mean shift_date? [Use shift_date] |
Out of Memory Error: failed to allocate data of size 512.0 MiB |
The query needed more memory than this tab has. The engine has restarted. [Add a LIMIT] [Convert to Parquet] |
HTTP 0: Failed to fetch (remote file) |
Couldn’t read the file at that URL. Its host may not allow other sites to read it (CORS). [How to allow CORS] |
| "Upload successful!" | (not applicable: nothing is uploaded) "production_log.parquet is ready to query" |
| "No data" | "No rows match this query. Try removing a filter." |
Design-side commitments for goal G6 and WCAG 2.2 AA on core flows:
| WCAG 2.2 criterion | How this design meets it |
|---|---|
| 1.4.1 Use of colour | Every state uses an icon or lamp, plus colour, plus words (§2, §4.4) |
| 1.4.3 Contrast (minimum) | All text pairs are 4.5:1 or higher in both themes (§4.7) |
| 1.4.11 Non-text contrast | Field edges, the focus ring and chart marks are 3:1 or higher (§4.5, §4.7) |
| 1.4.4 and 1.4.10 Resize and reflow | Usable at 200% zoom; the sidebar collapses (§6.6) |
| 1.4.12 Text spacing | No fixed-height text containers outside the grid rows |
| 2.1.1 Keyboard | Every action is reachable by keyboard; see the shortcut map (§9.1) |
| 2.2.2 Pause, stop, hide | The lamp blinks for 5 s at most (§8.3) |
| 2.3.1 Three flashes | One flash per second at most |
| 2.4.7 and 2.4.11 Focus visible, not obscured | 2 px ring with offset; sticky headers never cover the focused cell |
| 2.5.8 Target size (minimum) | Targets are 24 × 24 px or larger, even in compact density |
| 4.1.3 Status messages | Query results and errors are announced through live regions |
| Beyond WCAG | Windows contrast themes (forced colours), reduced motion, a data-table alternative for every chart (ADR-006), an accessible grid structure for screen readers |
The product is the marketing: Blinkql's strongest pitch is watching a 1.7 million row file answer in milliseconds.
-
README header: the animated wordmark, one line ("Queries in the blink of an eye"), then a light and dark screenshot pair using
<picture>, so GitHub shows the one that matches the viewer's theme. - Landing page (M0, #72): the hero is the working product with a sample factory file preloaded, not an illustration. The headline is set in Hero (Display on smaller screens) with the lamp as its full stop, and the lamp blinks once when the sample query returns. Until the engine arrives in M1, the hero is a replay of the workbench built with the real tokens, labelled as a preview, and the page says plainly that Blinkql is a work in progress and invites contributors. The replay is the page's one motion sequence; nothing else moves on its own.
- Social preview card (1280 × 640): the wordmark, the one-line pitch, and a cropped workbench showing the "rows in ms" readout.
-
Profile banner (1500 × 500), brand sheet and slide template: same rules. They use two quiet recurring elements: a field of unlit lamps, and a timing strip that spells blinkql in Morse code. All of them are in
brand/. - Screenshot rules: use realistic synthetic factory data, never lorem ipsum or real customer data. Always show the timing readout, and pair light with dark.
- Never stock photos of people looking at dashboards, 3D renders or gradient blobs.
| Decision | Recommendation | When |
|---|---|---|
| Typeface validation | Include at least one low-vision participant in the M3 usability check | M3 |
| Projector check | View the chart palette on a real meeting-room projector | M3 |
| Non-Latin data | Check fallback rendering for Devanagari, Tamil and CJK data | M4 |
| ⚡ in the docs | Replace it with the wordmark in the README; keep it as shorthand in the wiki | Anytime |
Decided: the component base is Base UI (2026-10-14, ADR-009).
How the numbers on this page were produced
- Colours are defined in OKLCH and converted to sRGB using Björn Ottosson's OKLab matrices. Anything outside the sRGB gamut was reduced in chroma until it fit.
- Contrast uses the WCAG 2.x relative-luminance formula.
- Colour-vision simulations use the Machado, Oliveira and Fernandes (2009) matrices at full severity, applied in linear RGB. Distances are Euclidean in OKLab, multiplied by 100.
- Font metrics and features (x-height, tabular figures, ligatures, digit widths) were read directly from the font files with fontTools.
- The screens on this page are mockups built with the exact tokens in Design Tokens and the real fonts. The data shown is synthetic.
References
- ANSI/ISA-101.01-2015, Human Machine Interfaces for Process Automation Systems
- ISO 3864-1 and ISO 7010, safety colours and safety signs
- RAL 7035 (light grey) and RAL 5005 (signal blue)
- WCAG 2.2
- Robert Bringhurst, The Elements of Typographic Style
- Atkinson Hyperlegible, Braille Institute
- G. M. Machado, M. M. Oliveira, L. A. F. Fernandes, "A Physiologically-based Model for Simulation of Color Vision Deficiency", IEEE TVCG, 2009
- OKLab, Björn Ottosson
-
shadcn/ui: theming,
shadcn createstyles, Base UI as the default (July 2026)
⚡ Blinkql: queries in the blink of an eye · MIT licensed · Live app · Issues · Discussions
Project
Engineering
- Tech Stack
- Architecture
- Performance & Benchmarks
- Decision Records (ADRs)
- Privacy & Security
- Browser Support & Limitations
Design
Community