FollowQue

The FollowQue Design System

One visual language for every screen of the Facebook sales workspace. Every token below is already implemented in src/styles.css and every component ships from @/components/fq/primitives, so engineering implements by composing — never by restyling.

01

Design philosophy

FollowQue sits three inches from a live customer conversation, eight hours a day. The interface earns its place by staying quiet and being instantly legible in peripheral vision.

Five principles

  1. 1. Quiet by default. Neutral surfaces, hairline borders, one shadow layer. Color is information, not decoration.
  2. 2. Emphasis is rationed. Only five moments may raise their voice: overdue follow-up, hot lead, AI generating, lead captured, GHL synced.
  3. 3. Native rhythm, not mimicry. Messenger's density and calm — our own palette and typography.
  4. 4. One glance, one answer. Every card resolves a single question: who needs me, what do I say, when do I return.
  5. 5. Fast beats fancy. Motion confirms, never performs. Nothing exceeds 260ms.

Emphasis budget

Overdue follow-upRed border + red count
Hot leadWarm status dot + badge
AI generatingIndeterminate bar
Lead capturedSuccess pop + toast
GHL syncedSuccess card + toast

Why: A fixed emphasis budget prevents the slow drift where every team adds "just one more highlight" until nothing stands out. Anything not on this list renders in neutral tones.

Do

  • Let whitespace and type weight create hierarchy
  • Use one accent per screen region
  • Keep the panel scannable at arm's length

Don't

  • Gradients, glows, or neon accents
  • Colored backgrounds for ordinary rows
  • Robot/AI iconography or sci-fi language

02

Color palette

A warm-neutral base carries 95% of the pixels. Six semantic accents carry meaning, and each one has exactly one job.

Neutral foundation

Background--backgroundPanel canvas
Card--cardRaised surfaces
Secondary--secondaryChips, hover, tracks
Border--borderHairline separation
Foreground--foregroundPrimary text
Muted foreground--muted-foregroundMeta text
Primary--primaryDeep teal — one CTA per view
Accent--accentQuiet AI/system tint

Why: The canvas is warm off-white rather than pure white so the panel reads as a distinct surface next to Messenger's cool white without competing with it. Teal is the only brand accent — it is far enough from Facebook blue to never be mistaken for a Facebook control.

Semantic accents — one meaning each

Success--successCaptured, synced, won
Danger--dangerOverdue, failed, destructive
Info--infoSystem explanation, AI notes
Warning--warningCaution, expiring, review
Hot--hotLead temperature: hot
Warm--warmLead temperature: warm
Cold--coldLead temperature: cold
Waiting--waitingAwaiting their reply

Why: Each accent ships with a -soft pair used for fills, so a status never relies on a saturated background behind text. Meaning is also duplicated in a label and a dot shape, so the system survives color blindness and grayscale screenshots.

03

Typography scale

Plus Jakarta Sans for everything human-readable; JetBrains Mono for counts, timestamps, and micro-labels. Two families, six sizes, three weights.

The quick follow-up wins

Display / 22px / 700Panel title, empty-state headline

The quick follow-up wins

Title / 16px / 700Screen heading, modal title

The quick follow-up wins

Body strong / 13px / 600Lead name, row title

The quick follow-up wins

Body / 13px / 400Message text, descriptions

The quick follow-up wins

Meta / 11.5px / 400Timestamps, helper text

The quick follow-up wins

Micro-label / 10px mono / 500Section eyebrows, counts

Why: A 400px panel cannot afford a ten-step scale — steps that close would read as accidents. Six sizes with a −0.011em global tracking keep dense rows legible while letting weight, not size, carry most hierarchy. Mono micro-labels make section headers recede instantly to peripheral vision.

04

Spacing & radius

A 4px base grid. Inside the panel we mostly use 6 / 10 / 14 — Messenger-like density that never feels cramped.

Spacing scale

2pxIcon-to-dot nudges
6pxWithin a row (icon ↔ label)
10pxBetween stacked elements in a card
14pxCard padding, screen gutters
20pxBetween card groups
32pxSection break in long views

Radius scale

6pxCheckbox
8pxIcon button, chip
12pxInput, button, list row
16pxCard, modal
999pxBadge, avatar, dot

