Logical Web
Design System

A single visual language for every Logical Web product. This document defines every decision — colour, type, spacing, and component — so products built in React or plain HTML/CSS look and feel like they belong to the same family.

This document is the source of truth. When building any Logical Web product, derive every colour, size, and spacing value from the tokens defined here. Never hardcode values that have a token equivalent.

Design principles

Sharp and structured. No decorative border radius. Surfaces are defined by clean 1px borders, not shadows. Components feel precise, not soft.

Teal as signal. The brand colour is used sparingly — primary actions, active states, and key highlights only. When teal appears, it means something.

Light by default, dark by preference. The system is designed in light mode but fully supports dark mode via CSS custom properties that respond to the OS preference. Background is always off-white — never pure white.

One ecosystem. Every product shares the same logo system, spacing scale, type scale, and component patterns. A user who knows one product will feel at home in any other.

Works in React and plain HTML. All tokens are CSS custom properties. Components are described as HTML/CSS patterns that can be adopted as React components without change.

Design Tokens

All values live as CSS custom properties on :root. Copy the token block into every project. Never use raw hex values or pixel sizes in component CSS — always reference a token.

/* Paste this :root block into every project's base CSS */
:root {
  /* Brand */
  --teal-500:  #1E9090;  /* Primary */
  --teal-600:  #157474;
  --teal-400:  #2AACAC;
  --teal-50:   #F0FAFA;
  --teal-100:  #CCEEEE;
  --teal-200:  #99DDDD;

  /* Neutral */
  --neutral-0:   #FFFFFF;
  --neutral-50:  #F7F8F8;  /* Page background */
  --neutral-100: #EDEEF0;  /* Default borders */
  --neutral-200: #D8DADD;  /* Strong borders */
  --neutral-400: #9CA0A6;  /* Placeholders */
  --neutral-600: #5A5F66;  /* Secondary text */
  --neutral-800: #1F2226;  /* Strong text */
  --neutral-900: #111417;  /* Primary text */

  /* Semantic */
  --color-success: #16A34A;  --color-success-bg: #F0FDF4;
  --color-warning: #CA8A04;  --color-warning-bg: #FEFCE8;
  --color-danger:  #DC2626;  --color-danger-bg:  #FEF2F2;
  --color-info:    #2563EB;  --color-info-bg:    #EFF6FF;

  /* Spacing (4px base) */
  --sp-1:4px; --sp-2:8px;  --sp-3:12px; --sp-4:16px;
  --sp-5:20px;--sp-6:24px; --sp-8:32px; --sp-10:40px;
  --sp-12:48px;--sp-16:64px;

  /* Layout */
  --sidebar-w: 220px;
  --topbar-h:  52px;
  --radius-sm: 2px;

  /* Transitions */
  --transition-fast: 100ms ease;
  --transition-base: 150ms ease;
  --transition-slow: 250ms ease;
}

Colour

The palette is built on two scales: teal (brand) and neutral (structure). Semantic colours cover feedback states. Hover swatches to see their hex values.

Teal — brand scale

--teal-50
#F0FAFA
--teal-100
#CCEEEE
--teal-200
#99DDDD
--teal-300
#55CACA
--teal-400
#2AACAC
--teal-500 ★
#1E9090
--teal-600
#157474
--teal-700
#0D5858
--teal-900
#062020

50 · 100 · 200 · 300 · 400 · 500 ★ Primary · 600 · 700 · 900

Neutral — structure scale

--neutral-0
#FFFFFF
--neutral-50
#F7F8F8
--neutral-100
#EDEEF0
--neutral-200
#D8DADD
--neutral-400
#9CA0A6
--neutral-600
#5A5F66
--neutral-700
#3D4148
--neutral-800
#1F2226
--neutral-900
#111417

0 · 50 · 100 · 200 · 400 · 600 · 700 · 800 · 900

Semantic colours

TokenUseBackground token
--color-success #16A34A✔ Confirmations, complete states--color-success-bg #F0FDF4
--color-warning #CA8A04⚠ Caution, degraded states--color-warning-bg #FEFCE8
--color-danger #DC2626✕ Errors, destructive actions--color-danger-bg #FEF2F2
--color-info #2563EBℹ Informational, in-progress--color-info-bg #EFF6FF

Colour usage rules

— Page background: always --neutral-50. Never pure white.

— Surface (cards, panels, sidebar, top bar): --neutral-0.

— Default borders: --neutral-100. Emphasis borders: --neutral-200.

— Primary text: --neutral-900. Secondary text: --neutral-600. Muted/labels: --neutral-400.

