This is the reference for how blockr looks and behaves: tokens, the
block, its layout, controls, actions, the floating layer, labels and the
special blocks. It states each rule once, with exact values and token
names. The design system page
draws the whole system with the same table of contents, so every section
here has an anchor of the same name there. Token values come from
inst/assets/css/blockr-tokens.css and
blockr-tokens-dark.css in blockr.ui.
1. Principles
- Components read meaning tokens only. Palette tokens
(
--blockr-grey-500,--blockr-blue-600) are read by other tokens and never by a component. - No UI colour outside the palette, and no grey that is not a ramp step. Data colours, block category colours and the logo’s green are the three exceptions, each with its own source (see Foundations).
- Tints are computed from meaning tokens with
color-mix(), never from a palette token and never written as rgba literals, so they follow a theme and the dark scheme. The ones used in many places are tokens themselves (see Borders). - Three text levels: default, muted, disabled.
- Every configurable option is in the gear. A block with no options has no gear. Simplified mode removes the gear button and nothing else.
- Changes apply as they are made. There is no Apply button in a block’s settings. Text and numbers commit on Enter or blur.
- One control per kind of decision (see the table in Controls). Shape tells a reader what can be clicked: badges have round ends, things you act on have 4px corners.
- Breakpoints follow the panel, never the viewport. Every size step is a container query on the block.
- Floating things are menus (choices and actions, opened by a click) and tooltips (names, on hover). There are no popovers.
- The sentence on a block’s face looks as it will in print.
Drawn in the design system page.
2. Foundations
Token grammar
The second word of a token’s name says what kind it is.
| Kind | Pattern | Example | Read by |
|---|---|---|---|
| Palette | --blockr-<hue>-<step> |
--blockr-grey-200 |
other tokens only |
| Meaning | --blockr-color-<property>-<role>[-<state>] |
--blockr-color-text-muted |
components |
--blockr-font-<aspect>[-<step>] |
--blockr-font-size-sm,
--blockr-font-mono
|
||
--blockr-radius-<step>,
--blockr-control-h[-<step>],
--blockr-shadow-<step>
|
--blockr-radius-lg |
||
--blockr-focus-<aspect>,
--blockr-transition,
--blockr-mark-<aspect>
|
--blockr-focus-outline |
||
--blockr-z-<layer> |
--blockr-z-menu |
anything positioned against the page (Stacking) | |
--blockr-category-<category> |
--blockr-category-plot |
the block’s mark (Block category colours) | |
--blockr-logo |
--blockr-logo |
the blockr logo (Logo) | |
| Local | --blockr-<owner>-<thing> |
--blockr-dock-spinner-delay |
its owner’s stylesheet only |
A global token’s second word is one of color,
font, radius, control,
shadow, focus, transition,
mark, z, category,
logo, or a palette hue. Anything else is local. For colour,
<property> is text, bg or
border and comes first, because a colour chosen for text is
not automatically right as a fill.
Palette
| Hue | Steps | Light values |
|---|---|---|
| grey | 50 100 200 300 400 500 600 700 800 900 |
#f9fafb #f3f4f6 #e5e7eb
#d1d5db #9ca3af #6b7280
#4b5563 #374151 #1f2937
#111827
|
| blue | 50 100 500 600 700 |
#eff6ff #dbeafe #3b82f6
#2563eb #1d4ed8
|
| red | 50 100 600 700 |
#fef2f2 #fee2e2 #dc2626
#b91c1c
|
| amber | 50 100 500 600 700 |
#fffbeb #fef3c7 #f59e0b
#d97706 #b45309
|
| green | 50 100 600 700 |
#f0fdf4 #dcfce7 #16a34a
#15803d
|
| accent | 50 100 500 600 700 | points at blue |
The meaning tokens use grey 50, 100, 200, 300, 500 and 900, plus 400 for disabled.
Text
| Token | Light | Use |
|---|---|---|
--blockr-color-text-default |
grey-900 | body text, values, headings (section headers, table headers, chart titles), menu items, tooltip body, code, the hover state of a muted icon |
--blockr-color-text-muted |
grey-500 | labels, placeholders, meta, hints, counts, empty states, column
types, row numbers, NA, status lines and footers, icons at
rest |
--blockr-color-text-disabled |
grey-400 | disabled controls only |
--blockr-color-text-on-accent |
#ffffff |
text on a solid accent fill |
--blockr-color-text-accent |
accent-600 | accent text on a plain surface |
--blockr-color-text-accent-strong |
accent-700 | text on the accent tint (every selected or “on” state, the avatar initial), where accent-600 falls below 4.5:1 |
--blockr-color-text-danger / -warning /
-success
|
red-700 / amber-700 / green-700 | status text |
Muted is 4.8:1 on white, 4.6:1 on grey-50 and 4.4:1 on grey-100.
Muted text therefore never sits on a hover or selected background: on a
hovered row, meta text and icons turn default; on a selected row, they
turn text-accent-strong with the rest of the row. A status
fill takes muted text only where it keeps 4.5:1: bg-warning
(4.7:1 in light, 4.6:1 in dark on a block), which holds a required
field’s placeholder, and bg-success (4.6:1 in both), with
the dark fills at 10% for that. On bg-danger muted is 4.4:1
in light, so muted text does not go there. Grey-400 is 2.5:1 and serves
disabled controls only.
Surfaces
Surfaces are layers. Each sits on the one before, and anything placed on a layer takes that layer’s field and border tokens.
| Token | Light | Job |
|---|---|---|
--blockr-color-bg-page |
grey-50 | the board canvas |
--blockr-color-bg-surface |
#ffffff |
a panel, a card, a block |
--blockr-color-bg-raised |
#ffffff |
menus, dropdowns, tooltips |
--blockr-color-bg-subtle |
grey-50 | a quiet fill inside a surface; the gear tray |
--blockr-color-bg-field |
grey-50 | an input field |
--blockr-color-bg-hover |
color-mix(in srgb, var(--blockr-color-text-default) 6%, transparent) |
hover and the keyboard row |
--blockr-color-bg-selected |
bg-accent-subtle |
the chosen thing: a selected row, the front tab of the active dock
group; its text is text-accent-strong, and it has no
edge |
--blockr-color-bg-selected-inactive |
color-mix(in srgb, var(--blockr-color-text-default) 10%, transparent) |
the front tab of every other dock group: selected, but not where you work; one step above hover |
--blockr-color-bg-accent |
accent-600 | solid accent fill (checked checkbox) |
--blockr-color-bg-accent-hover |
accent-700 | its hover |
--blockr-color-bg-accent-subtle |
color-mix(in srgb, var(--blockr-color-border-accent) 7%, transparent) |
the accent tint (below) |
--blockr-color-bg-accent-subtle-hover |
the same at 13% | its hover |
--blockr-color-bg-danger / -warning /
-success
|
red-50 / amber-50 / green-50 | status fills |
bg-hover is a wash of the text colour, so a hovered row
is one step away from whatever it sits on, surface or raised, in both
schemes. In light it lands on grey-100.
Borders
| Token | Light | Use |
|---|---|---|
--blockr-color-border-default |
grey-200 | every border, input borders included |
--blockr-color-border-strong |
grey-300 | hovered borders, stronger lines, a border drawn with a text colour |
--blockr-color-border-accent |
accent-600 | focus, full-strength accent edge |
--blockr-color-border-danger / -warning /
-success
|
red-600 / amber-600 / green-600 | status edges |
--blockr-color-border-accent-subtle |
color-mix(in srgb, var(--blockr-color-border-accent) 35%, transparent) |
the accent tint’s edge |
--blockr-color-border-danger-subtle /
-success-subtle
|
the status border at 35% | the edge of a status badge, a message, the destructive button |
--blockr-color-border-warning-subtle |
the warning border at 45% | the same; amber is paler, so its edge is stronger |
Input borders stay soft (border-default). The focus
state marks the active field.
The accent tint is text-accent-strong
on bg-accent-subtle with a
border-accent-subtle edge; its hover is
bg-accent-subtle-hover with border-accent. It
is the main button’s look, and every selected or “on” state borrows it:
a selected segment, an icon tile, a pressed icon button, the open gear,
a tag that is cutting rows, an “on” badge, the “+N” overflow, the ↵
button, a selected row and the active group’s front tab (these two
without the edge). Hovering a selected thing keeps its look.
Status
Every status role has a text, a background and a border token:
danger, warning, success. error is danger;
there is one red. Warning’s border is amber-600, #d97706: a
status border, and a status dot drawn in one, marks a state, so it
clears 3:1 on white (WCAG 1.4.11), where amber-500 is 2.2:1. Amber-700,
the warning text, would clear it too, but it is as dark as the red, and
an amber dot and a red one would then differ by hue alone. Blue is never
a status colour, because blue means “on”. Negative numbers in a table
are data, and their colour belongs to blockr.theme.
Focus
Two styles:
| Element | Focus | Shown on |
|---|---|---|
| a field: text, number, select, code editor, a name being renamed |
border-accent and --blockr-focus-ring
(0 0 0 3px rgba(37, 99, 235, 0.12)) |
:focus, mouse included, because it also means “you are
typing here” |
| everything else: buttons, tools, the gear, segments, checkboxes, tabs, pills, legend items |
outline: var(--blockr-focus-outline) (2px solid
border-accent),
outline-offset: var(--blockr-focus-offset) (2px) |
:focus-visible only, so a mouse click shows
nothing |
The ring alone is 1.2:1 on white and does not show on a frameless
tool; the outline is 5.2:1. The 2px offset keeps it off the gear’s frame
and a selected segment’s tint. The keyboard row in a menu is not focus
(focus stays in the filter box or on the list) and keeps the hover look.
The ring is written as a literal, so a theme that changes the accent
restates it. The 0.45 ring in blockr.viz and blockr.ggplot
(--blockr-color-focus) goes.
Dark scheme
The dark scheme is blockr-tokens-dark.css, keyed off
data-bs-theme="dark" (the attribute bslib writes and
blockr.core’s dark-mode option toggles). It restates the grey and blue
ramps positionally (grey-50 is still the quietest surface, grey-900 the
loudest text) and restates the accent and status meaning tokens
directly.
| Token | Dark |
|---|---|
bg-page / bg-surface /
bg-raised
|
#0e1219 / #161b23 /
#1e242e
|
bg-hover |
the same 6% text wash, lighter than what it sits on |
bg-selected, bg-selected-inactive
|
follow the accent tint and the text colour; not restated |
text-on-accent |
#0e1219 (the accent fill is lighter in dark) |
text-accent |
accent-500 |
text-accent-strong |
text-accent |
text- / border-danger,
-warning, -success
|
#f87171, #fbbf24,
#4ade80
|
bg-danger, -warning,
-success
|
the same hues at 10%, which keeps a required field’s
text-muted placeholder at 4.5:1 on a block |
focus-ring |
0 0 0 3px rgb(96 165 250 / 0.28) |
| shadows | the same geometry at 0.5 to 0.7 black |
The border steps (grey 100 to 300) are compressed harder than a
mirror, because a light line on a dark field reads stronger than a grey
line on white. In dark the gear tray takes bg-page. Exports
(PNG, pptx) always take the light values.
Theming
A theme overrides the palette, most usefully the accent ramp
(--blockr-accent-50, -100, -500,
-600, -700); every accent-derived token
follows, and blue-600 keeps meaning blue. A theme is
typically those five values, optionally the ten greys (for warm or cool
neutrals), and perhaps a radius and a font face. A theme that changes
the accent also restates --blockr-focus-ring. Colours
painted in JS or R (charts, plots) come from blockr.theme’s palettes,
not from these tokens.
Type
The body face is Open Sans, as bslib’s Shiny preset ships it, set in
one place: --bs-body-font-family. A theme that ships a face
sets --bs-body-font-family. There is no
--blockr-font-sans. The chart canvas takes its face from
the body.
Text is drawn with grayscale antialiasing
(-webkit-font-smoothing: antialiased,
-moz-osx-font-smoothing: grayscale), set once on
html in the theme layer. macOS otherwise uses subpixel
antialiasing, which draws Open Sans heavier and softer than the design;
Linux and Windows are unaffected.
| Token | Size | Use |
|---|---|---|
--blockr-font-size-xs |
12px (0.75rem) | field labels, section titles, captions, status lines, tooltips, offers |
--blockr-font-size-sm |
13px (0.8125rem) | output subtitle and sentence, code, menu meta text, tags |
--blockr-font-size-base |
14px (0.875rem) | body, field values, menu rows, buttons at 42px |
--blockr-font-size-lg |
16px (1rem) | the block title, output title, the one heading on an output block |
--blockr-font-size-xl |
20px (1.25rem) | page titles outside the board |
There is no md step. Two sizes sit off the scale on
purpose: badges, counts and the block name in the compact dock header at
11px, and text on a chart canvas at --blockr-mark-font-size
(11px).
| Weight token | Value | Use |
|---|---|---|
--blockr-font-weight-normal |
400 | body, labels, values |
--blockr-font-weight-medium |
500 | buttons, badges, pills, slot words, a selected segment, values in a chart tooltip |
--blockr-font-weight-semibold |
600 | block title, output title, section titles, counts, the current pick in a menu |
--blockr-font-mono is
'SF Mono', 'Fira Code', 'Consolas', 'Monaco', monospace.
Code is 13px on every code surface.
Shape and size
| Token | Value | Use |
|---|---|---|
--blockr-radius-sm |
4px | 26px buttons and tools, the gear, tags, pills, checkbox, menu rows, offers |
--blockr-radius-md |
6px | 30px buttons, segments inside a segmented control, messages |
--blockr-radius-lg |
8px | fields, 42px buttons, rows, menus, the gear tray, tooltips (the light card and the chart’s) |
--blockr-radius-xl |
12px | block panels |
--blockr-radius-pill |
999px | badges, counts |
| Token | Value | Use |
|---|---|---|
--blockr-control-h |
42px | inputs, selects, segmented controls in the grid, rows, buttons beside inputs |
--blockr-control-h-sm |
30px | buttons in toolbars and dialogs, the crossfilter card search |
--blockr-control-h-xs |
26px | header tools, the gear, buttons in a header strip, the builder/code switch, segmented controls inside a row |
Nothing you click is smaller than 24px (WCAG 2.2, 2.5.8); a control that stands on its own is at least 26px, and one inside a row, field or tag may be 24px. Below 24px only where the target is a word in running text (a slot, in a sentence or an empty state), whose height is the line’s, or where the same action has a full-size target too: the 16px checkbox box (its label), a tag’s × (the list unticks it) and the outline’s input markers (“Connect to…” in the row’s menu).
Elevation
| Token | Value |
|---|---|
--blockr-shadow-sm |
0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) |
--blockr-shadow-md |
0 4px 12px rgba(0, 0, 0, 0.1) |
--blockr-shadow-lg |
0 25px 50px -12px rgb(0 0 0 / 0.25) |
Menus, dropdowns, tooltips and the chart tooltip use
shadow-md on bg-raised. The gear tray is in
flow and has no shadow. In dark, shadows barely read; the lighter
bg-raised separates a menu from the surface.
Motion
--blockr-transition: 0.15s ease for hover and state
changes. The gear tray slides open and closed over 0.22s, so the output
is seen moving down and up.
Data marks
| Token | Value | Rule |
|---|---|---|
--blockr-mark-radius |
2px | on the silhouette of a bar or box, on the value end only; ends on an axis or against a sibling stay square; never near half the mark’s thickness |
--blockr-mark-font-size |
11px | all text drawn on a chart canvas: axis labels and names, data labels, legend keys |
--blockr-mark-weight-supporting |
0.6 | opacity of a mark that supports a choice (crossfilter picker bars) |
Data colours (chart palettes, map markers) come from blockr.theme, never from UI tokens.
Block category colours
Okabe-Ito, one token per category,
--blockr-category-<category>, read by the block’s
mark. A fixed set: identical under every theme and in dark mode. A
category outside the set takes uncategorized, blockr.core’s
default. Where the mark is an image, which cannot read the tokens,
category_color() gives the same values.
| Category | Colour |
|---|---|
| input | #0072b2 |
| transform | #009e73 |
| structured | #56b4e9 |
| plot | #e69f00 |
| table | #cc79a7 |
| model | #f0e442 |
| output | #d55e00 |
| utility | #cccccc |
| uncategorized | #999999 |
Logo
One token, --blockr-logo, #089e76, read by
the logo at the left of the navbar (The navbar).
Like the block category colours, it is outside the palette and identical
under every theme and in dark mode. It clears 3:1 on the surface, as an
icon must: 3.4:1 in light and 5.1:1 in dark.
Drawn in the design system page.
3. The block
Anatomy, top to bottom
-
Dock header. One row: the block’s mark (a 32px
tinted square in its category colour, carrying the status dot), the
block title at 16px (
font-size-lg) weight 600text-default, wrapping to a second line and cut after it, and on the right the preview’s eye, a block’s own control (such as the AI assistant) and the “…” menu. No subtitle: the block type and package are the mark’s tooltip (“filter block · blockr.dplyr”). Compact (a board option, off by default) turns the row into an eyebrow over the block’s content: the mark at 20px (glyph 12px), the name at 11px, weight 600, uppercase, 0.06em tracking,text-muted, on one line with an ellipsis, 6px above the content. - Header row. Output blocks put the output title and the sentence on the left and the tools on the right, gear last. Transform blocks have the tools only.
- Gear tray. Opens in flow directly under the header row.
- Controls on the face. Transform blocks only: fields in the grid, or rows for lists you add to and reorder.
- The output. Body, caption, status line.
There is no 16px section heading inside a block.
The block’s mark and bare mode
- The mark is a tinted square (the category colour at about 18% on the surface) with the glyph in the category colour. It comes in four sizes: 32px in the dock header, 24px in list rows (menus, pickers, block lists) with the package as a badge, 20px in the compact header, 16px in a tab. The glyph is half the mark plus 2px (18, 14, 12, 10) and the corner radius a quarter of it (8, 6, 5, 4).
- One element draws it everywhere, blockr.ui’s
.blockr-block-mark: 24px, or another size through.blockr-block-mark--32,--20and--16. It is built byblock_mark()in R and by a row’smarkinBlockr.menu. Where the mark has to be an image, as on the DAG’s canvas,block_mark_svg()draws the same mark as an SVG. - The header’s mark carries the block type and package as its tooltip, one of the few on something that is not a button or a cut-off label (see Tooltips).
- Bare mode (a board option, for dashboards): the card has no header; the tab carries the mark and the name, the actions sit right of the tabs, and a double-click on the tab renames. It needs dockViewR’s tab components; there is no CSS-only interim.
Header row
- Tools are 26px, right-aligned, gear last.
- The output title shares the row with the tools. It takes the width the tools leave and wraps down.
- The output title is 16px (
font-size-lg), weight 600,text-default. The subtitle below it is 13pxtext-muted. Title and subtitle belong to the output: they export with the chart or table.
What stays on the face
- A block’s face holds what most uses need; the gear holds the rest.
- An output block’s face holds the output and its sentence. A slot in the sentence is the only way to put one of its arguments on the face. Its gear keeps a row for every argument, slotted or not.
- A block whose job is to build a chart (blockr.ggplot’s) keeps its chart type and its mapping on the face, above the output, because almost every use sets them. Its title stays inside the plot image.
- Reader controls (for example the patient profile’s view toggles) sit on the face, not in the gear.
- Simplified and locked mode hide the gear button and nothing else.
Dock does this with one CSS rule on
.blockr-gear-btn; blocks never ask which mode they are in.
The gear tray
- In flow under the header row, above the face’s controls. It pushes the content down and covers nothing. It slides open over 0.22s.
- Look:
bg-subtlefill, no border, radius 8, padding 12px 14px 16px, 16px below it. A 10px beak, 8px from the right edge, points at the gear. Fields inside turnbg-surface(white), so the tray reads as a separate layer. In dark the tray and its beak takebg-page. - No heading. The tray’s accessible name is an
aria-label. Sections are separated by a section title and 24px of space, with no rules. A tray with one section has no title at all. - A section switched on by a checkbox keeps the checkbox before its title; its fields appear below when checked.
- Fields use the grid (Layout).
- It closes by the gear and by Escape only. A click on the output, on another block or on a menu inside the tray leaves it open.
- The open state lasts for the session (a re-render does not close it) and is not saved with the board. Several trays may be open at once.
- Changes apply as they are made. Text fields commit on Enter or blur. No tray has an Apply button for its fields; code is the one exception and commits on its own button (Run in the code and function blocks, Apply on the chart’s prepare script).
- The crossfilter’s settings and the patient profile’s gear panel use the same tray.
- In a block rendered in R,
gear_tray()andtray_section()draw it. Its fields are Shiny’s own inputs, withupdateOn = "blur"ontextInput()andnumericInput().
The output
- Body: the chart, table or text.
-
Caption: 12px
text-muted. Chart, table and composer captions stay italic. -
Status line: 12px
text-muted: row counts, filter state, timing, the drill receipt. See Labels and status.
The sentence and its slots
The sentence is the output’s subtitle, written by the block’s author
in the #| subtitle: template. Nothing is generated; a
setting the template does not name stays in the gear.
-
Line breaks: one sentence. The author breaks lines
by writing
\nin the template; blockr adds no breaks of its own. -
A slot is a live word: accent text
(
text-accent), weight 500, a 1px dashed underline 3px below the text, solid on hover. A click opens the list under the word (Blockr.Select.menu()). A flag word toggles in place. -
Many values: a slot shows the first three values,
then “+N more”, inside the same live word; the click opens the full
list. Exports print the same text. The cut is 3
(
TITLE_MAX_VALUES). -
Offers (“+ name”, for a setting that is unset) sit
on their own line under the sentence, three at most. Past three, the
last one reads “More settings” and opens the gear. An offer is a small
dashed button: 12px
text-muted, 1px dashedborder-strong, radius 4, 6px side padding; on hover the border and text turn accent. It disappears once its setting is set. Offers are not part of the printed sentence. - A clause in
[ ... ]leaves with its value. The sentence never prints “(none)”. - One template, two renderers: the face draws live words, exports print the same text.
Drawn in the design system page.
4. Layout
The grid
One grid for fields, on the block’s face and in the gear tray alike. The columns are about 130px wide; the panel’s width decides how many there are.
| Panel width | Columns |
|---|---|
| under 290px | 1 |
| 290px and up | 2 |
| 560px and up | 4 |
| 840px and up | 6 |
| 1110px and up | 8 |
Gaps: 12px between columns, 16px between rows of fields. The grid steps on a container query on the block. It moves from blockr.extra to blockr.ui.
Field sizes
A field says how many columns it needs.
| Size | Columns | Fields |
|---|---|---|
| small | 1 | number, checkbox (two when its label needs them) |
| large | 2 | select, text field, segmented control |
| full | the whole row | tags (multi-select), expression |
Columns line up across sections at every width. A section with few fields leaves room on the right; fields do not stretch to fill it. At two columns a large field fills the row.
Spacing
| Between | Space |
|---|---|
| label and its control | 4px |
| columns / rows of fields | 12px / 16px |
| a section title and its first field | 10px |
| the end of one section and the next section title | 24px |
| rows in a list | 6px |
| the gear tray and what follows | 16px |
Section titles
“A group of fields starts here”, on the face and in the gear tray:
12px (font-size-xs), weight 600, uppercase, 0.05em
tracking, text-muted. It works as a divider and must not
pull the eye from the fields. One class replaces the settings band
title, .dd-section-title, .dd-at-head,
.dd-type-group-head, the crossfilter table and search
headers and the select menu group titles.
Field labels
12px, weight 400, text-muted, 4px above the control, in
every package. The required-empty cue adds a trailing * to
the label.
Rows
A row in an add-and-reorder list (filter conditions, columns) is
42px, the same as an input, border included
(box-sizing: border-box), as .blockr-row is.
Look: bg-field, 1px border-default, radius 8,
padding 5px 10px, 10px gap between its parts, 6px between rows. Its
operator pill is 24px.
- A row’s content starts on the same edge as the label above it.
- The drag handle takes no space at rest. It appears in the container’s left padding, which is at least 14px.
- Handles and remove buttons appear on hover or keyboard focus of their row (see Actions).
- “Add condition” under the list is a quiet button.
The handle belongs to these bordered rows, which are full of controls, so a drag has to start somewhere that is not one of them. A list of plain rows with a hover fill (a menu-style list, such as the patient profile’s panels) has no handle: the whole row drags, with the grab cursor on hover, and Alt+Up/Down moves the keyboard row. A handle there would hang outside the fill.
Block lists
A list of blocks on a panel: the outline, the report, the deck. Every one uses the same row, and it is the row of the “+” menu the block was picked from (Picking a block).
-
Row: 34px, radius 6, 8px between its parts, no
space between rows. From the left: the order number where there is one,
the block’s 24px mark, the name at 14px
text-defaulton one line, then the row’s end. A cut name is named in full by the tooltip. A text item (a paragraph in the report) wraps to two lines. -
Order numbers only where the order is the product:
the report and the deck. 12px
text-muted, tabular figures, right-aligned in a 14px column. The outline’s order comes from the links and has none. -
The row’s end at rest holds state the reader needs
without opening anything. In the report, code and output are two 24px
pressed icon buttons, always shown: pressed (the accent tint) means
shown in the document, and a click toggles it. In the outline, the views
a block is on, as 13px
text-mutedmeta text: the first view, then “+N” intext-accent. Views show only on a board with two or more views and a panel of 360px or more. -
Hover and keyboard focus:
bg-hover, and a 24px “…” tool appears last. It opens the row’s action menu (action_menu()) with the actions that are not on the row, and Remove last after a divider. There is no × on the row. Meta text gives way to the “…”; a tool never covers the name. -
The current row: the block open in the dock is
bg-selectedwithtext-accent-strong, in every list. -
Gestures: a click opens the block. A double-click
renames it (Renaming in place). The whole row
drags, with the grab cursor and a 2px
border-accentline where it will land. Alt+Up/Down moves the keyboard row. -
Stacks (the outline): a tinted band with no border,
the stack’s colour at 7% on the surface, radius 8, 2px padding. Its
header is a row: the stack’s 24px mark in the stack colour, the name at
weight 600, the member count in 13px
text-muted, the fold chevron at the end. The pointer on the header deepens the whole band to 14%, because a click, a drag or a rename on the header acts on the stack. The stack colour is the board’s, like a block category colour. -
The end of the list: a quiet button named for what
lands in it, “Add block” or “Add slide”, 30px, aligned with the marks.
It opens the Picking a block menu. An empty list
is one
.blockr-emptyline.
Links in the outline
The outline draws the board’s links on its rail. A link is a thing you point at, as a row is: it lights up under the pointer and a click opens its menu. Adding a link starts from a block.
-
A link under the pointer thickens in
border-accent, and the tooltip names it: “Two species → Sepal ratio”, then “into data” intext-muted. Nothing is drawn on the link. - A click on a link opens its action menu. The head names the link (from → to, and the input). Then: Insert a block here; Rename input, where the target takes any number of inputs (the name turns into a field in place, Enter commits, a name already taken is refused in place); or Move to input, where the target has another free named input; then Remove link after a divider.
- Adding a link: drag a row’s dot onto another row; the row it would land on takes the accent tint, and a target with several free inputs asks which in a menu. Or click a free-input marker and pick the block that feeds it. Or “Connect to…” in the row’s “…” menu. A dot dragged into empty space adds a new block below, linked, through the Picking a block menu.
-
Input markers sit after the name: an 8px open
circle (1.5px
border-strong) for each named input with nothing linked into it, and “∞” (13pxtext-muted) for a block that takes any number of inputs. Each is a 16px target with a tooltip (“y: not connected”, “Takes any number of inputs”). A click opens “Connect a block to y”, listing the board’s blocks that can feed it. A linked input draws nothing: the rail shows it. - A click on a row’s dot lists the block’s connections, inputs then outputs, with the input as meta text. A click on one opens that link’s menu.
- The row’s “…” menu carries Insert a block before (into the block’s input link; with several, a list of them), Append a block and Connect to…, so every link gesture has a keyboard path.
- Removing a block with exactly one input bridges it: its parent takes each of its output links, into the same inputs. A block with two or more inputs is not bridged, and its Remove row says so as meta text (“its links are dropped”). The rule lives in blockr.dock’s remove action, so the outline, the DAG and the dock header behave alike.
Drawn in the design system page.
5. Controls
Which control for which decision
| Decision | Control |
|---|---|
| Turn an option on or off | Checkbox |
| One of two or three fixed, short values | Segmented control |
| One of four or more, or of anything from the data (columns, measures, levels) at any count | Select |
| The same, inside a row | Pill that opens a menu |
| Several from a list, from the data or from a fixed set | Select, multi, with tags |
| Kind of output (chart type) | Icon tiles, with group headings where types are grouped |
| How a value is edited (builder or code) | Pressed icon button |
| A number or a name | Field that commits on Enter or blur |
| A colour | Colour field |
Not used: switches, radio buttons, click-to-cycle pills, toggle-pill groups, rows of checkboxes for a fixed set, shinyWidgets and unstyled Bootstrap inputs. The selected state of every choice control is the accent tint: a selected segment, an icon tile, a pressed icon button, the open gear, a selected row, the active group’s front tab.
Text and number fields
- 42px,
bg-field, 1pxborder-default, radius 8 (radius-lg), 12px side padding, 14pxtext-default. Placeholdertext-muted. - Focus:
border-accentand--blockr-focus-ring, on any focus. - Disabled:
text-disabled. - Required and empty:
border-warning(amber-600,#d97706),bg-warningfill, and a*after the label. The cue clears when the field has a value. - Commit on Enter or blur. The ↵ button (24px square, the accent tint) is armed while the field is dirty. Escape reverts. See Keyboard shortcuts.
- Numbers use the same field. The input keeps
type=numberfor the keyboard, hides the spinner arrows, and commits like text; arrow keys still step. A Bootstrap override brings Shiny’snumericInputto the same look. - dplyr’s 30px inputs move to the 42px field when they stand on their own. Inside a row, a number or text input has no frame of its own: the 42px row is its field, as it is for a Select in a row.
Select
Blockr.Select is the only dropdown, single and
multi.
- In the grid it is a bordered 42px field with the chevron (see Chevrons). Inside a row it is bare.
- Options show the value first and the label after it as 13px muted meta text (for columns, see Column names and their labels); the label is cut first when space runs out.
- Its dropdown is a menu (Floating layer).
- A screen reader names it by its field’s label, which the caller
passes as
label; a menu uses its title.
Select, multi, with tags
- The picks sit as tags in the control. The dropdown lists every option and ticks the picks; a click on a ticked row unticks it. Tags can be dragged to reorder.
- A pick keeps the dropdown open and keeps the filter text, so one search serves several picks (“blood”, then each blood test). The filter clears when the dropdown closes.
- A tag’s × appears on the tag under the pointer or with keyboard
focus, never on every tag at once, and turns
text-dangeron its own hover. Its space stays reserved, so nothing moves. - The “+N” overflow stands in for hidden tags: tag geometry in the accent tint; a click expands the field.
- The same control serves data-driven lists and fixed sets (such as the summary table’s statistics). Fixed options stay hidden until opened.
Checkbox
On/off is a bare checkbox everywhere: in the gear tray, on a block’s
face and in the board options. Core’s bslib::input_switch
calls, Shiny’s checkboxInputs, the pharma header switch and
the on/off pills all become Blockr.checkbox. On the R side
the theme layer does it: theme_dep() draws Shiny’s
checkboxInput() and checkboxGroupInput() and
bslib’s input_switch() as this checkbox, so R code keeps
calling them.
- Box: 16px, radius 4, 1px
border-strong,bg-surface. Hover: bordertext-muted. - Checked:
bg-accentfill and border, the check intext-on-accent. - Label after the box, 9px gap, 14px
text-default. The label names the “on” state. - A native input underneath; Space toggles; keyboard focus puts the focus outline around the box.
- Disabled: box at 50% opacity, label
text-disabled. - In the grid: the box and its words, with no field shell and no empty label row. It sits on the bottom line of its grid row (the controls’ line) and takes one column, or two when its label does not fit in one. On a row of its own it takes only its own height.
- Checkboxes that together encode one choice (core merge’s
all.x/all.y) become that choice as a Select (“Join type”).
If a boolean ever has to sit inside a field shell again, the switch question reopens.
Segmented control
For a fixed set of two or three short values. Both values stay in view and the pick is readable without a click.
- In the grid: 42px,
bg-field, 1pxborder-default, radius 8, 3px inner padding and gap. Segments share the width, radius 6, 14pxtext-muted; hovertext-defaultonbg-hover. - Selected segment: the accent tint, weight 500.
- It takes two columns, like a select. Three segments need about 240px.
- Inside a row: 26px, 2px padding and gap, radius 6, segments 12px at radius 4.
- Four or more values, labels too long for the cell, or values from the data go to Select.
- It replaces the cycle pills of this kind, viz’s Order select, the head block’s “Tail” switch (“From: First / Last”), stats’ shinyWidgets buttons and the dm, io and pharma segmented copies.
A pill that opens a menu
For four or more values inside a row: the filter operator, the join key operator.
- 24px inside a 42px row, radius 4, 1px
border-default,bg-hover(the tags’ fill, a step darker than the field), 12px weight 500text-defaultmixed 70% withbg-field(text-mutedis under 4.5:1 onbg-hover), with the 10px chevron so it reads as a choice. Hover:border-strong,text-default. - A click opens
Blockr.Select.menu()anchored on the pill, listing every value. - The click-to-cycle pill is retired. Join type, outside a row, is a Select field in the grid.
Builder or code switch
A pressed icon button, 26px, the tool look (like the gear). Pressed:
the accent tint. It sets aria-pressed. Its tooltip names
the other state (“Edit as formula text”, “Back to the builder”). It
stays out of the field grid.
Renaming in place
One pattern for every name that can be edited where it is shown: the block title, an outline row, a view name, a tab (in bare mode).
- Start: a double-click on the name, or “Rename” in the item’s “…” or row menu. A single click keeps its other job (select, open).
- At rest the name is plain text; on hover it takes the
bg-hoverwash (radius 6, 4px 8px padding, pulled back by the padding so the text does not move) and the text cursor. There is no pencil. - Its tooltip names the gesture: “Double-click to edit”. While the name is cut off, the tooltip leads with the whole name and the gesture follows, muted. Where the gesture is a single click (a name in the views menu’s manage mode), the tooltip says so (“Click to rename”).
- Markup:
data-blockr-editableon the element that shows the name, with the gesture as its value where it is not the double-click. The script blockr-ui.js gives it the tooltip and blockr-tooltip.css the cursor, both loaded wherever blockr-ui.js is; nothing else is needed. - While editing, the text becomes a field at its own size and weight:
1px
border-accent, the focus ring,bg-surface, the text selected. No label. - Enter commits, Escape restores, a click elsewhere commits. An empty
or duplicate name is refused in place: the field’s
border-dangeredge and a one-line 12pxtext-dangermessage under it.
Colour field
For a single colour, such as a plot background.
- The text field’s shell (42px,
bg-field,border-default, radius 8) with an 18px swatch (radius 4, 1pxborder-default) 12px from the left, then the value as hex in capitals, 14pxtext-default. - A click opens the browser’s colour picker; the pick commits when the picker closes. Typing a hex value commits on Enter or blur like any text field.
- The swatch shows the value in both schemes; it is data, not a UI colour.
Search field
A field that narrows a list on a block’s face: a crossfilter card, the patient profile’s sidebar.
- 30px (
control-h-sm), radius 6 (radius-md),bg-field, 1pxborder-default, 13pxtext-default. Placeholder “Search…” intext-muted. - A 14px magnifier in
text-muted, 9px from the left; the text starts at 30px. - While there is text, a 24px clear button at the right: a thin ×,
text-muted, radius 4, the hover wash. Escape clears too. - Focus:
bg-surface,border-accentand the focus ring. - It filters as you type. A list shows it only above 8 entries, as a menu shows its filter box.
- Menus keep Select’s filter box, which has no magnifier: the list under it already says what it filters.
Drawn in the design system page.
6. Actions
Buttons
Four kinds. All are weight 500 and show the focus outline on
:focus-visible.
| Kind | Rest | Hover | Use |
|---|---|---|---|
| Main | the accent tint: text-accent-strong,
bg-accent-subtle, border-accent-subtle
|
bg-accent-subtle-hover, border-accent
|
at most one per view |
| Secondary |
bg-surface, border-default,
text-default
|
bg-hover, border-strong
|
everything else with a frame |
| Quiet | text only, text-muted, 8px side padding |
bg-hover, text-default
|
low-weight actions; replaces the grey-400 text links |
| Destructive |
text-danger, border-danger at 6% fill,
border-danger-subtle
|
border-danger at 12% fill,
border-danger
|
only to confirm a removal |
| Disabled |
text-disabled, transparent,
border-default
|
none |
The classes are .blockr-btn with a kind,
.blockr-btn--main, --secondary,
--quiet or --danger, and a size,
.blockr-btn--m, --s or --xs
(blockr-buttons.css); a bare .blockr-btn is a
secondary m.
There is no solid accent button. blockr-theme.css maps
Bootstrap’s button classes onto the kinds: btn-primary is
main; btn-default, btn-secondary,
btn-outline-secondary and btn-light are
secondary; btn-link is quiet; btn-danger is
destructive; .btn takes size m and btn-sm size
s. Shiny’s actionButton() and downloadButton()
follow this section through it. The mapping also restyles the buttons of
Shiny’s own modals, which is intended. btn-success has no
kind and goes.
| Size | Height | Text | Side padding | Radius | Where |
|---|---|---|---|---|---|
| m | 42px | 14px | 16px | 8px | beside inputs |
| s | 30px | 13px | 11px | 6px | toolbars, dialogs |
| xs | 26px | 12px | 9px | 4px | a header strip, next to the gear |
The crossfilter reset, the patient-profile drill reset and the ↵ button of a text field are instances of the main style. The two resets take these sizes; the ↵ button keeps the field’s 24px (see Text and number fields), since it sits inside a 42px field.
Icon buttons (tools)
Header tools (search, download, the builder/code switch) are 26px,
radius 4, bare, icon text-muted. Hover:
bg-hover, icon text-default. Every icon-only
button has a tooltip. Tools do not shrink with the panel.
The dock header has the “…” menu, and next to it a block’s own
control where the block has one (the AI assistant’s sparkle, at every
width, until a global assistant replaces it). They look like the tools
above: the title’s size and weight make the header read as the block’s
name, and a quieter icon would fall under the 3:1 an active control
needs (WCAG 1.4.11), as text-disabled (2.5:1) and
border-strong (1.5:1) do on white. The preview’s eye stays
in the header too, left of the block’s own control: while building, the
preview is what gets switched most. Controls is the menu’s first row,
with its familiar icon and a check when on, then a rule, then the
block’s actions. The header has one size at every card width, or the
compact one when the board asks for it. The accent tint’s fill and frame
are for the builder/code switch, which changes how a value is edited
rather than what is shown.
The gear button
- 26px, radius 4, 1px
border-default, the one framed square in the header row. Icon: Bootstrapgear-fillat 14px,text-muted. - Hover:
text-default,border-strong,bg-hover. - While the tray is open: the accent tint.
- One class,
.blockr-gear-btn, on every gear and on nothing else. Tooltip “Settings”. - No gear when a block has no options, including per mode.
Small icons
Drag handles, remove buttons and fold arrows are drawn with thin
strokes and small dots in text-muted. Handles and remove
buttons are hidden at rest and appear on hover or keyboard focus of
their row; remove turns text-danger on hover. Fold arrows
stay visible. The crossfilter’s filter cards are the exception (see Special blocks).
The icons are blockr.ui’s, one SVG file each, read two ways:
Blockr.icons in JavaScript and small_icon() in
R. A package draws them from there rather than keeping a copy of the
SVG, and rather than from Font Awesome through
shiny::icon(), which loads two stylesheets of about 2,400
rules (26 KB compressed) and a 156 KB font for one glyph.
Chevrons
One chevron does every disclosure, dropdown and “go to” arrow:
Blockr.icons.chevron in blockr.ui,
small_icon("chevron") in R. It is a 12px box with the path
M3 4.5l3 3 3-3, half the box wide and a quarter high with a
right angle at the tip, stroked at 1.4px with round caps and joins and
no fill.
- Size: 12px; 10px inside a 24px pill or a bare select in a row. The stroke stays 1.4px at both sizes.
- Colour:
text-mutedat rest;text-defaulton hover of the chevron or of the row or control it belongs to;text-disabledwhen disabled. - A fold (something opens in place: a table section, an outline chapter, a settings section) points down when open and right when closed, rotating −90°.
- A menu or select (something floats: Select, a pill, the navbar’s view menu, a split button) points down and flips up (180°) while its menu is open.
- A “go to” arrow (an options page, back, the next page of rows) points right or left and does not rotate.
- Every rotation uses
--blockr-transition. - No other glyph does this job: no Bootstrap Icons chevrons, no Bootstrap’s own select and accordion arrows, no border triangles, no ▾ ▸ ‹ › characters.
Keyboard shortcuts
Hint only where a key does something a user would not guess. Enter committing a field, ⌘↵ running code, ⌘S saving. Keys every web page has (Tab, the arrows, Escape closing a menu) get no hint.
-
What each key means, everywhere:
Key Means ↵ commit this one thing: a field’s value, a pick in a menu, one value added to a list ⌘↵ (Ctrl+↵) run or send, where ↵ makes a new line: Run in the code editor Esc cancel: drop what is not committed, close what floats. Never saves. Tab, a click elsewhere leave; a field commits when you leave it. The code field’s open completion list is the exception: Tab takes the highlighted completion, as an R editor does ⇧↵ nothing of blockr’s own Notation. Keys are glyphs: ↵ Enter, ⇧ Shift, ⌘ Command, ⌥ Option, and “Esc”. On a Mac they run together with no “+” (⌘⇧S, ⌘↵). Elsewhere modifiers are words joined by “+” (Ctrl+Shift+S, Ctrl+↵). Enter is ↵ on both.
Only the form that applies, never “⌘/Ctrl”. blockr.ui decides once per page:
.blockr-macon the root,Blockr.isMac,Blockr.keys("Mod+S")in JavaScript andshortcut("Mod+S")in R, whereModis Command on a Mac and Ctrl elsewhere. No package checks the platform itself.Where the hint goes. After the action’s name, as muted meta text: the meta slot of a menu row (“Save ⌘S”), after a button’s label (“Run ⌘↵”). It never replaces the name, and it is not repeated in a tooltip. The one exception is a text field’s ↵ button, where pressing the key is the whole action: it shows ↵ alone and names itself “Apply (Enter)” to screen readers.
No sentences. Where several keys need explaining, pair each key with its verb: “↵ add · Esc cancel”.
The navbar
The board’s toolbar: 48px high, bg-surface, a
border-default rule under it, every control 30px, tokens
only.
- Left: the logo, then the workflow’s name, then its save state as
12px
text-mutedtext (“Not saved”, “Saved just now”), then one quiet “…” workflow menu (save as, copy link, history, new). No split buttons. - The workflow’s name is its ID: chosen in a dialog at the first save, unique, like a file name (spaces allowed). It is shown in the body face, weight 600, never monospace. There is no separate title.
- Right: the view menu as a quiet button (current view marked by
weight 600, no fill), the board options gear, and the account avatar
last: a 28px circle with the count’s accent fill
(
border-accentat 14%) and atext-accent-stronginitial. - Hover on every navbar control is
bg-hover. The spinner’s label is the light tooltip.
Drawn in the design system page.
7. Floating layer
blockr has no popovers. Floating things are menus (choices and actions, opened by a click) and tooltips (names, on hover). The gear tray is in flow and not part of this layer.
Stacking
A z-index is one of two things, and no other number is written:
- Inside a component it orders siblings, from -1 to 3. It cannot reach past the component’s own stacking context, so it needs no scale.
-
Against the page (fixed, or portalled to
<body>) it names a layer. The layers are Bootstrap’s stack, which bslib puts on every page, so a blockr layer lands between Bootstrap’s own instead of guessing past them.
| Layer | Token | Value | Bootstrap |
|---|---|---|---|
| Overlay in the content (a drag ghost, a drop line) | --blockr-z-overlay |
1000 | $zindex-dropdown |
| Sticky header | --blockr-z-sticky |
1020 | $zindex-sticky |
| Fixed bar | --blockr-z-fixed |
1030 | $zindex-fixed |
| Side panel over the page | --blockr-z-offcanvas |
1045 | $zindex-offcanvas |
| Modal | --blockr-z-modal |
1055 | $zindex-modal |
| Menu | --blockr-z-menu |
1070 | $zindex-popover |
| Tooltip | --blockr-z-tooltip |
1080 | $zindex-tooltip |
| Toast | --blockr-z-toast |
1090 | $zindex-toast |
A layer sits above everything it can open from: a menu opens from a
modal or a side panel, and a tooltip names a menu’s rows. The overlay
layer is for something that belongs to the content but has to be fixed
on <body> (Shiny’s output wrappers can leave nothing
to position against): it covers the content it is dragged over and stays
under the navbar and everything that floats. The tests in blockr.ui hold
its stylesheets to these two forms and the layers to Bootstrap’s
values.
Dismissing
Anything that can be dismissed is a layer: a menu, a tooltip, the gear tray, a side panel, and a field holding an edit not yet committed. Layers stack in the order they opened. One listener on the page decides what Escape and an outside click close; a control does not stop the event itself.
- Escape acts on the top layer only. A second press acts on the next one. Escape never commits: a dirty field reverts, a title being renamed restores, a menu closes and returns focus to its trigger.
- An outside click (outside the layer and everything opened from it) closes menus and tooltips, and commits a dirty field, as blur does. It leaves the gear tray and a pinned side panel open.
- The gear tray closes by its gear and by Escape. It is in the page, not over it, so Escape closes the tray that holds focus; several trays can be open at once.
- A side panel floats until it is pinned. Unpinned, Escape and an outside click close it. Pinned, it is part of the page, and only its close button closes it.
The stack is Blockr.layer (blockr-ui.js). It reads
Escape and the pointer before anything else on the page does, so a menu
over a modal, or a field in a side panel, takes the key before the modal
or the panel sees it. A click is read on pointerdown: it lands in the
topmost layer it hits, a layer’s trigger counting as part of it, and
closes the layers opened after that one. A tooltip is a layer too, so
while one shows, the next Escape hides it and leaves the layer under it
open.
Menus
Every menu, dropdown and list that floats uses one surface:
-
bg-raised, 1pxborder-default, radius 8 (radius-lg, the same as the field it opens from),shadow-md. - Rows at 14px, radius 4 (
radius-sm), 10px side padding. Meta text 13px. - Row height and inner padding follow the kind of list, wherever it opens from. A list of values is scanned, so it stays dense; a menu of actions is mostly a short list and gets more room.
| List | Rows | Inner padding |
|---|---|---|
a list of values (Blockr.Select,
Select.menu()) |
32px | 4px |
a menu of actions (Blockr.menu,
action_menu()) |
34px | 6px |
The dock “…” menu, the download menus, the add menu and the @ menu move to it. The beak belongs to the gear tray only.
Menus of actions are
Blockr.menu(blockr.ui): rows, gaps, dividers, group titles and an optional head. A menu of actions is a little airier than a Select list: 6px panel padding and rows 34px, at the same radius 4 in the same radius-8 panel.A menu can carry one tool, at the end of its caption line in the top right corner: a 24px icon button, named by its tooltip, that opens the menu’s list somewhere fuller and takes what was typed in the filter box along. The block actions’ menu has one, “Open in the sidebar” (Picking a block).
Plain actions have no icon. A row gets one only when it is more than a plain action: it opens a mode or another surface (“Manage pages”), or it destroys something (“Remove”, a bin). Such a row sits in a group of its own, after a gap or a divider, so a label with an icon never sits right under one without. The toggle for what the card shows keeps its icon too (Controls, the sliders).
A destructive row is plain text at rest and turns
text-dangeron abg-dangerwash under the pointer or as the keyboard row. The current item (the page you are on) is weight 600 with a check at the end, no fill. Its classes (.blockr-menu,.blockr-menu__item, …) also style a menu whose markup is rendered elsewhere, so it looks the same.A menu of actions (downloads, the “…” menu) is
action_menu()orBlockr.menu: its rows run once and close it. The one remembered row is a toggle for what the card shows: a check at the end while on, first, in its own group before a divider. A menu that sets a value isBlockr.Select.menu(). The two share the surface and the placement.
Row states.
| State | Look |
|---|---|
| hover and keyboard row (same look) |
bg-hover; meta text and icons turn
text-default
|
| current pick | weight 600 |
| toggle on | a check at the end, normal weight |
| multi pick | a tick (14px, text-accent) in a slot every row keeps;
normal weight |
| disabled | listed in text-disabled, no hover, the reason in a
tooltip |
The views menu drops its blue fill for the current view.
Group titles and dividers. A group title uses the
section-title style (12px, 600, uppercase, 0.05em,
text-muted), 8px above and 4px below, inset to the row text
(10px). In a list that scrolls it is sticky. A divider (1px
border-default, the menu’s inner padding above and below,
edge to edge) separates groups without a title, such as ordinary actions
from a destructive one. A destructive item is plain at rest, as above,
and red only under the pointer or as the keyboard row.
Placement. One routine for all of them (Select’s
computePosition, moved to blockr.ui): fixed position on
body, 4px from the trigger, flips above when there is no
room below, follows scroll, stays 8px from the viewport edge.
| Menu | Width |
|---|---|
| field dropdown | the control’s width, at least 190px |
| menu from a button or a word | sized to content, 180 to 320px |
A menu lines up with the trigger’s left edge, or its right edge when
the trigger is in the header row. Height up to 320px, then the list
scrolls under its sticky titles and filter box. A multi menu’s tags take
up to three rows and scroll on their own past that, so the list keeps
room under many picks. A filter box (30px) appears above 8 options. It
is part of the menu, not a field: 13px, and no focus ring, since it
holds focus the whole time the menu is open. A Select.menu
names the role it sets in its title, because the word that opened it
names only the value.
Content. Long lists render up to 200 options and end
with a count row that asks you to type; server-searched lists say how
many values there are. Empty and loading rows are centred, italic,
text-muted (“No matches”, “No options”, “Loading…”).
Behaviour. The focus stays in the filter box or on the list, and the keyboard row is its active descendant, in a list of values and a menu of actions alike. Arrows move the row, Enter picks, typing filters. In a menu of actions the pointer moves the same row, Home and End jump, and Space picks too; opened from the keyboard, the menu starts on its first row, and the down arrow on its trigger opens it. Escape closes and returns focus to the trigger; an outside click closes, and so does Tab, after stopping at the menu’s tool where there is one. A pick closes the menu; a multi pick keeps it open and keeps the filter text.
The block’s “…” menu
Controls (the sliders, a check when on), a divider, then Rename, Append block, Copy block ID (the ID as mono meta text), and Remove block after a divider, a destructive row. No head: the block’s type and package are the tooltip on its mark, and the dock header has no subtitle.
Tooltips
One style for every name shown on hover: a small light card.
bg-raised, 1px border-default,
shadow-md, radius 8 (radius-lg, as the chart’s
data tooltip), 12px text-default, padding 5px 9px. It shows
on hover and on keyboard focus, and leaves with the pointer or focus;
Escape hides it.
- Every icon-only button and every cut-off label gets one, and so do
the block’s mark in the dock header (its type, then the package as a
neutral badge) and every name that can be renamed in place (the gesture,
see Renaming in place). So does a
segment with a terse label (
%,n,All): it shows the title its caller passes. So do a table’s sorted header (its order in words) and a link in the outline (the blocks it joins). Nothing else does: a button that names its action in full, like Save, has no tooltip. - No native
titletooltips remain in blockr markup: an element built in R carries its tooltip asdata-blockr-tooltip, with a badge after the name (the mark’s package) asdata-blockr-tooltip-badge, and a control built in JavaScript sets it withBlockr.tooltip.set(). A third-party widget’s titles are its own and stay native. - It matches the chart’s data tooltip (see Special blocks), so blockr has one floating style.
- It shows after the pointer rests 300ms, and at once on keyboard focus or while warm (within 400ms of another card leaving, so moving along a row of icons does not wait at each one). It sits above the element and flips below only where there is no room above.
-
Blockr.tooltip(blockr-ui.js) draws it; controls set it in place oftitle.
Picking a block
- Adding a block is one menu at the “+”, everywhere: the outline, a block’s append, the board’s Add block. Search on top, category titles, one row per block type with its 24px mark, its name and the package as a badge. Rows carry no description. A pick adds the block with a generated ID, and its link with a generated ID on the first free input. The menu’s tool, “Open in the sidebar”, opens the same list as the dock’s block browser, whose rows unfold into a form that sets the block’s ID and title, its links’ IDs and the input a link lands on before the block is added: an ID cannot be changed afterwards.
- Picking a block already on the board (add a panel, link to, add to a stack) uses the same menu, listing the board’s blocks: mark, title, and the block type as meta text. The Bootstrap modal with selectize goes.
Drawn in the design system page.
8. Labels and status
Four kinds, told apart by shape
| Kind | Class | Job | Shape |
|---|---|---|---|
| Badge |
.blockr-menu__badge,
.blockr-tooltip__badge
|
you only read it: “Categorical”, “blockr.dplyr”, “Added” | capsule |
| Tag | .blockr-select__tag |
a value you picked, with × when it can be removed | 4px corners |
| Pill | .blockr-pill |
a click changes a setting (the operator) | 4px corners |
| Count | none yet | a number on a button | capsule |
The word “chip” goes. Round ends mean read-only; tags, pills and buttons, which you act on, keep 4px corners.
Badges
- 18px capsule (
radius-pill), 11px weight 500, 7px side padding,bg-subtlefill, 1pxborder-default,text-muted. - Neutral unless it states a status or that something is on:
-
on, added, cutting rows: the accent tint
(
bg-accent-subtle,border-accent-subtle,text-accent-strong); -
danger, warning, success:
bg-<status>,border-<status>-subtle,text-<status>.
-
on, added, cutting rows: the accent tint
(
- Types, packages and significance levels are neutral; the type icon beside a name already carries the type. io’s blue “info” badge becomes neutral.
- Data colours (the arm label in the cohort list, the tile’s colour-by pill) come from the board’s scale and are outside this rule.
- A count with a unit (“4 blocks”) is a badge.
Tags
24px, radius 4, 1px border-default, 8px side padding.
Inside a field (a multi Select, a row) a tag takes
bg-hover, a step darker than the field, and its name is
13px text-default mixed 80% with bg-field, so
the tags do not outshout the rest of the block. A column label after the
name is 12px, the same mix at 70%, since text-muted is
under 4.5:1 on bg-hover. The crossfilter’s “Filter by” tags
sit on the surface: bg-surface, text-default,
and bg-hover while their card is open. The × follows Controls. The accent tint on a tag means the value
is cutting rows. The crossfilter’s “Filter by” pills are tags by this
rule, and the read-only items in a drill receipt are tags without ×.
Pills
Specified under “A pill that opens a menu” in Controls.
Counts
- 16px capsule, at least 16px wide, 4px side padding, 11px weight 600, tabular figures.
- Fill: the host control’s colour at 14%. On an accent host that is
color-mix(in srgb, var(--blockr-color-border-accent) 14%, transparent)withtext-accent-strongtext. - Shown only when the number is above zero.
- On a secondary or destructive button the count takes that button’s
colour:
text-defaulton abg-hoverfill, ortext-dangeronborder-dangerat 14%. A count never brings a second colour into a button. - A count never counts active filters (see Messages). No control shows a count at the moment.
Showing that a block filters
- The control that undoes a filter says what the filter kept, never a count of filters: see the rule under Messages.
- Dots are for block status only; the heatmap’s filter dot goes.
Block status
An 8px dot on the block’s mark with a 2px bg-surface
ring, one spec for dock and the DAG (block_status_badge()).
Its fills are meaning tokens, with no status tokens of their own: the
spec names each token next to its light value, which a renderer that
cannot read CSS (the DAG’s canvas) draws with.
| State | Fill | When |
|---|---|---|
| stale | text-muted |
the inputs changed since the block last ran |
| waiting |
border-warning, drawn as a hollow ring (1.5px,
bg-surface inside) |
an input is not ready: nothing linked in, or the block upstream is unset, waiting or failed |
| failed | border-danger |
the block failed |
| unset | border-warning |
a required input is empty; the same amber as the empty field’s cue |
Unset and waiting share the amber and differ by shape: the block that
needs you has the solid dot, and the blocks below it, which wait for it,
have rings. The amber replaces the off-palette #eab308
blockr.dock’s block_status_badge() gave unset.
Status line and plain text
The status line under the output is 12px text-muted: row
counts, filter state, timing, the drill receipt. These stay plain text,
with no badge: column types in the table preview (int, dbl), the status
line and its receipt, the filter trail printed in captions (in column
labels, see below), the Big N at the end of a column header.
Column names and their labels
A column has a name (AGE) and often a label (“Age”). The
board speaks in names, an exhibit in labels.
| Where | Shows |
|---|---|
| a control that holds a column: Select options and the chosen value, tags, filter rows, crossfilter cards, gear fields, the @ menu | the name, then the label as meta text: 13px text-muted,
8px after the name, no separator, cut first when space runs out (12px
inside a tag) |
| an exhibit: chart axes, legends and tooltips, output and summary tables, exports | the label only; an output table header shows the name in its tooltip |
| the table preview (a building surface) | the name, with the label on its own line under it, 12px
text-muted
|
| filter text printed with an exhibit: the filter trail in captions and exports | labels: “Sex = F; Age 18 to 64” |
| filter text that stays on the board: crossfilter shelf pills, drill receipts | names: “SEX = F” |
- No label, or a label equal to the name: the name alone. Never “unset”, never the name twice.
- A menu opened from a word that prints labels (a slot in a chart’s
title or subtitle, “Colour by Actual Treatment”) leads with the label
and mutes the name, so the row reads like the word that was clicked
(Select’s
labelFirst). The word belongs to the exhibit, which speaks in labels. - A tooltip that shows both has the name, then the label muted, like the control. The “+N” overflow lists the hidden tags that way, one per line.
- An aggregate reads “Mean of Age”, on a chart, in a table and in a rank table header.
- Code and decode pairs (PARAMCD and PARAM) follow the same rule: where one is picked, the code with the decode as meta (“ALB Albumin (g/L)”); in an exhibit, the decode, with the code in the tooltip.
- One reader for every package:
attr(x, "label", exact = TRUE), returning one string or nothing. Withoutexact, a column with haven value labels returns those, and the readers that expect one string break. - A sentence slot prints the name (
{@x}) or the label ({label(@x)}), as its author writes it.
Messages
- Required and empty: the amber cue on the field (Controls), never a banner.
- Syntax and validation text:
text-danger. - Empty and loading states in lists: centred, italic,
text-muted. - A panel or block with nothing to draw says so in one line of italic
text-muted, 13px, where the content would be and left-aligned with it (.blockr-empty,--panelor--blockfor the padding). The italic marks the line as standing in for content. No icon, no second line. A link in it is a slot and stays upright. There are two kinds, and the words differ:- Waiting: a choice is missing (no patient picked, a required input unset). Say what is missing; when the place to choose it is in the block, a slot takes you there: “No patient selected. Search for one”.
- Empty: the answer is none (no records in the window, a filter that keeps no rows). Say what was looked for and where, so the reader can tell none from broken: “No medication records between D-30 and D189”. A panel keeps its title and sentence above the line; the panel does not change size to hold it.
- A block that narrows what the reader sees (the crossfilter, the population filter, the patient profile under a drill) says so in one place on its header row. While nothing is filtered it is grey 12px text with the count (“306 patients”); while something is, the same place is the 26px main button with the reset icon and what the filter kept (“179 of 306 patients”), which clears it. Count in the unit the reader thinks in: the subject when the block knows it, rows otherwise. When the total is unknown, the button says what is shown (“6 patients”). Its tooltip names the clauses it clears. No count of active filters: the controls that set them show that.
- A block that sends a filter on without narrowing its own view (a chart or table drill) names the clause in its status line, with Reset beside it.
- A block-level error or warning (a failed expression, dropped rows)
uses the message style, above the output:
text-<status>onbg-<status>with aborder-<status>-subtleedge, 13px, radius 6. Danger for errors, the amber set for warnings.
Drawn in the design system page.
9. Special blocks
Charts
Around the plot: the header row, output title, sentence, caption and status line follow The block. Axis titles, legend titles and tooltips use column labels (Labels); blockr.ggplot’s axis titles move from names to labels. Header tools are 26px (the chart’s and composer’s 30px tools move to 26px). The download is a tool: with one format it downloads, with several it opens an action menu (
action_menu()), one row per format with the extension as meta.On the canvas: all text at
--blockr-mark-font-size(11px), read by chart.js once per render. The gutter measurement (11px), the 14px label-row budget and the printed charts keep their numbers.-
Canvas ink is read from the tokens at render, so it follows the scheme and a theme’s greys:
Canvas element Token axis and data labels text-mutedaxis line border-strongsplit lines border-defaultseparators, halo bg-surfacereference lines border-dangerface the body face Exports (PNG, pptx) take the light values. Retiring the “ECharts Theme” board option is a follow-up.
Facet strip: the section-title style (12px, 600, uppercase, 0.05em,
text-muted), centred over its panel. No card around the panel and no band behind the name; 24px between panels. Exports match.Legend band: HTML. Item text
text-muted, like the axis labels. Its items take the focus outline. The 25 x 14px swatch stays.Data tooltip: the raised card (
bg-raised,border-default, radius 8,shadow-md). Headline 13px weight 600 with the series swatch, which carries the series colour; rows 12px, labelstext-muted, valuestext-defaultat 500 with tabular figures. A split bar shows every segment of the hovered bar in one tip, so the swatch moves to the rows: the headline is the category, each row a level with its swatch and the column name (“drv 4”), and a count or a sum ends in a total under a hairline. Other aggregations name themselves under the headline (“Mean”) instead of a total.Slider bubble: stays inverse (dark label), because it labels a thumb during a drag.
Data colours: blockr.theme palettes.
Tables
One style for the table preview and for every output table (the table block and the composer table, which render annotated data frames).
-
Rows: 30px, 14px text on a 20px line with 5px above
and below, 12px side padding. No rules in the body; the hover wash
(
bg-hover) carries the eye along a row. -
Header: names 13px (
font-size-sm), weight 600,text-default. A second line, 12pxtext-muted, holds the column label (weight 400) in the preview and the Big N (weight 600: bold, but lighter than the name) in an output table. When some columns have a label, a column without one keeps the empty line so every name sits level. The preview adds the type (dbl, 11px muted) below. -
Header rule: one unbroken 1px
border-defaultrule under the whole header, row-label and row-number columns included. The card’s rule above the preview and the footer rule are the same grey, so a table has one line weight. - Spanners (a header over several columns) keep a tile: a 5% wash of the text colour, radius 7px at the top, 5px short of the columns at each side, so two spanners side by side read as two.
-
In a block card a table runs to the card edges: it
pulls itself out by the card’s side inset
(
--blockr-card-inset, set by blockr.dock) and pads its first and last columns and its footer back in by as much. The rules, the hover wash and the scroll area reach the edges; every text keeps the card’s alignment. Plots and diagrams stay inset. - Alignment: numbers right-aligned in tabular figures, with their header right-aligned over them; text left. There is no centred option. A spanner is centred over its columns. Exports (PPTX, XLSX, HTML) take the same alignment.
-
Row labels (the first column of an output table) in
text-default. Missing values intext-muted:NAat 12px in the preview, an em dash in a summary table. - Groups (an output table’s sections, and rows with deeper-indented children): the heading at weight 600 with the fold chevron (see Chevrons) in the gutter left of its text; each indent level adds 16px. Groups are set apart by space, not by a rule: a group’s last row has 7px below it and the next heading 11px above.
-
Fold all: the chevron in the top-left header cell,
in the same gutter as the group chevrons. A first-column title
(composer’s
column_header_left) stays the producer’s text, line breaks and weight included, beside it; only the chevron is the button. Without a title it reads “All”. It points down while every group is open and right otherwise. Alt+click on a group heading folds or opens every group. Exports never carry it. -
Hover and drill (output tables with a drill): the
hover previews what a click claims. A column header lights its column, a
row label its row, a value the crossing (row and column at 3% of the
text colour, the cell at 9%). A drill takes the same shape in
bg-selectedwithtext-accent-strong, with no accent bars; the crossing cell takesbg-accent-subtle-hoverat weight 600. - Sorting: the preview sorts on a header click: ascending, descending, missing values first (then ascending), off. A click on the name itself does not sort, since the name is text you can select. Ascending and descending put missing values last. An output table of an annotated data frame does not sort: its row order is its structure.
-
The sort cue: sort bars, 12px, three bars of 1.4px
that draw the values top to bottom: short to long for ascending, long to
short for descending. Missing values first adds a hollow dot at the top
right. The sorted column’s cue is in
text-accent. On hover the header takes thebg-hoverwash and an unsorted column shows the ascending bars intext-muted, as what a click would do; neither shows while the pointer is on the name, which a click does not sort. The cue sits on the name’s line: after the name, or before it on a right-aligned column, so the name stays over its numbers. The sorted header names its sort in the light tooltip (“Missing values first, then ascending”) and carriesaria-sort. -
Preview footer: the row range (12px muted) left,
the pager right as 26px tools with the chevron pointing left and right,
one
border-defaultrule above. Row numbers are a 44px right-aligned gutter, 12px muted.
Crossfilter
- Header row: the standard one. The count, which turns into Reset all while something is filtered, and the gear sit on the header row at the top. “Group by” is a field with a label; “Filter by” is a section title over the tags and cards. (The population filter, a block that joins two blocks in one, keeps its own layout.)
- Reset all: the header row’s count, as a block that filters shows it (see Messages): the 26px main button with the reset icon and what the filters kept while something is filtered. Its tooltip names the clauses it undoes.
-
Filter by tags: 13px tags. A tag whose card is open
takes the hover wash (
bg-hover); a tag cutting rows wears the accent tint, which wins when both hold. - Filter cards: the column’s name with its label as meta. Reset and remove are 26px tools, always shown; the reset is disabled while the card cuts no rows. Above 8 values the card has a search field (Controls).
-
The card’s list is a table and follows Tables: headers 13px weight 600 with the rule under
each, rows 30px at 14px with 12px side padding, Count right-aligned with
the sort bars before it, missing and empty values (
NA,(empty)) intext-mutedand last. It starts sorted by Count, largest first; a click flips the sorted column and a click on the other sorts it (values A to Z, Count largest first). It has no unsorted state. The 200px scroll area shows about 6 rows. -
Pools: the group’s and the subgroup’s are edited
alike, and the subgroup’s say so (“Subgroup pools”, “Add subgroup
pool”). A group’s pools may overlap; a subgroup’s may not, because
tables pool only the outer split. While a subgroup’s pools overlap, its
fields take the amber cue (
border-warning,bg-warning) and one 12pxtext-warningline names the levels in two columns and the way out. -
Bars: accent fill at
--blockr-mark-weight-supporting, value end rounded by--blockr-mark-radius. - Settings: in the gear tray.
- “+ More filters” opens a menu of every column, grouped by table, each with its type as a neutral badge. A column with a card is a multi pick (the tick); a click adds or removes its card.
Code editor
The code, function and composer blocks share one CodeMirror editor.
The editor is a large input:
bg-fieldbackground, 13px mono.-
Syntax colours come from four local tokens:
Token Points at --blockr-code-keywordtext-accent--blockr-code-stringtext-success--blockr-code-numbertext-warning--blockr-code-commenttext-muted Line numbers
text-muted; the active linebg-hover; the input-line band is mixed fromborder-accent.Footer: Run and Accept all are 26px main buttons; Reject all is a 26px secondary button. The syntax message is
text-danger.Code is 13px on every code surface; dplyr’s expression input moves from 14px.
The green and red line backgrounds of an AI review diff are diff meaning and stay.
Composer
The composer keeps its busy cue (after 300ms the body dims to 0.45
and a clock runs under the sentence) and the “composed in” time that
fades after 2s, both in text-muted. Every block gets the
same cue: past 300ms of computing, its output dims to 0.45 and a clock
(“computing… 1.2 s”) runs under it; nothing shows for a faster run.
blockr.core applies it from outside the block.
Drawn in the design system page.
Appendix
Where the code lives
Package blockr.ui owns the tokens
(inst/assets/css/blockr-tokens.css,
blockr-tokens-dark.css), the block category colours and the
logo’s green among them, and the shared components: the params grid
(moving from blockr.extra), Blockr.Select and its placement
routine, Blockr.checkbox, the badge, tag and pill styles,
the slot and offer rules (today declared twice, in blockr.viz
chart.css and blockr.dm
crossfilter-block.css), the gear button, the tool button
and the action menu (tool_button(),
action_menu()), the button classes
(.blockr-btn), the gear tray for blocks rendered in R
(gear_tray()), the small icons (Blockr.icons,
small_icon() in R), the block’s mark
(block_mark(), block_mark_svg()), and the
column-label reader. Package blockr.dock owns the dock header, the “…”
menu, the block status dot and its spec
(block_status_badge(), which the DAG draws from too) and
the rule that hides the gear in simplified mode. Package blockr.theme
owns data colours.
The token tests in blockr.ui check that the vocabulary changes only deliberately, that every name follows the grammar or is a legacy alias, that the controls read meaning tokens only, that icons, status borders and the logo clear 3:1 on the surface in both schemes, and that consumers’ fallbacks agree with the tokens. Palette and legacy reads elsewhere in blockr.ui change only deliberately too. To do: warn on every palette or legacy read in the packages that consume the tokens.
Legacy token aliases
Old names stay in blockr-tokens.css for a release and
each keeps its old value. Packages move over when convenient; the alias
goes when nothing reads it.
| Old name | Points at |
|---|---|
--blockr-color-text-primary |
color-text-default |
--blockr-color-text-secondary |
grey-700 (by job: labels and status lines go to
text-muted; headings, content and hover states to
text-default) |
--blockr-color-text-subtle |
grey-400 (by job: placeholders, meta and icons go to
text-muted; disabled controls to
text-disabled; a border to border-strong) |
--blockr-color-text-meta |
color-text-muted |
--blockr-color-border |
color-border-default |
--blockr-color-border-hover |
color-border-strong |
--blockr-color-bg-input |
color-bg-field |
--blockr-color-primary |
color-bg-accent |
--blockr-color-primary-hover |
color-bg-accent-hover |
--blockr-color-primary-bg |
accent-50 (keeps its old value; the accent tint
color-bg-accent-subtle is a 7% mix, nearly the same
colour) |
--blockr-color-error,
--blockr-color-danger
|
color-border-danger |
--blockr-color-success |
color-border-success |
--blockr-color-warning |
color-border-warning |
--blockr-color-warning-bg |
color-bg-warning |
--blockr-color-warning-text |
color-text-warning |
--blockr-font-size-section |
font-size-lg |
--blockr-font-size-title |
font-size-xl |
--blockr-shadow |
shadow-sm |
--blockr-shadow-dropdown |
shadow-md |
One new token reuses an old name:
--blockr-color-bg-hover was grey-100, and it stays grey-100
in light until the fallbacks that write #f3f4f6 move to the
text wash. The dark scheme uses the wash already.
Removed from the UI tokens: --blockr-color-negative
(data colour, belongs in blockr.theme). Local names move under their
owner: dock’s spinner-delay, sidebar-*,
stack-height and header settings to
--blockr-dock-*; viz’s rank-* to
--blockr-viz-rank-*. blockr.outline drops its own
--md-* variables and reads the blockr tokens.
Not specified yet
Dialogs (surface, width, title, button order), dock tabs beyond their
selected state, the DAG view, notifications (Shiny’s
showNotification), and the icon set with its sizes (the
chevron is specified under Actions).