Why: Radius encodes size, not style: the bigger the surface, the rounder the corner. That single rule keeps nested elements visually concentric and stops components from looking like they came from different kits.

05

Elevation

Three levels. Ever. Shadows are cool-tinted and low-opacity so they read as depth, not as a drop shadow effect.

Level 1 — card

--shadow-card

Resting cards, list containers

Level 2 — lift

--shadow-lift

Hover, primary button, toast

Level 3 — panel

--shadow-panel

Modal, the panel itself

Why: Borders do the separating; shadows only signal interactivity and layering. Limiting elevation to three steps means a raised element always means something actionable is happening — which is why a hover lift of a single pixel is enough feedback.

06

Buttons

One primary action per view. Everything else is secondary or ghost. Danger is reserved for irreversible loss.

Why: Medium (40px) is the default because it clears the 40px comfortable-touch threshold on trackpads and dense rows alike, while large (48px) is reserved for the single commit action at the bottom of a flow — the height itself tells the user "this is the one that matters."

07

Inputs, search, dropdowns & choices

Field labels are always visible, never placeholders-as-labels. Focus is a 2px teal ring with a matching border.

Text, search, select

Choices

Why: Checkbox = multiple, radio = one of a set, toggle = an instant setting with no save step. Keeping those three meanings strict removes the moment of hesitation where a user wonders whether a change already took effect.

08

Status system, badges & tags

Lead temperature is the product's core vocabulary. It always renders as dot + word, in the same five colors, everywhere.

HotWarmColdWaitingWon
AgencyPricing objectionFollow-up · Thu 9:00GHL: synced

Why: Badges carry state the user cannot change by looking; chips carry facts and filters. Because temperature colors never appear on anything else, a single glance down the queue communicates the day's shape without reading a word.

09

Alerts, toasts, modals & empty states

Feedback escalates in exactly three steps: inline alert (contextual), toast (transient confirmation), modal (blocks until decided).

Alerts

AI used the last 12 messages

Nothing leaves your browser until you press send.

Synced to GoHighLevel

Maya is now in Pipeline · Qualified.

Follow-up due in 30 minutes

Devon asked for pricing on Tuesday.

Sync failed

GoHighLevel rejected the contact — duplicate phone number.

Toast & modal

Lead captured

Toasts sit above the tab bar, live 2.4s, and never carry an action the user must take.

Empty state

No follow-ups due today

Today is clear. Capture the prospect in your open Messenger thread and it will be waiting for you tomorrow.

Why: Empty states always name the next action, because in a sales tool "nothing here" is either a win worth confirming or a gap worth filling — never a dead end.

09a

Empty states

Every empty surface in FollowQue is authored, not defaulted. Copy lives in one registry (src/lib/empty-states.ts) and renders through <Empty spec={…} />, so the same situation reads identically everywhere.

Follow-up queue · first run

No leads captured yet

Open a Facebook conversation and capture the prospect. FollowQue keeps the context so you never re-read a thread to remember where you left off.

Queue · Today bucket

No follow-ups due today

Today is clear. Capture the prospect in your open Messenger thread and it will be waiting for you tomorrow.

Today · nothing needs attention

You're all caught up

Nothing overdue, nothing hot waiting on you. This is what a worked pipeline looks like — check back after your next conversation.

Today / Conversation · no Facebook thread

No conversation selected

Open a Messenger thread in this tab and FollowQue reads the prospect's context automatically. Or pick someone you've already saved.

AI replies · thread too short

No AI suggestions available yet

This thread has fewer than three messages, so a draft would be guesswork. Add a note about what they want and suggestions unlock.

Sync · GoHighLevel not connected

GoHighLevel isn't connected

Connect your location once and every lead you capture lands in your pipeline with the conversation summary attached — no retyping.

Any search field

No matches for that search

Try a first name, a business, or a shorter phrase. Search looks at names, businesses and the notes you've saved.

Today / Queue · Overdue bucket

No overdue follow-ups

Every promise you made has been kept. Keep the streak by setting a date the moment you capture someone.

Today · Hot leads

No hot leads right now

Mark a conversation Hot when a prospect asks about pricing or a demo — they'll surface at the top of Today.