— Use --teal-500 for: primary buttons, active nav items, focus rings, and key accents only. Teal should be rare enough that it draws attention.

Typography

A single typeface — Inter — used across all weights and sizes. Load via Google Fonts or serve locally.

<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
Display — 48px Light 300
Heading 1 — 36px Semibold 600
Heading 2 — 24px Semibold 600
Heading 3 — 20px Semibold 600
Lead paragraph — 17px Regular 400. Used for introductory text and marketing sub-headlines.
Body text — 14px Regular 400. The default for UI text, descriptions, and content.
Small / UI text — 13px. Nav items, table cells, labels, secondary content.
Label / Eyebrow — 11px Medium 500 · Uppercase · 0.1em tracking
Monospace — 12px · Code, tokens, identifiers
Type scale — Inter
TokenSizeUse
--text-xs11pxLabels, eyebrows, table headers, badges
--text-sm13pxNav items, table cells, secondary text
--text-base14pxDefault body, form inputs
--text-md15pxModal titles, slightly prominent body
--text-lg17pxLead paragraphs, marketing sub-headlines
--text-xl20pxPage titles (app)
--text-2xl24pxSection titles
--text-3xl30pxh2 level headings
--text-4xl36pxh1 level, hero headlines
--text-5xl48pxDisplay / marketing hero — light weight only
Marketing headlines use weight 300 (Light) for the base with weight 600 (Semibold) for emphasis. App headings use 600 throughout. The maximum weight is 600 — never use 700 (Bold).

Spacing

All spacing is based on a 4px grid. Use only named spacing tokens — never arbitrary pixel values.

--sp-1
4px
--sp-2
8px
--sp-3
12px
--sp-4
16px
--sp-5
20px
--sp-6
24px
--sp-8
32px
--sp-10
40px
--sp-12
48px
--sp-16
64px
TokenValueCommon use
--sp-14pxIcon-to-label gaps, tight inline spacing
--sp-28pxButton icon gaps, stacked form elements
--sp-312pxInput padding, nav item gaps
--sp-416pxCard padding (small), form field gaps
--sp-520pxCard padding (default), panel body
--sp-624pxModal padding, header gaps
--sp-832pxMain content padding, major section gaps
--sp-1040pxPage padding, marketing section gaps
--sp-1248pxDoc page padding, hero top padding
--sp-1664pxHero sections, large vertical breathing room

Buttons

All buttons share a base .btn class. A variant modifier sets the visual style. Size modifiers adjust padding and font size.

Variants

.btn--primary · .btn--secondary · .btn--ghost · .btn--danger · .btn--danger-ghost

With icons

Icon left · Icon-only (.btn--icon)

Sizes

.btn--sm · default · .btn--lg

States

[disabled] · .btn--loading

Full width

.btn--full

Usage rules

Primary: the single most important action on a page. One per view.

Secondary: supporting actions — filter, export, cancel.

Ghost: low-emphasis actions in toolbars or alongside primary actions.

Danger: irreversible destructive actions only. Always pair with a confirmation modal.

— Button labels use sentence case. Never ALL CAPS.

Forms

Text inputs

This will appear in all notifications.
Enter a valid email address.
Read-only field.
Default · Error (.input--error) · Disabled states

Input with addon / group

https://
.input-addon · .input-group

Select

.select-wrap > select.input

Checkboxes & radios


.check-field · checkbox · radio

Toggle

Use for binary on/off settings. Use a checkbox for form submission choices.
.toggle

Cards & Panels

Recent ideasView all →
Card body content. The header uses --sp-4/--sp-5 padding and a 1px border to separate zones.
Raised card
Use .card--raised to lift a card with a shadow. Reserve for modals, popovers, or key callout panels.
.card · .card--raised

Badges

Status labels, categories, and counts. Always short — one or two words maximum.

Active Complete Review Error In progress Archived New 4
.badge--teal · --success · --warning · --danger · --info · --neutral · --solid · .count-badge

Alerts

Inline feedback panels for persistent contextual messages. Use toasts for transient notifications.

Changes saved
Your settings have been updated.
Storage at 80%
Consider archiving old projects to free up space.
Deployment failed
Check the build log. The previous version is still live.
Maintenance window
Scheduled downtime on Saturday 21 June, 02:00–04:00 UTC.
A neutral alert for non-urgent informational messages.
.alert--success · --warning · --danger · --info · --neutral

Toasts

Transient notifications triggered by a user action. They appear at the bottom-right. Success toasts auto-dismiss after 4 seconds; error toasts stay until dismissed.

