Skip to content

Design Language

Karthik P edited this page Oct 16, 2026 · 6 revisions
Blinkql

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.

The Blinkql workbench in light mode: a SQL editor with an OEE query above a results grid of 40 rows returned in 212 ms

The short version

If you read nothing else:

  1. 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.
  2. Signal blue marks what you can act on. That's the Run button, links, focus and selection, and nothing else.
  3. 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.
  4. Type built to be told apart. Atkinson Hyperlegible Next and Mono make B10O1 and B1001 look different, which matters in factory data.
  5. Dense, docked and keyboard-first, built from shadcn/ui components with the compact Mira style.
  6. Motion only explains a change. Nothing moves for decoration, and every animation respects reduced-motion settings.
  7. 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.

Contents

  1. The brief
  2. Direction: the control room
  3. Brand mark
  4. Colour
  5. Typography
  6. Layout and space
  7. Components: shadcn/ui
  8. Motion
  9. Interaction
  10. Writing
  11. Accessibility checklist
  12. Marketing surfaces
  13. Open decisions
  14. Method and references

1. The brief

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

2. Direction: the control room

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:

  1. 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.
  2. It puts the data first. When the interface is quiet, the numbers and charts are the loudest thing on screen.
  3. It earns trust. People bringing payroll or NDA data want a tool that feels precise and serious. A playful one doesn't feel safe.
  4. It lasts all day. Low-glare greys and very little motion reduce fatigue during long sessions.
  5. 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.

Principles

  1. 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.
  2. 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.
  3. The data is the loudest thing on screen. Chrome recedes: machine-grey bars, hairline dividers, and no cards wrapped around content.
  4. 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.
  5. 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.
  6. Calm surface, instant response. The visuals are quiet and the feedback is immediate. Motion only explains a change.
  7. Privacy where people worry. Remind people their data stays local at the moments that matter: dropping a file, opening a URL, sharing a link.

What we deliberately avoid

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.

3. Brand mark

Blinkql wordmark: lowercase blinkql with a blue dot over the i

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.


4. Colour

Colour palette sheet in light and dark themes, with hex values, OKLCH values and contrast ratios

4.1 Four kinds of colour

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.

4.2 Machine grey

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.

4.3 Signal blue

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

4.4 Status colours

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-foreground text, or use --warning-text for 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

4.5 Data colours

Data colours: data ink, six categorical colours, the same colours simulated for three colour-vision deficiencies, and sequential and diverging ramps

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-1 to --chart-5. Blinkql adds --chart-6, plus the data-ink, sequential and diverging tokens. See Design Tokens.

4.6 Light and dark

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 (--popover sits 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.

4.7 Verified contrast

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

5. Typography

Type scale using Atkinson Hyperlegible Next, a legibility comparison with IBM Plex Sans and Archivo, tabular figures, and SQL set in Atkinson Hyperlegible Mono

5.1 Typeface: Atkinson Hyperlegible Next and Atkinson Hyperlegible Mono

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 ⚠️ Different only by width ❌ 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.

5.2 Roles

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.

5.3 Scale

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).

5.4 Rules

  • 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.

5.5 The headline treatment

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.

First-run screen: the headline "Drop a file to start" ends in a dim blue lamp, with Choose file and Open a URL buttons and three factory sample datasets


6. Layout and space

6.1 Anatomy

┌──────────────────────────────────────────────────────────────────────────────┐
│ 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)

The workbench in dark mode showing a downtime Pareto chart: four dark bars make up 80% of downtime and six faded bars the rest, with a cumulative line and a labelled 80% reference

  • 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".

6.2 Dimensions

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).

6.3 Spacing

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.

6.4 Corners and elevation encode docked versus floating

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.

6.5 Alignment

  • 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.

6.6 Smaller screens

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).

7. Components: shadcn/ui

Component sheet in light and dark themes: buttons, engine status readouts, a column menu, a persistent error alert, form fields, a switch, a tooltip, a badge and a toast

7.1 Setup

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

7.2 Compose, don't create

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.

7.3 Component map

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)

7.4 Button variants

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.


8. Motion

8.1 Principles

  • 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 transform and opacity, 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.

8.2 Tokens

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"

8.3 The lamp

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.

The six lamp states: starting the engine, ready, running, done, needs attention, and error
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.

8.4 What moves and what doesn't

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

9. Interaction

9.1 Keyboard first

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.

9.2 Running a query

  1. Run (or Ctrl Enter) starts the query. The button becomes Stop (Ctrl .), and the lamp starts blinking.
  2. Running again while a query is still going replaces it. The old query is cancelled, never queued (P-06).
  3. The first rows appear as soon as they stream in. The row count and timer update in place with tabular figures, so nothing shifts.
  4. When the query finishes, the lamp flashes once (the blink) and the results bar shows "40 rows in 212 ms".
  5. Sorting and filtering in the grid run as SQL (Architecture rule 4), so they show the same lamp and timing as any other query.

9.3 Errors

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 Alert in 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.

9.4 Files

  • 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.

9.5 The results grid

  • 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.

9.6 How data is displayed

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

9.7 Feedback and safety

  • 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 --ring outline with a 2 px offset, never removed.

10. Writing

10.1 Voice

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.

10.2 Vocabulary

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

10.3 Patterns

  • 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".

10.4 Before and after

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."

11. Accessibility checklist

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

12. Marketing surfaces

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.

13. Open decisions

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).


14. Method and references

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 create styles, Base UI as the default (July 2026)

Clone this wiki locally