Queue · Upcoming bucket

Nothing scheduled ahead

Set a follow-up date when you capture a lead and it will appear here, sorted by the day you promised.

Queue · Waiting for reply

No threads waiting

Every conversation has the ball in your court. Nothing here means nobody is left hanging.

Queue · Completed

Nothing completed yet

Finished follow-ups land here so you can see the week's work in one place.

Why: Four rules, no exceptions. (1) Name the situation in plain language — never "Error" or "Nothing found". (2) Teach the next move in one sentence that also says why the feature is worth using. (3) Exactly one CTA, and only when there is a real next step. (4) A cleared queue is a win, not a void: those states use the muted success chip instead of the neutral grey one, so "no overdue follow-ups" feels earned rather than blank.

09b

GoHighLevel sync preview

One confidence block shown directly above the sync button. It answers two questions — is this someone I already have, and what is about to change — and nothing else.

Existing contact matched

Existing contact

Maya Okafor · Okafor Creative
  • Update phone
  • Update notes
  • Add Facebook profile
  • Create follow-up task

Matched on email. Existing details stay — nothing is overwritten without a new value.

No match found

New contact

This will create a new GoHighLevel contact.

  • Add contact details
  • Add notes
  • Add Facebook profile
  • Create follow-up task

Do

  • State New contact or Existing contact in one line, before anything else
  • Name the matched person and company so recognition is instant
  • List outcomes as plain verbs — Update phone, Add Facebook profile
  • Keep the block quiet: no status color, no accent except the check marks

Don't

  • Show field-by-field mapping, merge tools, or conflict resolution
  • Expose duplicate management or sync configuration here
  • Make the user decide anything before pressing Sync
  • Use alarm colors — a match is normal, not a warning

Why: Sync is the moment users fear most: they are writing into their CRM from a browser panel. Showing recognition ("this is Maya at Okafor Creative") plus a four-line outcome list converts that fear into a one-second read. Because the preview only describes consequences and never asks for configuration, confidence goes up while the interface stays smaller, not larger.

09c

Progressive feature discovery

FollowQue teaches itself in the moment a feature becomes useful, then never mentions it again. Every teaching moment lives in one registry (src/lib/discovery.ts) and renders through five components, so nobody can add a sixth kind of interruption.

Hint

One calm line under the thing it explains. No card, no icon shouting. Use for a control the user can already see but might not notice.

Tap any date to move a follow-up.

Coach mark

Small anchored card with an arrow, sitting beside a control. Never dims the screen, never chains into a tour. One per screen, ever.

Narrow the list to just what's due

Tooltip

On demand only — hover, focus, or long-press. It never appears by itself, so it can never interrupt.

GoHighLevel is connected

First-use card

Shown the first time a screen does real work, to set expectations before the user reacts to the output.

These are starting points, not sends.

Celebration

Quiet acknowledgement of a first milestone, at the size of an alert. No confetti, no full screen, no sound.

Your first contact is in GoHighLevel.

Governance — enforced by DiscoveryProvider

  • One at a time. Never two teaching surfaces on screen together. The lower priority waits.
  • Two per session. After two, the rest wait for another day. Nobody is taught six things at once.
  • Once is once. Seen, dismissed, or acted on — it never returns. No 'tips' setting to police.
  • Doing beats reading. Use the feature and its lesson retires silently, unshown.
  • Dismissable in one tap. Every hint and coach mark has a close affordance ≥ 24px, and Escape closes it.
  • Never blocks. No overlays, no dimming, no 'next' chains. The screen stays usable underneath.
  • Quiet celebration. Only the five emphasis-budget moments celebrate, once, at the size of an alert.
  • Guidance is a preference. Full, Low or Off. Low teaches each major workflow once, then goes quiet for it.
  • Completion, not dismissal. Closing a hint hides it; only finishing the action marks a workflow learned.

The registry