Idea saved.
Failed to save. Check your connection.
Session expiring in 5 minutes.
3 ideas imported from CSV.
.toast · Positioned: fixed, bottom:24px, right:24px, z-index:9000

Toast container CSS

.toast-container {
  position: fixed;
  bottom: var(--sp-6);
  right: var(--sp-6);
  z-index: 9000;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  align-items: flex-end;
}

— Toast messages should be under 10 words.

— Never use a toast as the only error feedback — also show inline field errors where applicable.

Modals & Confirmations

Standard modal

.modal · Standard form modal

Confirmation dialog

.modal.modal--confirm · Destructive action — always required before delete/remove/revoke

Usage rules

— Every destructive action must trigger a confirmation modal before executing.

— The title names the action. The body names the specific item being affected.

— Cancel is always left. The confirming action is always right.

— Modal backdrop: background: rgb(0 0 0 / 0.35). Never fully opaque.

— Max modal width: 480px (standard), 400px (confirmation).

Tables

NameStatusAddedOwner
Automated onboarding sequenceReview2 Jun 2026Tom W.
Invoice from project hoursPromoted28 May 2026Tom W.
Client portal redesignArchived14 May 2026Tom W.
table.data-table · Row hover on tbody tr · Last column for actions

Tabs

Overview
Ideas 12
Activity
Settings
Tab panel content area
.tabs > .tab-item · .tab-item.is-active

Empty States

Every list, table, or content area must have an empty state. Tell the user what goes here and give one clear action.

No ideas yet

Ideas you add will appear here. Start by capturing something your team has been talking about.

.empty-state · Icon + title + description + single CTA

Loading States

Skeleton screens

Use skeleton screens for initial page loads. They give the user a sense of what's loading without a jarring blank screen.

.skeleton · .skeleton-text · Shimmer animation

Progress bar

.progress > .progress-bar · --warning · --danger

App Layout

All product apps use the same shell: a fixed top bar, a fixed left sidebar, and a scrollable main content area. The sidebar is optional for simpler products.

<!-- App shell structure -->
<header class="topbar">…</header>
<aside class="sidebar">…</aside>
<main class="main-content">…</main>
/* App layout CSS */
.topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--topbar-h);   /* 52px */
  z-index: 100;
}

.sidebar {
  position: fixed;
  top: var(--topbar-h);
  left: 0; bottom: 0;
  width: var(--sidebar-w);   /* 220px */
  z-index: 50;
  overflow-y: auto;
}

.main-content {
  margin-top: var(--topbar-h);
  margin-left: var(--sidebar-w);
  padding: var(--sp-8);
  min-height: calc(100vh - var(--topbar-h));
}

/* No sidebar variant */
.main-content--no-sidebar { margin-left: 0; }
Products that don't need a sidebar omit the aside and use .main-content--no-sidebar.

Top Bar

Present in every product. Contains — left to right — the brand, search, and user controls. Height is fixed at --topbar-h: 52px.

Logical WebLogical Seed
TW
Tom
Brand zone · Search · Notifications · Settings · User chip
ZoneContentsNotes
Left (brand)lw-brand (logo + product name)Same width as sidebar: var(--sidebar-w)
CentreSearch input (max 360px)Keyboard shortcut: ⌘K / Ctrl+K
RightNotifications · Settings · Separator · User chipUser chip opens dropdown on click

Stat Cards

Used in dashboard views to surface key numbers. Arranged in a 4-column grid by default. Reduce to 3 or 2 columns when there are fewer metrics.

Total ideas
48
↑ 6 this week
Under review
12
4 need attention
Promoted
7
↑ 2 this month
Storage used
2.4 GB
↑ 18% vs last month
.stat-card · 4-col grid · .stat-card-delta--down for negative trends
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px">
  <div class="stat-card">
    <div class="stat-card-label">Label</div>
    <div class="stat-card-value">48</div>
    <div class="stat-card-delta">↑ 6 this week</div>
  </div>
</div>

Marketing Site

The main Logical Web website shares the same token system but has a different layout to the product apps — no sidebar, a taller nav (56px vs 52px), and more generous hero spacing. Max content width: 960px, centred.

Navigation

Marketing nav — 56px height · Parent logo + links + CTA button

Hero section

Website Services

Clean, considered
web services
for small business.

We build and maintain websites, tools, and digital infrastructure for businesses that want things done properly.

Marketing hero — weight 300 headline + weight 600 emphasis in teal

Marketing layout rules

— Page background: --neutral-50 (same as apps).

— Nav height: 56px. Fixed at top.

— Hero headline: font-weight:300, letter-spacing:-1.5px, emphasis phrase in font-weight:600 and --teal-500.

