Skip to contents

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

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

  1. 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 600 text-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.
  2. 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.
  3. Gear tray. Opens in flow directly under the header row.
  4. Controls on the face. Transform blocks only: fields in the grid, or rows for lists you add to and reorder.
  5. 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, --20 and --16. It is built by block_mark() in R and by a row’s mark in Blockr.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 13px text-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-subtle fill, 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 turn bg-surface (white), so the tray reads as a separate layer. In dark the tray and its beak take bg-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() and tray_section() draw it. Its fields are Shiny’s own inputs, with updateOn = "blur" on textInput() and numericInput().

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 \n in 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 dashed border-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-default on 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-muted meta text: the first view, then “+N” in text-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-selected with text-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-accent line 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-empty line.

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” in text-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 “∞” (13px text-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, 1px border-default, radius 8 (radius-lg), 12px side padding, 14px text-default. Placeholder text-muted.
  • Focus: border-accent and --blockr-focus-ring, on any focus.
  • Disabled: text-disabled.
  • Required and empty: border-warning (amber-600, #d97706), bg-warning fill, 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=number for the keyboard, hides the spinner arrows, and commits like text; arrow keys still step. A Bootstrap override brings Shiny’s numericInput to 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-danger on 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: border text-muted.
  • Checked: bg-accent fill and border, the check in text-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, 1px border-default, radius 8, 3px inner padding and gap. Segments share the width, radius 6, 14px text-muted; hover text-default on bg-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 500 text-default mixed 70% with bg-field (text-muted is under 4.5:1 on bg-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-hover wash (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-editable on 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-danger edge and a one-line 12px text-danger message 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, 1px border-default) 12px from the left, then the value as hex in capitals, 14px text-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, 1px border-default, 13px text-default. Placeholder “Search…” in text-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-accent and 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: Bootstrap gear-fill at 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-muted at rest; text-default on hover of the chevron or of the row or control it belongs to; text-disabled when 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-mac on the root, Blockr.isMac, Blockr.keys("Mod+S") in JavaScript and shortcut("Mod+S") in R, where Mod is 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-muted text (“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-accent at 14%) and a text-accent-strong initial.
  • 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.

Every menu, dropdown and list that floats uses one surface:

  • bg-raised, 1px border-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-danger on a bg-danger wash 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() or Blockr.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 is Blockr.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 title tooltips remain in blockr markup: an element built in R carries its tooltip as data-blockr-tooltip, with a badge after the name (the mark’s package) as data-blockr-tooltip-badge, and a control built in JavaScript sets it with Blockr.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 of title.

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-subtle fill, 1px border-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>.
  • 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) with text-accent-strong text.
  • Shown only when the number is above zero.
  • On a secondary or destructive button the count takes that button’s colour: text-default on a bg-hover fill, or text-danger on border-danger at 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. Without exact, 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, --panel or --block for 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> on bg-<status> with a border-<status>-subtle edge, 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-muted
    axis line border-strong
    split lines border-default
    separators, halo bg-surface
    reference lines border-danger
    face 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, labels text-muted, values text-default at 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, 12px text-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-default rule 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 in text-muted: NA at 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-selected with text-accent-strong, with no accent bars; the crossing cell takes bg-accent-subtle-hover at 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 the bg-hover wash and an unsorted column shows the ascending bars in text-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 carries aria-sort.
  • Preview footer: the row range (12px muted) left, the pager right as 26px tools with the chevron pointing left and right, one border-default rule 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)) in text-muted and 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 12px text-warning line 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-field background, 13px mono.

  • Syntax colours come from four local tokens:

    Token Points at
    --blockr-code-keyword text-accent
    --blockr-code-string text-success
    --blockr-code-number text-warning
    --blockr-code-comment text-muted
  • Line numbers text-muted; the active line bg-hover; the input-line band is mixed from border-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.


10. For package authors

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

Decision record

The reasoning behind each rule, the options that were compared and the questions they answered were in naming.md, questions.html and topics/ in dev/design-system/. They were removed once this document was complete; git history has them (git log --all -- dev/design-system/naming.md).