KindSurfaceFires whenCopy
coachFollow-up queue · filter rowSecond visit to the queue, once there are more than four leadsNarrow the list to just what's due
hintFollow-up queue · under the first rowUser has opened the queue but never changed a follow-up dateTap any date to move a follow-up.
hintToday · waiting for replyA thread has been quiet for seven daysFollowQue nudges you when a thread goes quiet for 7 days.
firstUseReply drafts · above the first draftFirst time drafts are generatedThese are starting points, not sends.
firstUseSync to GoHighLevel · above the match previewFirst time the sync preview is shownWe check for an existing contact first.
celebrationCapture · after savingFirst lead ever savedFirst lead saved.
celebrationQueue · after the first date changeFirst follow-up date the user sets themselvesThat one won't slip.
celebrationSync to GoHighLevel · after the first successful saveFirst contact saved to GoHighLevelYour first contact is in GoHighLevel.
tooltipHeader · GoHighLevel status dotHover, focus, or long-press — never automaticGoHighLevel is connected

Do

  • Teach one thing, on the screen where it already matters
  • Retire a lesson silently the moment the user does the thing
  • Keep every hint closable in one tap, and with Escape
  • Celebrate only the five emphasis-budget moments, once each

Don't

  • Chain coach marks into a product tour
  • Dim the screen or block the control being explained
  • Show a hint while a screen is still loading in
  • Re-teach on a later session because engagement dipped

Why: Discovery is charged against the same emphasis budget as everything else: at most one teaching surface on screen, at most two per session, and never a second time. A hint that returns is an interruption; a hint that appears once, in the right moment, reads as the product being helpful. Because relevance is declared by the screen and the budget is enforced centrally, no single feature team can spend everyone else\'s attention.

10

Loading & progress

Five named loading states, one per surface, all in src/components/fq/loading.tsx. Skeletons mirror the shape of the content that is coming; bars carry determinate work; a spinner survives in exactly one place.

Loading Messenger — <MessengerLoading />

Loading AI — <AiLoading />

Loading follow-ups — <FollowUpsLoading rows={2} />

Loading CRM match — <CrmMatchLoading />

Loading search — <SearchLoading rows={3} />

Progress

Syncing 32 of 50 contacts64%

Indeterminate — AI drafting

Why: Rules. (1) Skeleton whenever we know the shape; a spinner only where the user pressed a button and is waiting on someone else's system — saving to GoHighLevel. (2) The skeleton copies the real layout: same padding, radii and line count, so nothing jumps when data lands. That absence of reflow is what actually reads as "fast". (3) One quiet mono caption per surface in plain language ("Checking if you already have them"), announced with role="status" so screen readers hear it once. (4) Timing via useLoading(): nothing renders for the first 120ms, so quick loads stay invisible, and once shown a skeleton holds for at least 320ms so it never flickers. (5) Loading is never an emphasis moment — no accent colour, no motion beyond the 1.4s shimmer, which pauses under prefers-reduced-motion.

11

Cards, lists & tables

Four card archetypes cover the whole product. Each one is the same shell — 16px radius, hairline border, 14px padding — differing only by content and, when warranted, a single tone.

Lead card

Queue card — overdue (emphasis moment)

Devon Ricci · 2 days overdue

Promised a pricing breakdown on Tuesday.

AI card

Consultative draft

"Happy to walk through the 40-lead setup — is the bottleneck volume, or keeping up with replies once they land?"

Success card

In your pipeline

Maya Okafor · Qualified

List rows & tables

Why: Lists are the table for a 400px panel: the same data, stacked, with the status column pinned right. Full tables appear only in the web dashboard, where they inherit these exact row heights (44px), hairline dividers, and right-aligned status.

13

Motion rules

Motion exists to explain a change of state. If a user notices the animation itself, it is too much.

Duration & easing tokens

--dur-fast120ms · hover, press, focus
--dur-base180ms · view change, reveal
--dur-slow260ms · success confirmation
--ease-standardcubic-bezier(0.22, 1, 0.36, 1)
fq-rise6px up + fade — entering content
fq-popscale 0.82→1 — success only
fq-shimmer1.4s loop — skeletons

Rules

  • · Hover lifts 1px and brightens; never scales up.
  • · Press scales to 0.985 — a physical confirmation with no travel.
  • · Views enter with fq-rise; they never slide horizontally.
  • · fq-pop is licensed for exactly two events: lead captured, GHL synced.
  • · Nothing loops except skeletons and the indeterminate AI bar.
  • · All motion respects prefers-reduced-motion by falling back to opacity.