— Max content width: 960px, centred with margin:0 auto.

— Section dividers: 1px --neutral-100 border between sections.

— Footer: minimal — copyright left, 2–3 links right.

/* Marketing layout CSS */
.mkt-container {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 var(--sp-10);
}

.mkt-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 56px;
  background: var(--neutral-0);
  border-bottom: 1px solid var(--neutral-100);
  z-index: 100;
}

.mkt-hero {
  margin-top: 56px;
  min-height: calc(100vh - 56px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--sp-16) var(--sp-10) var(--sp-12);
}

Writing Guidelines

Copy is part of the design. Every label, button, error, and empty state should feel like it was written by the same person — plain, direct, and useful.

Core rules

Sentence case everywhere. "Add new idea" not "Add New Idea". The only exception is acronyms (API, CSS, VPS).

Active voice, specific verbs. "Save changes" not "Submit". "Delete project" not "Remove".

Name the thing. Errors and confirmations always name the specific item. "Delete Client Portal v2?" not "Delete this project?"

No filler. Cut "successfully" from toasts ("Saved" not "Saved successfully"). Cut "Please" from errors ("Enter a valid email" not "Please enter a valid email").

Errors explain, don't apologise. "That email is already in use. Sign in instead?" not "We're sorry, an error occurred."

Empty states invite action. Tell the user what goes here and give them one clear action to start.

Button labels

Use thisNot this
Save changesSubmit / Update
Add ideaCreate new / New idea
Delete projectRemove / Yes, delete
CancelClose / Go back / Never mind
Sign inLogin / Log in
Sign outLogout / Log out
Get in touchContact us / Reach out

Error messages

SituationMessage
Required field empty"[Field name] is required."
Invalid format"Enter a valid [field name]."
Duplicate entry"That [thing] already exists."
Network failure"Something went wrong. Try again."
Permission denied"You don't have access to do that."
Not found"That [thing] doesn't exist or has been deleted."

Dark Mode

Dark mode is handled automatically via a @media (prefers-color-scheme: dark) block that overrides the neutral scale. No JavaScript toggle is needed — the OS preference is respected automatically.

@media (prefers-color-scheme: dark) {
  :root {
    --neutral-0:   #111417;   /* Surface — was white */
    --neutral-50:  #181C20;   /* Page bg — was near-white */
    --neutral-100: #1F2428;   /* Borders */
    --neutral-200: #2A3038;   /* Strong borders */
    --neutral-400: #6A7280;   /* Placeholders */
    --neutral-600: #9CA0A6;   /* Secondary text */
    --neutral-700: #BCC0C6;
    --neutral-800: #D8DADD;
    --neutral-900: #F0F1F2;   /* Primary text — was near-black */
  }
}
The teal scale and semantic colours do not change in dark mode. Only the neutral scale flips. This means no separate dark-mode component styles are needed — the token swap does all the work.

If you later want a manual toggle (a user preference stored in settings), set data-theme="dark" on <html> and duplicate the dark mode token overrides under that selector alongside the media query.

/* Manual toggle support (add alongside the media query) */
[data-theme="dark"] {
  --neutral-0:   #111417;
  --neutral-50:  #181C20;
  /* … rest of dark tokens … */
}

Icons

All icons use Tabler Icons — an open-source set with 5,000+ icons in a consistent stroke style. One import covers the whole product family.

<!-- CDN (HTML projects) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">

<!-- Usage in HTML -->
<i class="ti ti-bulb"></i>
/* React projects */
npm install @tabler/icons-react

import { IconBulb, IconBookmark, IconActivity } from '@tabler/icons-react'

<IconBulb size={16} stroke={1.5} />

Icon sizes by context

ContextSizeStroke
Inline in body text14px1.5
Nav items (sidebar)15px1.5
Buttons (default)14px1.5
Buttons (large)16px1.5
Top bar icon buttons16px1.5
Alert / toast16px1.5
Empty state icons22px1.5
Logo mark icon10–14px (varies by mark size)2
Always use stroke icons (not filled) for UI. Filled variants are reserved for indicating an active or selected state — for example a filled bookmark when an item is saved.

Commonly used icons

ti-plus
ti-search
ti-settings
ti-bell
ti-edit
ti-trash
ti-dots-vertical
ti-x
ti-chevron-right
ti-chevron-down
ti-download
ti-upload
ti-filter
ti-copy
ti-archive
ti-logout
ti-help-circle
ti-external-link
ti-circle-check
ti-alert-circle
Common UI icons — full library at tabler.io/icons

React Usage

