Internal ui is the shared interface of Landings Admin and LP Constructor: one set of tokens and components for both products, their design canvases and, later, their code. Internal tools for marketing: calm, dense, desktop-first (1440, checked at 1024, 1280, 1920), light and dark.
Code: GitLab · package: npm (npm i @mrktg-common/internal-ui) · this system as a page: S3.
Content
- Interface copy is English, sentence case: “Make v4 live”, “Publish to production”.
- Buttons name the action, verb first. Never “OK” or “Yes”.
- People appear by email; the name goes in the tooltip. Your own rows are marked “you”.
- Say what happens and what stays: “Published landings stay on Aurora.” “The site may still show the previous version.”
- Time always with its zone: “Tue, Oct 13, 10:00 · Europe/Moscow · UTC+3 · in 5 days”. Relative times carry the absolute one in a tooltip.
- Request IDs and addresses are mono with a copy button.
- No emoji, no exclamation marks.
Colour
Tokens live in tokens.json; both themes switch with data-theme="light" or "dark" on <html> (default: the system's). Never write a hex in a component: name a token.
Roles, the same in both products:
- Green (
success*): Live, nothing else, in every environment. Where it is live is said by the environment — the Dev pill in the header, the PROD / TEST chip in the row — not by the colour, just as buttons don't change colour on dev. Not “saved”, not “copied”, not “available”. - Yellow (
warning*): unsaved, missing, drift from the site, Unpublished, consequences in a form, and a non-production environment (Dev pill, TEST chip, dev stripe), always with the word. - Red (
danger*): errors and irreversible actions (Unpublish, Delete). - Accent: a process running, selection, focus, links, info.
- Neutral (
bg-subtle,text-muted): Test, Draft, Previous, saved states, counts. Outline: exists but not current (Never published, Archived).
Every status carries a word and a mark (dot, hollow dot, spinner, icon), so it reads without colour.
Text: text for content, text-muted for hints and secondary data, text-faint only for placeholders and separators on surface (it fails on bg, bg-subtle and danger-soft in light). Text on an accent fill is accent-contrast, never white. Contrast for every pair is in the “Colour and contrast” section.
Type, space, shape
- Inter 400/500/600/700 from Google Fonts, falling back to system-ui; mono for addresses, slugs, keys. Base size 13 (
text-base); scale 11 · 12 · 13 · 14 · 16 · 18 · 22. - 4 px spacing step; page gutter 32 (
space-8). - Radius 4 for sm controls and menu rows, 6 for md controls, 10 for cards, popovers, modals, toasts; pills for badges.
- Controls 28 (
control-height-sm: toolbars, panels, rows) and 34 (control-height-md: forms, pages). Header 56, editor toolbar 44, table head 36, rows 52. - Shadows only for things that float:
shadow-mdmenus and tooltips,shadow-lgmodals and toasts.
States
Every interactive part has hover, keyboard focus (focus-ring, a 3 px accent halo), off, and error where it takes input.
- Off, with the reason. Don't hide or silently disable what people expect. Use
aria-disabled="true", keep focus, show why and how to get it (tooltip or a hint line, plusaria-describedby). Nativedisabledonly where the reason is obvious (Redo, Previous on page 1). - Error under the field that caused it, with
circle-x; general errors in an alert with Retry and the request ID. - Canvases show states statically with
.is-hover,.is-focus,.is-open.
Iconography
Lucide only, inline SVG with class ic (16), ic-sm (14), ic-xs (12), ic-lg (28), ic-xl (40), stroke 2, currentColor. In code: lucide-react with className="ic". Product tiles: Landings Admin globe, LP Constructor blocks on an accent tile. Icon-only buttons get aria-label and a tooltip.
Using it on a design canvas
- Install this system on the canvas (Theme menu, or ask Claude to install Internal ui). It copies
tokens.json,tokens.css,components/bundle.cssandcomponents/canvas.cssintoproject/ds/internal-ui/and records the version. - In every artboard, right after the
support.jsline, link in this order:ds/internal-ui/tokens.css,ds/internal-ui/components/bundle.css,ds/internal-ui/components/canvas.css, then the product's own stylesheet (on the constructor canvas,lpc.css). In<helmet>: the Inter link from Google Fonts. - Build with the classes from the component cards. Product stylesheets add only what the product alone has and never restyle a system class.
canvas.css holds spec-sheet helpers (sheet, panel, kv, doc, qa, swatches) and is for canvases only. Put .is-still on an animated part (or any ancestor) to show it as a still frame: spinners stop, a countdown or indeterminate bar holds at --still (60% by default) instead of ending empty.
Using it in code
Planned package @mrktg-common/internal-ui, same version as this system: tokens.css and components.css (this bundle.css) imported once at the app root, plus thin React components on these classes. Overlays and the toast stack are absolute inside artboards; the app renders them in a portal with .is-fixed. Product screens keep their own CSS modules and use the package's components.
Update rule
- The version is
meta.releaseintokens.json, and every change gets a line in the Changelog section: patch for fixes that change no markup, minor for additions, major when a class, token or markup changes. - Product canvases get the system only by installing it with a version. After a change here, each product canvas reinstalls it; nobody edits the copy under
project/ds/on a product canvas. - A change that both products need is made here first, then reinstalled. Something only one product has stays on that product's canvas and links to this system.
- Until the code package exists, this system is the source; after its first release the repository is the source and this system is re-synced from it at the same version.
Colour and contrast
WCAG 2 AA: text 4.5:1, text 24 px+ and UI marks (control edges, focus, icons that carry meaning) 3:1. Dark *-soft fills are translucent; their ratios are measured over surface.
Text pairs
| Text | Ground | Light | Dark |
|---|---|---|---|
text |
surface |
16.5:1 | 13.0:1 |
text-muted |
surface |
6.0:1 | 6.2:1 |
text-muted |
bg-subtle |
5.1:1 | 6.6:1 |
text-muted |
any *-soft |
5.2:1 and up | 4.6:1 and up |
text-faint |
surface |
4.7:1 | 4.8:1 |
text-faint |
bg |
4.4:1 — fails | 5.4:1 |
text-faint |
bg-subtle |
4.1:1 — fails | 5.1:1 |
text-faint |
danger-soft |
4.1:1 — fails | 3.8:1 — fails |
accent-contrast |
accent |
4.6:1 | 5.9:1 |
accent-text |
surface |
7.3:1 | 8.3:1 |
accent-text |
accent-soft |
6.3:1 | 6.6:1 |
success-text |
success-soft |
4.8:1 | 6.9:1 |
warning-text |
warning-soft |
5.0:1 | 6.9:1 |
danger-text |
danger-soft |
5.4:1 | 6.0:1 |
danger-text |
surface |
6.1:1 | 7.5:1 |
surface (tooltip text) |
text |
16.5:1 | 13.0:1 |
surface (Dev tag text) |
warning-text |
5.5:1 | 9.3:1 |
So: text-faint goes on surface only. Inside an invalid field the placeholder switches to text-muted. Disabled fields (bg-subtle) show their placeholder in text-muted.
Not for text
dangeronsurfaceis 4.4:1 in light: borders and icons only; the required mark usesdanger-text.successandwarningare icon colours (3.5:1 and 3.2:1 on surface in light).- White on
dangeris 4.4:1 light, 3.1:1 dark: the danger button is outlined, not filled. - White on the dark
accentis 3.1:1: text on accent is alwaysaccent-contrast.
UI marks
- Switch track off:
text-faint, 4.7:1.border-strongwould be 1.5:1. - Accent selection outline on a white page in dark theme: 3.1:1 (page content isn't themed).
border(hairlines) is decorative; control edges useborder-strong.
Patterns
Page or modal
Long or losable work (a new landing with an upload) is a page. Short decisions are modals: sm 440, md 560, lg 800.
Confirmations
- Changes the live site (Publish, Make live):
mdmodal with the change block (v7 → v4) and the facts (Address, Environment). The primary button names the action. - Irreversible now (Unpublish on production): danger alert and the slug typed to confirm; the danger button stays
aria-disableduntil it matches. On Dev only the yellow alert, no typing. - Doesn't touch the site (Archive, Restore):
sm, one paragraph. - Reversible and small (delete a block, clear the canvas): no confirm. Do it and offer Undo in a toast for 10 s.
- Consequences inside a form (changing the brand) are a warning alert above the button, never a
confirm(). No browser dialogs anywhere, except the browser's own leave-page prompt.
Long operations
Progress modal with steps and no close button; “Run in background” moves it to a banner. The result closes by itself with a countdown, then a toast with a link.
Toast, banner or alert
- Toast: what just happened. Goes away (6 s, Undo 10 s, errors stay).
- Banner: the state of the whole page or editor. Stays until the state changes. One at a time.
- Alert: a message tied to a place: a form, a modal, an empty table.
Errors
- Field errors under the field, after blur or submit.
- A failed load: an alert or empty state with Retry and the request ID (mono, copy button). No red when the live site isn't affected.
- After a failed publish: the step that failed and “The site may still show the previous version”.
Off, with the reason
Options people can't use stay visible: aria-disabled, a lock where it's about rights, the reason and the way to get access. Example: “Production needs the landings-publishers group. Ask Dana Fox or Sam Taylor to add you.”
Lists to choose from
Group headings with menu-label; the state of a whole group in one line under its heading; a value that isn't in the list pinned on top as “Selected”; an empty search says what didn't match and gives a hint.
People, ownership, View only
By email with the name in the tooltip; “you” on your own rows. Someone else's object: a View only plaque with a lock in place of the actions.
Empty states
Icon, title, one sentence, one action (two at most).
Adopting on the product canvases
What each product canvas changes when it installs a release. Each product thread does this on its own canvas, newest release first.
1.11
LP Constructor canvas
- Reinstall Internal ui 1.11.0.
- Nothing to redraw: the SourceDetailed board already shows the rules grouped (the form’s language, then “Other languages”). In code the list now uses
groupsinstead of putting the form’s language first by hand.
Landings Admin: no change.
1.10
LP Constructor canvas
- Reinstall Internal ui 1.10.0.
- ABM Section Form, Source Detailed: a Combobox over the confirmation-email rules with
ok/warn/hintlines under it (MIFD-2289). See the “Source Detailed” section in Handoff.
Landings Admin: no change.
1.9
LP Constructor canvas
- Reinstall Internal ui 1.9.0.
- Editor and Vibe (VbParFailed, VbParReady): with the properties panel open, the toast stack sits 24 px left of the panel (
--toast-inset-end: 320pxon.toasts), not over Generate and the fields. Matches the toasts item in Handoff, “Параллельная генерация Vibe HTML”.
Landings Admin: no change, the stack keeps the window edge.
1.8
Landings Admin canvas
- Reinstall Internal ui 1.8.0.
- Dev headers (
hdr-dev): the logo tile iswarning, as on the dev favicon. Nothing to change in the markup.
LP Constructor: no change, its own mark isn't recoloured.
1.7
LP Constructor canvas
- Reinstall Internal ui 1.7.0.
- Projects and Templates: the “Long list · only the table scrolls…” board drops its inline styles:
main.page.is-fillwithpage-head,toolbar,contentand thetbl-boxinside. Matches the “Прокрутка списков” row in Handoff.
Landings Admin canvas
- Optional: the Landings list as
page is-fill, the same way.
1.6
LP Constructor canvas
- Reinstall Internal ui 1.6.0.
- Header and the Shell sheet (page 2): the
logo-marktile withblocksgives way to the constructor's favicon inlogo-img, 26 px, no tile; the same in the editor's compact header. Matches the “Logo” row in Handoff.
Landings Admin: no change, the globe tile stays.
1.5
Both canvases
- Reinstall Internal ui 1.5.0.
- Live is green in every environment. Live on Test and in the dev admin:
b-neutral→b-success, the dot stays. Live-version rows loseis-nonprod. “vN is live on Test” alerts and toasts usealert-success/t-success. Where it is live is said by the Dev pill and the PROD / TEST chips, as before.
LP Constructor canvas
- Projects, CompBadges, CompSite, Publish (page 11), Toolbar: Live on TEST rows and “Live on test” go green.
Landings Admin canvas
- Dev environment (ListRuDev, landing page on dev, Make live on dev): Live green, the live-version row without
is-nonprod. - Page 8: Published list TEST rows show Live green.
1.4
LP Constructor
- Reinstall Internal ui 1.4.0.
- Projects → Brand: INT and RU groups get the whole-group row (
wholeGroup): “All INT brands” / “All RU brands” first under the heading, mixed while part of the market is ticked. The button reads “Brand: All INT” when exactly one market is picked.
1.2
Both canvases
- Reinstall Internal ui 1.2.0.
- Breadcrumbs:
nav.crumbswitharia-label="Breadcrumbs", links,chevron-rightseparators (ic,aria-hidden), the last itemspan.herewitharia-current="page". - Animated parts that a static artboard shows (countdown, indeterminate bar, spinners): add
.is-stillinstead of inlineanimation-delay/animation-play-state.
LP Constructor canvas
- Editor header: replace
doc-title/crumb-back/crumb-sep/doc-namewithnav.crumbs.crumbs-lg, the project name as.here; the type badge and the save state follow the nav as before. Remove those four classes fromlpc.css.
Landings Admin canvas
- Remove
.crumbsand its rules fromla.css; the landing page markup already matches (addaria-hiddento the chevrons,aria-currentto the slug). - PublishProgress (done): the countdown's inline
animation-delayandanimation-play-statebecome.is-still.
Both canvases
- Reinstall Internal ui 1.1.0 (same links as 1.0).
- Remove from the product stylesheet what moved here:
drop*,file*,progress*,report*, the report row class (rep-checkinla.css,div.checkinlpc.css),c-ok,c-info,c-warn,c-err,paths,steps,step*,.tbl tr.grp. - Markup:
- report rows become
report-row, the text inreport-text; - the file row's text goes in
file-body, the size or what was found infile-meta; actions infile-actions; - the dropzone's second line is
drop-hint, buttons indrop-actions; a dropzone in a panel or modal can usedrop-sm; - progress bars get
role="progressbar"and their values; the bytes and percent under a bar go inprogress-label; - a result that closes by itself uses
progress-countdown(neutral) with its duration in--countdown; - done steps turn neutral by themselves; a failed step gets its error in
step-err.
- report rows become
LP Constructor canvas
- How translations work: numbered steps use
step is-numinstead of the inline accent styles.
Landings Admin canvas
- Keep in
la.css:.tbl tr.is-past, and.hdr-center/.project-namefor the constructor mockups on pages 8–9. - Delete event… in the event window:
btn-dangerin the footer's.start, without the inline red on a ghost button.
1.0
What each product canvas replaced when it installed Internal ui 1.0.0.
Both canvases
- Install Internal ui 1.0.0. In every artboard replace
<link rel="stylesheet" href="./admin.css">with, in order:ds/internal-ui/tokens.css,ds/internal-ui/components/bundle.css,ds/internal-ui/components/canvas.css, then the product stylesheet. - Remove
project/admin.css. - Markup the new classes need:
- clickable table rows get
is-link(the old rule made every row a pointer);first/lastcell classes are no longer needed; - sortable headers become
th-sortbuttons witharia-sorton theth; - Live on Test or in the dev admin:
b-neutralwith a dot; the live-version row on Test or Dev addsis-nonprod; - Draft gets the hollow dot, Unpublished
eye-off, Error and Publishing failedcircle-x; - selected option: check after the label (
.check), not before it; - toast description
toast-desc(wasmutedorhint), its action abtn-secondary btn-sminsidetoast-act, a close button on every toast; saving and copying toasts uset-neutral; - banner text in
banner-text, buttons inbanner-actions; editor banners addbanner-tight; - change block sides in
change-side; the new sidechange-v is-target(was an inline accent colour); dates and nameschange-v-sm; - Cancel in modals is
btn-secondary; a note or third action in the footer is.start; - native
disabledwith a reason becomesaria-disabled="true"plus the reason (tooltip or hint); - the Copied state and positive field checks are neutral by themselves (no markup change).
- clickable table rows get
LP Constructor canvas
lpc.csskeeps only constructor parts:--panel-left-width,--panel-right-width,--rail-width,--canvas-*; crumbs and save state,kbd, toolbar (tb*,lang-btn,split,zoom), editor layout, palette, outline tree, properties panel, canvas layer, the site popover (pubcell,more-pill,site-*), favicon and browser tab. Remove from it what moved here:--toolbar-height,--duration-base,nav,theme-row,seg-sm,seg-icon,env-chip,label-row, and the spec helpers (swatch-grid,sw*,ratio,diff-old,new-mark,fix-mark,anno,stage) now incanvas.css.- Page 1 Tokens: keep only the constructor's own tokens (panels, rail, canvas layer) and link to this system for the rest.
- Page 2 Components: Header, user menu, buttons, fields, segmented, select and multi-select, badges, toasts, banners, modals now come from here. Keep on the page: Toolbar, type and translation badges, saved versions, date range, radio cards, filter bar layout, left panel, properties panel, canvas layer, On the site.
- Status changes on Projects, CompBadges, CompSite, Publish (page 11) and the Toolbar: TEST rows show Live neutral; Unpublished and Publishing failed get their icons.
- Brand filter (ProjBrand): group headings not clickable; Apply goes, “Applies as you tick” instead.
- Toasts: “Project saved” and “Version saved” use the neutral icon; 6 s.
Landings Admin canvas
- Move admin-only classes out of
admin.cssinto an admin stylesheet: breadcrumbs, landing page layout (lp-*,note-text), activity feed, dropzone, file row, progress, archive report, publish steps, schedule (sched-*,tl*,ev-*,next-ev,when-grid,utc-line,ver-opt), and the constructor mockups of pages 8–9 (pub-*,result*,section-title,static-val,ed-*,project-name,c-user*,hdr-center).is-lateris incanvas.css. - Page 6: Header, Badges, Controls and Feedback cards come from here. Keep on the page: Zip upload, slug field, note inline edit, publish steps, activity events.
- Confirm modals (Archive, Make live, Restore, Unpublish): Cancel ghost → secondary.
- Sort menu on the list goes; sort by the column headers.
- Dev environment (ListRuDev, landing page on dev, Make live on dev): Live neutral, live-version row
is-nonprod. - Page 8: Published list TEST rows show Live neutral; toolbar badge Unpublished and Publishing failed get their icons.
- User menu: add the Theme row (System, Light, Dark).
- Row action “Not on the site”:
aria-disabledwith a tooltip instead ofdisabled.
Changelog
1.11.0 · Oct 20, 2026
- Combobox: groups of suggestions, a
menu-labelover each group, as in Select menu. A group with no match hides with its label, an empty group never shows, arrows skip labels, Enter takes the first match in group order, “No … match” is one line for the whole list, the chevron opens every group with the current value checked in its own. React:groupsonCombobox.
1.10.0 · Oct 20, 2026
- Text field: warning state,
.warnwithtriangle-alertinwarning-text— the value is allowed but probably wrong, saving and publishing go on. The control keeps its border; only one line shows, in the order error, warning, check passed, hint.FieldandTextFieldtakewarning. - Combobox: a text field with suggestions that keeps any typed value (
combo,combo-btn,markinmenu-item). Filters while typing, checks the value on blur or pick; loading, nothing matched and couldn’t load. For closed lists Select menu stays. React:Combobox.
1.9.0 · Oct 14, 2026
- Toast: useToastInsetEnd(width) moves the stack off a side panel;
--toast-inset-endon.toasts.
1.8.0 · Oct 14, 2026
- Header: on dev the logo tile is
warning, matching the dev favicon; an own mark isn't recoloured.
1.7.0 · Oct 14, 2026
- Page:
is-fill— a list page takes the window, only the table scrolls; the table head sticks and the footer stays.
1.6.0 · Oct 14, 2026
- Header: productMark — the product's own mark (26 px, no tile) instead of the icon tile.
1.5.1 · Oct 13, 2026
- Table: the minimum width is never less than the fixed columns plus 120 px for each column without a width (
minWidthon the column overrides it). Before, a hand-setminWidthcould leave the flexible column (Comment, Note, Name) zero pixels when the window narrowed.
1.5.0 · Oct 13, 2026
- Live is green in every environment:
b-successwith a dot on Test and in the dev admin too; where it is live is said by the Dev pill and the PROD / TEST chips.StatusBadgeignoresproduction(deprecated, goes in the next major), the live-version row has nois-nonprod, “vN is live on Test” usesalert-success/t-success.
1.4.0 · Oct 13, 2026
- Multi-select: whole-group row (All INT brands) with a mixed state; the button names one whole group.
1.3.2 · Oct 13, 2026
- Library favicon on the design system page (design/assets).
1.3.1 · Oct 13, 2026
- Examples: cards, tests and token samples use only made-up brands (Aurora, Borealis, Cirrus and the like), example.com addresses and name.surname@example.com people. A test fails on the company’s brands, domains and real names.
1.3.0 · Oct 12, 2026
- Change block: a side can carry several meta lines (date and who), as the card always allowed.
- Button:
buttonClasslives in a module without'use client', so server components (Next layouts, sign-in screens) can style links as buttons.
1.2.1 · Oct 12, 2026
- Search: the browser's own clear button no longer shows next to the component's; a
reffrom the product reaches the input without breaking Clear.
1.2.0 · Oct 12, 2026
- Breadcrumbs: one component for the landing page in Landings Admin and the editor header in LP Constructor. Separator is a
chevron-righticon intext-faint(decorative), current itemtext500;crumbs-lgin the header (14 px, the document title at 600). Replaces.crumbsinla.cssandcrumb-back/crumb-sep/doc-nameinlpc.css. - canvas.css:
.is-stillfreezes animations on an artboard on a fixed, non-empty frame.
Not added: the result state (big icon, title, actions instead of the form). Only LP Constructor's Publish window has it; the copy on the Landings Admin canvas (pages 8–9) is a mockup of that window. It stays a constructor part; when another window in either product needs it, it comes here.
1.1.0 · Oct 12, 2026
Added what both products use for files and long operations, proposed by the Landings Admin designer:
- Dropzone: empty, hover, focus, dragging over, wrong type or too big (checked before upload), off with the reason, compact
drop-sm. - File row: icon, name, size or what was found, Replace / Change / Remove; uploading with progress inside; failed with Retry.
- Progress: determinate with bytes and percent, determinate with a count, indeterminate, countdown before a result closes by itself (neutral, a timer).
- Result report: rows ok / info / warn / error, mono paths with the bad part marked, optional head with a count. A passed check is neutral.
- Steps: waiting, running, done, failed with the step number and the error; numbered instructions (
is-num). Done is neutral: the green comes with the result, and only for production. - Table: group heading row
tr.grp.
Not added, because only one product has them: the muted “past” table row and the centred project title in the header (both stay on the Landings Admin canvas), and a red-at-rest ghost button. A destructive third action in a modal footer uses btn-danger in .start.
1.0.0 · Oct 12, 2026
First release, named Internal ui (namespace and canvas folder internal-ui, code package @mrktg-common/internal-ui), merged from the Landings Admin canvas (page 6, admin.css), the LP Constructor canvas (pages 1–2, admin.css + lpc.css), lp-constructor/src/styles/tokens.css and both products' UI code. Agreed screens (the brief, admin pages 8–9) are kept; where they differed from the rest, the agreed version won.
Decisions in this release:
- Green only for live on production. Live on Test and in the dev admin is neutral with a dot; Copied, Available and Saved are neutral; the live-version row is tinted only on production (
is-nonprodotherwise). - Every status has a mark: Draft hollow dot, Unpublished
eye-off, Error and Publishing failedcircle-x, Archivedarchive. - Selected option: check on the right. Group headings aren't clickable. Multi-select filters apply as you tick, no Apply button.
- Cancel in modals is secondary. Off states use
aria-disabledwith the reason. - Logo hover:
accent-text, no underline. Header tile icon 14 px. Dev pill icontriangle-alert. - User menu has the theme switch in both products.
- Switch track off is
text-faint. Required mark and invalid-field placeholder fixed for contrast. - Toast: 6 s with pause on hover, Undo 10 s, errors stay; description 12 px; one action button; close always.
- Banner: neutral, accent, warning, danger; inset 32 on pages, 16 in the editor (
banner-tight). - Change block:
change-v-smfor dates and names,is-targetfor the new side. - Tables: sticky head, header sorting (
th-sort,aria-sort), clickable rows markedis-link, footer with count and optional pager. - Copy field: md 34 and sm 28, Open optional, Copied 2 s.
- Select lists in code are
fixedabove modals with a 320 px scroll. - Tokens:
toolbar-heightandduration-basejoin the shared set; tone borders stored as their rgba values (same colours as thecolor-mixthey replace).
Alert
A message inside the content: in a form, a modal, an empty table. Icon, optional title, text, optional actions.
alert-info(info): context, what stays as it is.alert-success(circle-check): only for “vN is live”, in any environment; the title names it (“v9 is live on Test”).alert-warning(triangle-alert): consequences people must see before they press the button (“Changing the brand…”). Inside the form, above the button, never aconfirm().alert-danger(circle-x): a failure, with Retry; request IDs incodewith a copy action.- Errors get
role="alert", the restrole="status". - For the state of the whole page, use Banner; for a reaction to what just happened, Toast.
Badge
Short status or label on a pill: 11 / 500, radius-full. Every badge carries a word, and a status badge also a mark, so meaning never rests on colour alone.
Tones and what they mean
b-neutral: Test, Draft, Previous, saved, counts that need no action.b-outline: exists but not current: Never published, Archived.b-accent: a process running (withmini-spin), Beta, selection.b-success: Live only, in every environment; the environment is said by the Dev pill or the PROD / TEST chip.b-warning: unsaved, missing, drift, Unpublished.b-danger: errors and failed publishes.b-solid: the Admin role.
Contrast
Each *-text on its *-soft fill passes AA in both themes (4.8:1 and up light, 6.0:1 and up dark).
Don't use a badge for the source of a landing (Constructor, Zip): that is an icon, see People and source.
Brand name
A brand is its 16 px favicon and its name (title.en from the brands package).
- The favicon comes from the brands package; while it loads or if it fails, the lettered tile (
.fav: first letter onbg-subtle) stands in. - The name truncates with an ellipsis and gets a tooltip in narrow cells.
fav-lg(20 px) where the brand is a field value.
Card
A surface for a group of facts or one piece of content: Live version, Note, Details.
card-head(title 14 / 600, one ghost action on the right),card-body16 px.- Facts as
kv(muted terms, 96 px column). - Cards don't nest and don't get coloured borders.
Centered card
One card in the middle of the screen: sign-in, No access, a fatal error.
- Product tile icon in a 56 px
accent-softdisc (cc-icon-dangerfor errors), title 22 / 700, one muted subtitle line. - One primary action, full width; a hint under it says where it goes.
- Errors as an Alert above the button. Busy: spinner and “Signing in…”.
Change block
“Was → will be” in confirm modals: what the action changes, before people press it.
- Two sides with a
arrow-rightbetween: label (12 / 500 muted), value, optional meta lines. - Value
change-v18 / 700 for versions;change-v-sm14 / 600 for dates and names. The target side isis-target(accent-text). - Removal:
change-offindanger-text(“Not on the site”). - Under it,
facts: Address (mono), Environment (env pill), Brand.
Checkbox
Native checkbox in accent, label to the right; the whole row is the hit area (check-row).
- Use for a yes/no that is applied with a form (Apply, Save). For a setting that applies at once, use Switch.
- A hint goes under the label inside the row.
- Off with the reason:
aria-disabled, row at 0.55 opacity, the reason as the hint and inaria-describedby. - Focus:
focus-ringon the box.
Combobox
A text field with suggestions from a list. Any typed value is kept: the list helps to type a known value, it doesn't limit the field. For a closed list (brand, language, time zone) use Select menu.
Anatomy
- Input:
.controlinside.combo,role="combobox",aria-autocomplete="list",aria-expanded,aria-controlson the list,aria-activedescendanton the focused option.monofor codes and keys. - Chevron:
combo-btn(btn btn-ghost btn-sm btn-icon),tabindex="-1",aria-label="Show suggestions"; opens the whole list. - List:
.popunder the input, as wide as the input,role="listbox"; options aremenu-itemwithrole="option", an optionaltrail(language, count) and the check on the current value, as in Select menu.pop-scrollfrom 8 options. - The typed part of each option is bold:
<mark>insidemenu-item, no fill. - Groups (optional):
menu-labelabove the group's options, as in Select menu and Multi-select. Each group isrole="group"witharia-labelledbyon its label; the label is not an option and never takes focus. A group with no label is the plain list: one group without a label behaves exactly like the ungrouped Combobox. An option belongs to one group only.
Behaviour
- Typing opens the list and filters it (substring, case-insensitive). The first match gets keyboard focus; Enter takes it.
- ↓ / ↑ move through options, Enter takes the focused one, Esc closes the list and keeps the text, Tab leaves with the text as typed.
- No matches: one
menu-emptyblock — what didn't match and “You can keep it as typed.” Enter keeps the text.
With groups
- Order: groups in the order given, options in their group's order. Put the most likely group first (the form's language, then “Other languages”).
- Filtering: a group with no matching option hides with its label; the groups that keep matches keep their labels, even if only one group is left, so it stays clear which group a match is from.
- An empty group is never shown, with or without filtering.
- ↓ / ↑ go through options across groups and skip the labels. The first match in group order gets keyboard focus; Enter takes it.
- Nothing matches in any group: one
menu-emptyfor the whole list, not one per group. - The chevron opens every group; the current value has its check inside its own group, and the list scrolls to it.
- Loading:
menu-emptywith a small spinner and “Loading …”; typing works meanwhile. - The list couldn't load: no list, the chevron is hidden; the field works as a plain input and a
hintline says what isn't checked. - Checking the value (found, not found) happens on blur or on picking an option, never on every keystroke. The result is one line under the field:
okwhen found,warnwhen the value is allowed but unknown,erronly when the value can't be used. See Text field for the order of lines.
Behaviour in code
The list is positioned like Select menu's: fixed to the input, at z-modal + 1, so it works inside modals and isn't cut by overflow.
Classes: combo, combo-btn, control, pop, pop-scroll, menu-item, menu-label, trail, check, menu-empty, plus mark inside menu-item.
Copy field
A value people copy: a landing address, a request ID, error details. Mono text, optional Open, then Copy.
- md 34 by default,
copy-sm28 in rows and error cards. - Copy: “Copy” → “Copied” with a check for 2 s, neutral (
bg-subtle);aria-labelchanges to “Address copied” so screen readers hear it. - Open (
external-link, new tab) only for addresses. Not on the site: the address is struck through intext-muted, Open isaria-disabledwith the reason intitle; Copy still works. - Long values truncate; the full value is in
title.
Dropzone
Where people drop or choose a file: a zip of a landing, a translations table, an image.
What the product provides
The title (what to drop), one hint line (type and limit), the Choose file button, the accepted types and the size limit.
States
- Empty: dashed
border-strong. Hover:accentborder. Keyboard focus:focus-ring(the zone is focusable and opens the file picker on Enter or Space). - Dragging over: 2 px dashed
accentonaccent-soft, title says “Drop to upload”. - Wrong type, too big: checked in the browser before anything is sent;
is-errorborder and an.errline under the zone naming the file and the rule. The zone stays ready for another file. - Off with the reason:
aria-disabled, the reason in the hint. drop-sm: one row, for panels and modals.
After a file is chosen, the zone gives way to a File row.
Classes: drop, drop-sm, drop-title, drop-hint, drop-actions, is-over, is-error.
Empty state
What a list or tab shows when it has nothing: an icon, a title, one sentence, one action (two at most).
- First use: what goes here and the primary action (“New landing”).
- Nothing matches: what didn't match and the way out (Clear filters, Include archived).
- Can't load: no red when the site isn't affected; Retry and the request ID with a copy button.
- Icon 40 px in
text-faint(decorative, on surface).
Env tag
Where something lives: the contour (INT, RU) and the environment. Always a word, never colour alone.
- Pill (
.env): contour + tag. Production:border-strong, tag onbg-subtle. Dev:warningborder,warning-softfill, tagwarning-textwithtriangle-alert. Optional host (env-host) on the sign-in screen; the header shows the host intitle. - Chip (
.env-chip): PROD (tag-prod, neutral) or TEST (tag-test, warning) before a published landing. - Yellow here means “not production”. The words differ by product on purpose: the admin runs as Dev, the constructor publishes to Test (the brands' dev stands).
File row
One chosen file: icon, name, size or what was found in it, and what you can do with it.
file-ic:packagefor archives,filefor tables and images.file-nametruncates; the full name intitle.file-meta(12, muted): size, rows, columns found.- Actions on the right, ghost sm: Replace or Change, Remove (
x,aria-label="Remove file"), Cancel while uploading. - Uploading: a Progress bar with its label inside the row.
- Failed:
is-error, the message under the name, Retry.
Header
App header, 56 px (header-height): product mark and name, then the product's own part, then the user button. Same component in both products; only the slots differ.
What the product provides
- Mark and name. One of two, never both:
- Tile (
productIcon): a lucide icon at 14 px (.ic-sm) on thelogo-marktile,accentfill,accent-contrasticon. Landings Admin:globe, “Landings Admin”. - Own mark (
productMark): the product's image (its favicon, as SVG orimg) inlogo-img, 26 × 26, no tile, no fill, same gap to the name. LP Constructor: its favicon, “LP Constructor”. The mark is decorative (aria-hidden); the name, oraria-labelon the link in compact mode, names the product. It must read onsurfacein both themes; the product checks that, the system doesn't recolour it.
- Tile (
- Compact (editor): the mark alone at the same 26 px, tile or own mark.
- Left slot. Admin: the contour and environment pill (
.env, see Env tag). Constructor: section nav (.nav, current linkaria-current="page"), or in the editor the tile alone (aria-labelon the link) followed by breadcrumbscrumbs crumbs-lgending in the project title. - Dev. Add
.hdr-dev: the 3 pxwarningstripe on top, and the tile (logo-mark) turnswarning, matching the product's dev favicon; the icon staysaccent-contrast(3.2:1 light, 8.6:1 dark). An own mark (logo-img) isn't recoloured. Only with the Dev pill: colour is never the only signal. - Right slot. The user button and its menu (User menu).
States
- Logo hover: text turns
accent-text, no underline. Focus:focus-ringaround the whole logo. - Nav link hover:
bg-subtle; current:accent-soft/accent-text. - User button hover and open:
bg-subtle; focus:focus-ring.
Don't
- Don't underline the logo or nav on hover.
- Don't put an own mark on the accent tile or restyle
logo-markin the product: an image goes inproductMark. - Don't show people by name: the button shows the email, the name goes in
title.
Classes: hdr, hdr-dev, hdr-left, hdr-right, logo, logo-mark, logo-img, nav, user-btn, user-name, avatar.
Modal
Short decisions and small forms. Long or losable work (a new landing with an upload) is a page.
Sizes
modal-sm 440 (simple confirm), modal-md 560 (confirm with “what changes”, forms), modal-lg 800 (tables, history). A product may set another width for one window (the schedule event, 600).
Anatomy
Head: title (16 / 600), optional subtitle (modal-sub), close ×. Body: 20 px padding, 16 px gaps. Footer: Cancel is btn-secondary, the primary names the action (“Make v4 live”, “Unpublish”). The start slot (.start) holds a note or a third action (“Leave without saving”). A destructive third action there (“Delete event…”) is btn-danger, never a ghost recoloured red.
Kinds
- Changes the live site:
md, the change block, the facts (Address, Environment), primary button. - Dangerous now (prod Unpublish): danger alert, typed slug,
btn-dangerstaysaria-disableduntil it matches exactly. - Doesn't touch the site (Archive, Restore):
sm, one paragraph. - Progress: no ×, Esc and the overlay don't close it; only “Run in background”.
Behaviour
Focus goes to the first field or the primary button and stays inside; Esc and the overlay close it unless it is a progress window. In code the overlay gets .is-fixed.
Multi-select filter
Filter a list by several values: brands, type, status. A filter button opens a list of checkboxes.
- Button:
btn-secondary. With one value it names it (“Status: Error”), with one whole group it names the group (“Brand: All RU”), otherwise acount-pill; all threeis-active. - List:
aria-multiselectable, each row alabel.menu-itemwith a checkbox, optional favicon, and a count on the right. - Applies as you tick: no Apply button. Footer: Clear on the left, “Applies as you tick” on the right. The selection goes to the URL.
- Groups with
menu-label; group state or a caveat in onemenu-hintline (“Counts are at least…”).
Whole group
Headings stay labels and are never clickable. A group that people pick as a whole (brands of one market) gets a whole-group row first under its heading, the same pattern as “All INT brands” in Select menu.
- Row:
menu-itemwith a checkbox,layersicon in the favicon column (only when the group's options have favicons), “All INT brands”, and the sum of the group's counts (“≥” if any count is “at least”). - Checkbox: empty when nothing in the group is ticked, mixed (native
indeterminate,aria-checked="mixed") when part is, checked when all are.aria-selected="true"only when all are. - Click or Space: from empty or mixed ticks every option of the group, from checked clears them. Options still tick one by one; the row follows them.
- Button: exactly one whole group and nothing else → “Brand: All INT”. Whole group plus anything else → the count.
- Opt-in per group. Groups without a heading (“No brand”) and filters where nobody picks a whole group don't get the row.
Page
Layout of a list page under the header: page head, toolbar, content. Both products' lists (Landings, Projects, Templates) use it.
.page-head:page-title(22 / 700) with an optionalpage-count(muted, tabular), one primary action on the right..toolbar: search (with-icon, 280 px), filter buttons (see Multi-select), switches, then a ghost Clear filters. Wraps at narrow widths..content: the table or empty state.- Gutter 32 px (
space-8) everywhere on the page, banners included.
Filters, sort and page go to the URL.
Fill
page is-fill for a list that can be long (Projects, Templates, Landings). The page takes the window under the header and banners, and only the table scrolls: the page head, toolbar and table head stay, the footer with the count and pager stays under the rows.
.page.is-fillis a direct child of.app; the header and page banners are its siblings and keep their height..appis the window's height (100dvh) and doesn't scroll. Below 560 px of window it keeps 560 px and the window scrolls as usual..contentis a column; thetbl-boxis as tall as its rows but never taller than the room left, 32 px (space-8) above the window's bottom. Onlytbl-scrollscrolls; the table head sticks to its top.- Empty states, errors and the first load keep their own height, as on a plain page.
- Pages that aren't lists (a landing, settings) don't use it: they scroll as a whole.
Classes: page, is-fill, page-head, page-title, page-count, toolbar, content.
People and source
Who owns what, and where a landing came from.
- People by email, the name in
title. Your own rows get.you. - Avatar: initials on
accent-soft, or the photo; 32 px,avatar-sm24. - Role: Admin
b-solid, Publisherb-neutral, No publishing groupb-outline. Only in the user menu, never next to other people. - View only (
.readonly): lock, “View only · owned by”, in place of the action buttons. Neutral on purpose. - Owner note: one line with a lock under a row that someone else owns.
- Source: not a status, so no pill.
src-mark(icon witharia-label) after the slug in lists;src(icon + word) in versions and activity: Constructor (blocks), Zip (package).
Progress
A thin bar for work whose length is known, or for waiting on the server.
- Determinate:
progresswith the bar's width;role="progressbar"witharia-valuenow,aria-valuemin,aria-valuemax. Aprogress-labelunder it: bytes on the left, percent on the right; or a count in the line above (“Loading hotel 3 of 5…”). - Indeterminate (
progress-ind): the length is unknown; say what is happening under it. - Countdown (
progress-countdown, duration in--countdown): before a result closes by itself; neutraltext-faint, because it is a timer, not a status. Offer “Keep open”. - The bar is
accent; the trackbg-subtle(borderin dark). - On a design canvas, add
.is-still(fromcanvas.css) to freeze the animation on a fixed frame (--still), so a countdown never shows an empty bar.
Result report
What a check or an import found: an archive before publishing, a translations table, a hotels list. One row per finding, the worst first.
Rows
c-ok(check,text-muted): passed. Neutral, not green: green is for Live.c-info(info,accent): done for you (“Unwrapped folder …”, “Row 5 skipped”).c-warn(triangle-alert,warning): worth a look, doesn't block.c-err(circle-x,danger): blocks the next step; say what to do and repeat that it blocks in an.errline under the report.
Parts
report-head(optional): what was checked and a count on the right (“1 warning”). In a modal the summary can be a line above the report instead.report-rowwithreport-text; a bold first phrase says the finding, the rest explains.paths: file locations in mono, the location muted, the bad part inmark.
Search
Text field with a search icon and a clear button once there is a query.
- Icon
searchinside the left padding (with-icon). The label is visually hidden (sr-only), the placeholder says what is searched: “Search slug, title or note”. - Clear: ghost sm icon button,
aria-label="Clear search", returns focus to the field. Esc clears as well. - The query goes to the URL with the other filters; results update 300 ms after the last keystroke.
- No results: the list shows the No results empty state, not an error.
Segmented
The one way to pick one of two to four short options: Environment, Visibility, Theme, alignment, device.
- Each option is a button with
aria-pressed; the group hasrole="group"and a label. - Sizes: md 30 inside a 34 frame,
seg-sm26,seg-iconsquare (each button needsaria-label). - A locked option stays visible: lock icon,
aria-disabled, the reason in a tooltip andaria-describedby. The default selection never lands on a locked option. - Options that need a sentence each (Create project: Landing or Email) use radio cards in the product, not this.
Skeleton
Grey shapes in place of content while the first load runs, so the layout doesn't jump.
- Same row height as the real rows (
table-row-height52), shapes at the real widths of text, badges and buttons. - Pulses (1.4 s); slower with reduced motion.
aria-hiddenon the shapes and onerole="status"line, “Loading landings”, for screen readers.- On
bg-subtleusesk-on-subtle. - Reloads keep the old content with a small spinner instead of flashing a skeleton.
Spinner
Something is running and its length is unknown.
spinnersm 16 (in buttons and hints), md 24 (inline loading), lg 40 (whole screen).- On an accent fill:
spinner-inverse. In badges:mini-spin. In banners and step icons:spin16(takes the text colour). - Pair it with words: “Saving…”, “Loading options…”. A button keeps its width while busy.
Status sets
The agreed status vocabulary, built from Badge. Each status has its own mark, so the set reads in greyscale.
Landing (Landings Admin)
Live — b-success + dot (any environment) · Draft — b-neutral + hollow dot · Publishing — b-accent + spinner · Error — b-danger + circle-x · Unpublished — b-warning + eye-off · Archived — b-outline + archive, next to the status, not instead of it.
Live is green in every environment. The Dev pill in the header or the PROD / TEST chip in the row says where it is live; the badge doesn't repeat it.
Version
Live · Previous (neutral) · Never published (outline) · Publishing (accent, on the version being switched to).
Publish button (LP Constructor toolbar)
No badge while loading or when not published · Live on test (success) · Live on production (success; wins when both) · Publishing · Unpublished (warning + eye-off) · Publishing failed (danger + circle-x, same as Error in the admin).
Published list
env-chip (PROD neutral, TEST warning), brand favicon and slug, status with the live version: “Live · v3”.
Steps
The stages of a long operation (publish, unpublish), or numbered instructions.
Operation
is-wait: empty ring, muted title.is-run:accent-softwith a spinner, a live timer;aria-current="step".is-done: check onbg-subtle. Neutral: the green comes with the result, and only when it is Live.is-fail:danger-softwithx, title indanger-text, the error under it (step-err) and what it means for the site. The number of the failed step goes in its meta (“Step 3 of 3”) and in the banner or toast that follows.- Time on the right, tabular.
The steps sit in a progress modal without a close button; “Run in background” moves them to a banner.
Numbered
is-num: the number on accent-soft, for instructions (“How translations work”).
Switch
On/off that applies at once: Show archived, Only mine, Show on mobile. role="switch" on a native checkbox.
- Track off
text-faint(4.7:1 on surface), onaccent; thumbtext-inverse. - Label to the right, 13 / 500, in the same
labelso the whole row toggles. - Focus:
focus-ringon the track. - Off with the reason:
aria-disabled, 0.55 opacity, a tooltip with the reason. - For values applied with a form, use Checkbox.
Table
Lists of landings, projects, templates, versions. Head 36 (table-head-height), rows 52 (table-row-height), in a tbl-box card.
Head
Sticky to the top of tbl-scroll, so it only sticks when the table scrolls inside: on a page is-fill (see Page). On a plain page the whole page scrolls and the head goes with it. Sortable columns are buttons in the header (th-sort) with aria-sort on the th and an arrow; no separate sort menu. Sorted column in text, others text-muted.
Rows
- A row that opens something is a link (
is-link, the main cell is a real<a>); hoversurface-hover. - Main cell:
cell-main(500) andcell-sub(12, muted), both truncate with a tooltip; the table never wraps. - Archived:
is-archived,bg-subtle, slug muted. - Live version:
is-current, green tint in every environment; the version cell or the environment chip says where. - Group heading:
tr.grpwith one cell spanning all columns (Upcoming, Past 30 days); 32 px,bg, not clickable. - Row actions: ghost sm icon buttons, right; an action that doesn't apply stays visible with
aria-disabledand a tooltip. - First load: skeleton rows of the same height.
Footer
On a page is-fill the footer stays under the visible rows; only the rows scroll.
Count on the left (“51–100 of 128”), pager on the right when the list pages (50 per page). Page, sort and filters go to the URL.
Narrow screens
Below the table's minimum width it scrolls inside tbl-scroll; the page doesn't. The minimum is never less than the fixed columns plus 120 px for each column without a width (minWidth on the column changes it), so a flexible column such as Comment or Note never collapses to nothing.
Tabs
Switch between views of one object: Versions, Activity, Schedule on a landing.
role="tablist", each tabrole="tab"witharia-selected; arrows move between tabs.- Selected:
textwith a 2 pxaccentline. Counts in a neutral badge; a count of upcoming things in accent. - A tab that doesn't apply yet stays,
aria-disabled, with the reason in a tooltip. - The tab goes to the URL.
Text field
Label, control, then one line under it: a hint, an error or a check. Inputs, textareas and select buttons share .control.
What the consumer provides
label (always; never placeholder-only), optional hint, required or optional mark, the error text, mono for slugs, URLs and keys.
States
- Default:
border-strongonsurface; placeholdertext-faint(only on surface). - Focus:
accentborder +focus-ring. - Error:
invalid(dangerborder,danger-softfill),aria-invalid, message under the field in.errwithcircle-x. The placeholder turnstext-mutedinside an invalid field:text-faintondanger-softis 4.1:1. Focus of an invalid field usesfocus-ring-danger. - Off with the reason:
aria-disabled="true"+readonly,bg-subtle, and a hint that says why and where to change it. - Check passed:
.ok, neutral with a check. Green is for Live only. - Warning:
.warn,warning-textwithtriangle-alert. The value is allowed but probably wrong (no such campaign, an unusual value); saving and publishing go on. The control keeps its normal border and fill: red edge and fill mean “can't go on”, and a yellow edge would read as the same thing. Notaria-invalid; the line is linked witharia-describedby. Contrast:warning-textonsurface5.5:1 light, 9.3:1 dark; onbg-subtle4.7:1 light. - Checking: hint with a small spinner.
One line under the control
Only one line shows, in this order: error, warning, check passed, hint. An error replaces a warning; a warning replaces the check and the hint. If the hint says something people need while they fix the value, put it into the error or warning text.
Rules
Required mark is danger-text (the danger hue is 4.4:1 in light). Errors appear after blur or submit, not while typing, except format errors in slug fields, which react on the fly. Counter right of the label in .label-row.
Warnings follow the same timing as errors: after blur, after picking a suggestion, or when the form opens with a saved value.
Classes: field, label, opt, required, control, control-sm, mono, textarea, select-btn, invalid, hint, err, warn, ok, label-row, counter.
Toast
Confirms what just happened, bottom right (off a side panel when one is open), then goes. 380 px, stack of up to three, newest at the bottom.
Timing
- Status toasts close after 6 s; the timer pauses while the pointer is over the stack or focus is inside it.
- A toast with Undo stays 10 s.
- Errors stay until closed.
Anatomy
Icon · title (600) · optional description (12, text-muted) · optional one action (btn-secondary btn-sm) · the close button, always.
Tones (icon colour)
t-success: only “vN is live on Production” or “… on Test”.t-neutral: saved, deleted, copied.t-info: background work finished (cache purged).t-warning,t-danger: problems; danger toasts getrole="alert", the restrole="status"(polite).
Position
Bottom right, 24 px (space-6) from the window's edges. When a side panel covers the right edge (the editor's properties panel, 320 px), the product moves the stack off it with useToastInsetEnd(width) while the panel is open: the stack keeps its 24 px from the panel, not from the window. The hook resets to the window edge when the panel closes or unmounts.
- The stack slides over
duration-base; no slide with reduced motion. Toasts don't re-mount, their timers keep running. - In CSS it is
--toast-inset-endon.toasts(the panel's width; 0 by default).
A lasting state is a Banner, not a toast.
Tooltip
Short text on hover and on keyboard focus: the name of an icon button, the full text of a truncated cell, the absolute time behind “2 h ago”, why something is off.
- 400 ms delay, shows on focus too, hides on Esc.
role="tooltip"linked witharia-describedby(or the button'saria-labelfor icon names). textground,surfacetext, 12 px, up to 320 px wide.- Under the element by default, above near the bottom edge.
- Never the only place for something people must read to finish a task.