Why: A single decelerating curve across the whole product makes unrelated components feel mechanically related. Capping everything at 260ms keeps the panel feeling instant during rapid-fire conversation switching, where any transition longer than a blink becomes an obstacle.

14

Icons, illustration & avatars

Lucide, 1.9px stroke, 15–17px in chrome and 13–14px inline. Outline only, never filled, never colored except to mark a status. Every lead status carries one fixed glyph so temperature survives greyscale and color blindness.

Icon set

Why: Active tabs thicken the stroke to 2.4px rather than switching to a filled variant — weight change reads as emphasis without introducing a second icon vocabulary. There is no robot, brain, or spark-burst imagery: AI is signalled by one small sparkle in a muted teal tile, and nothing else.

Status glyphs

HotWarmColdWaitingWon
HotWarmColdWaitingWon

Why: One fixed glyph per status, defined once in src/lib/status-icons.ts and used on every surface: Flame for Hot, a rising line for Warm, Snowflake for Cold, Clock for Waiting, a circled check for Won. The second row is the same badges in greyscale — five distinct silhouettes, so nothing depends on colour vision. The glyph is decorative; the word next to it is what a screen reader announces.

Avatars

Why: Initials on a neutral tile with a hairline ring, plus the status glyph in the corner — the same trick a messaging app uses for presence, repurposed for temperature. Photos are optional and cropped to the same circle, so a row's rhythm never changes based on whether an avatar loaded.

15

Accessibility rules

Non-negotiable. These are acceptance criteria, not aspirations: focus-ring behaviour, keyboard navigation per surface, and the contrast pairing every component must clear — measured live from the tokens currently on screen.

Requirements

  • · Body text ≥ 4.5:1 contrast; meta text ≥ 4.5:1 (muted-foreground is tuned to pass, not to look faint).
  • · Minimum interactive target 32×32px; primary targets 40×40px.
  • · Every control is reachable by Tab in visual order; the panel traps focus inside open modals.
  • · Focus ring: 2px teal at 60% + matching border, never removed, visible on both themes.
  • · Status is never color-only — always a dot plus a word.
  • · Icon-only buttons carry an aria-label; toggles expose checked state.
  • · Full parity in dark mode, which raises accent lightness rather than reusing light values.

Implementation tokens

focus ringring-2 ring-ring/60 + border-ring
min targeth-8 (32px) / h-10 (40px)
hit padding≥ 8px between adjacent targets
text base13px / 1.5 line-height
tracking-0.011em body, -0.02em headings

Why: Contrast and target size are the two failures that make an all-day tool tiring rather than unusable, and tiredness is what makes a rep stop opening the panel. Treating them as tokens means a compliant component is the easiest one to build.

15a

Focus ring behaviour

One ring, one recipe, every interactive element. Focus is the only affordance a keyboard user has, so it is never removed, never restyled per component, and never softened to look tidier.

Live sample

Tab through the samples below — the ring appears on keyboard focus only (:focus-visible), never on pointer press.

Inline link

Why: Using :focus-visible rather than :focus means mouse users never see a ring after a click, so the panel stays quiet — while keyboard users always do.

The recipe

ringring-2 ring-ring/60
offsetring-offset-2 ring-offset-background
inputsring-2 ring-ring/60 + border-ring
trigger:focus-visible only
transitionbox-shadow 120ms var(--ease-standard)

Rules

  • · Never outline: none without a replacement ring in the same rule.
  • · The ring sits outside the element (offset 2px) so it stays legible on cards, tinted rows and dark mode alike.
  • · On inputs the border also switches to --ring so the whole control reads as focused, not just its halo.
  • · Rings never animate in size or colour — only opacity/box-shadow, 120ms.
  • · Focus is visible inside scrollable rails: scroll the focused chip into view rather than clipping it.
  • · Composite widgets (nav bar, tabs, menus) expose one tab stop; arrows move within.

Focus movement on state change

Destructive-free actions

  • · Complete / Snooze: focus moves to the next row in the list, not to the top.
  • · If the list empties, focus lands on the empty state heading.