All component patterns in this document are plain HTML/CSS. To use them in React, convert each component to a functional component. CSS classes stay identical — no CSS-in-JS required.

Recommended file structure

src/
  styles/
    tokens.css        ← The :root token block — import once in main.jsx
    base.css          ← Reset + body + typography defaults
    components.css    ← All component CSS from this document
  components/
    ui/
      Button.jsx
      Input.jsx
      Select.jsx
      Toggle.jsx
      Badge.jsx
      Alert.jsx
      Toast.jsx
      Modal.jsx
      Dropdown.jsx
      Tabs.jsx
      Card.jsx
      Table.jsx
      EmptyState.jsx
      Skeleton.jsx
    layout/
      TopBar.jsx
      Sidebar.jsx
      PageHeader.jsx

Example: Button component

// components/ui/Button.jsx
export function Button({
  variant = 'primary',
  size,
  icon,
  loading = false,
  disabled = false,
  full = false,
  children,
  onClick,
  ...props
}) {
  const classes = [
    'btn',
    `btn--${variant}`,
    size    && `btn--${size}`,
    loading && 'btn--loading',
    full    && 'btn--full',
  ].filter(Boolean).join(' ')

  return (
    <button
      className={classes}
      disabled={disabled || loading}
      onClick={onClick}
      {...props}
    >
      {icon && <i className={`ti ti-${icon}`} />}
      {children}
    </button>
  )
}

// Usage
<Button variant="primary" icon="plus" onClick={handleAdd}>
  Add idea
</Button>

<Button variant="danger" loading={isDeleting}>
  Delete project
</Button>

<Button variant="secondary" size="sm" icon="download">
  Export
</Button>

Example: Logo mark component

// components/ui/LogoMark.jsx
export function LogoMark({ icon, size = 'default', isParent = false }) {
  const sizeClass = size === 'sm' ? 'lw-logo--sm'
    : size === 'lg' ? 'lw-logo--lg' : ''

  return (
    <div className={['lw-logo', sizeClass, isParent && 'lw-logo--parent'].filter(Boolean).join(' ')}>
      {!isParent && icon && <i className={`ti ti-${icon}`} />}
    </div>
  )
}

export function Brand({ icon, isParent = false, productName, size }) {
  return (
    <div className="lw-brand">
      <LogoMark icon={icon} isParent={isParent} size={size} />
      <div className="lw-brand-text">
        <span className="lw-brand-company">Logical Web</span>
        {productName && <span className="lw-brand-product">{productName}</span>}
      </div>
    </div>
  )
}

// Usage in TopBar
<Brand icon="bulb" productName="Logical Seed" />

// Usage on marketing site
<Brand isParent />

Example: Toast system

// A simple toast context for React
// context/ToastContext.jsx
import { createContext, useContext, useState, useCallback } from 'react'

const ToastCtx = createContext(null)

export function ToastProvider({ children }) {
  const [toasts, setToasts] = useState([])

  const addToast = useCallback(({ message, type = 'success', duration = 4000 }) => {
    const id = Date.now()
    setToasts(prev => [...prev, { id, message, type }])
    if (type !== 'danger') {
      setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), duration)
    }
  }, [])

  const removeToast = useCallback((id) => {
    setToasts(prev => prev.filter(t => t.id !== id))
  }, [])

  return (
    <ToastCtx.Provider value={addToast}>
      {children}
      <div className="toast-container">
        {toasts.map(t => (
          <div key={t.id} className={`toast toast--${t.type}`}>
            <i className={`ti ti-${t.type === 'success' ? 'circle-check' : t.type === 'danger' ? 'alert-circle' : 'info-circle'}`} />
            <span className="toast-msg">{t.message}</span>
            <button className="toast-close" onClick={() => removeToast(t.id)}>
              <i className="ti ti-x" />
            </button>
          </div>
        ))}
      </div>
    </ToastCtx.Provider>
  )
}

export const useToast = () => useContext(ToastCtx)

// Usage anywhere in the app
const toast = useToast()
toast({ message: 'Idea saved.', type: 'success' })
toast({ message: 'Failed to save. Try again.', type: 'danger' })

Accessing tokens in JavaScript

// Read a CSS token value in JS (e.g. for canvas drawing or charting)
const teal = getComputedStyle(document.documentElement)
  .getPropertyValue('--teal-500').trim()
// → '#1E9090'
Import tokens.css and base.css once at the top of main.jsx. Component CSS can be split per-component or kept in a single components.css — either works as long as classes are globally available (i.e. not CSS Modules).
Logical Web Design System

Version 1.0 · June 2026 · Internal use only