Layers

  • · Opening a modal or menu moves focus in; closing returns it to the trigger.
  • · Esc always exits the topmost layer only.

Async

  • · Generating replies keeps focus on the button; results are announced via aria-live=polite.
  • · Sync success/failure never steals focus — the Retry button is the next tab stop.

15b

Keyboard navigation rules

A rep working the queue should never need the mouse. Every surface below has a defined key map; anything not listed falls back to plain Tab and Enter.

Key map by surface

Panel shell

Tab / Shift+Tab
Moves through header → view content → bottom nav, in visual order.
Esc
Closes the topmost transient layer: menu, then modal. Never closes the panel itself.

Bottom navigation

Tab
Enters the bar once; the active item receives focus first.
← / →
Moves between destinations without activating (roving tabindex).
Enter / Space
Switches view. Focus lands on the new view's heading.

Tabs (segmented) & filter rail

← / →
Selects the previous/next tab immediately — content follows selection.
Home / End
Jumps to first/last tab.
Tab
Leaves the group and enters the tab panel.

Lead / follow-up cards

Enter
Opens the conversation — the card's single default action.
Tab
Steps into the row's own actions (Complete, Snooze) after the card itself.

Snooze menu

Enter / Space / ↓
Opens the menu and focuses the first preset.
↑ / ↓
Moves between 1 day / 3 days / 1 week.
Enter
Applies the preset, closes the menu, returns focus to the trigger.
Esc
Closes without changing the follow-up date.

Modal / sheet

Open
Focus moves to the modal heading; background is inert.
Tab
Cycles inside the modal only — focus is trapped.
Esc
Dismisses and restores focus to the element that opened it.

Toast with Undo

Tab
Reaches Undo without dismissing the toast; hovering or focusing pauses the timer.
Enter
Reverts the action; focus returns to the list position it came from.

Fields, selects, toggles

Space
Toggles a checkbox or switch.
← / →
Moves the RadioGroup selection.
Enter
Submits the surrounding form's single primary action.

Global principles

Always true

  • · DOM order equals visual order — no positive tabIndex anywhere in the panel.
  • · One tab stop per composite widget; arrow keys move inside it (roving tabindex).
  • · Enter activates the default action, Space activates buttons and toggles.
  • · Esc is reserved for dismissal and never triggers a data change.
  • · No keyboard shortcut without a modifier — a single letter would collide with typing a reply.
  • · Reversible actions apply immediately and offer Undo; we never gate them behind a confirm dialog that traps focus.

Screen-reader contract

Announcements

  • · Toasts render in an aria-live=polite region; the Undo button is a real button inside it.
  • · Sync failure and overdue counts use aria-live=assertive — the only two assertive regions in the product.
  • · Skeletons expose aria-busy=true on the container they replace, not per shimmer block.
  • · Icon-only controls carry aria-label; decorative icons are aria-hidden.
  • · Tabs use role=tablist/tab/tabpanel with aria-controls; the bottom nav uses nav + aria-current=page.

Do

  • Return focus to a sensible neighbour after a row disappears.
  • Let Tab reach the Undo affordance before the toast expires.
  • Keep arrow-key selection and content in sync in the segmented tabs.

Don't

  • Don't add hover-only actions — anything a mouse can do, a key must reach.
  • Don't autofocus anything except the first field of an opened modal.
  • Don't trap focus in the panel shell; only modals trap.

15c

Required contrast checks per component

Every approved token pairing, measured against the theme you are currently viewing. Text needs 4.5:1, large text and non-text UI (borders, dots, focus rings) need 3:1. Flip the theme toggle to re-run the whole matrix in dark mode.

Measured live · light theme
ComponentPairingRequiredMeasuredResult
Measuring tokens…

Why: Contrast is checked as a token pairing, not as a screenshot: a component built from approved pairings is compliant by construction, which is why a new screen needs no separate audit. If a pairing you need is not in this table, it is not approved yet — extend the system rather than hardcoding a colour.

Implementation source of truth: tokens in src/styles.css, components in src/components/fq/primitives.tsx. Build screens by composing these components — if a screen needs a style that is not here, extend the system first.

Contrast Audit

Dev & QA only — not a customer setting