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.
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
#F0FAFA
#CCEEEE
#99DDDD
#55CACA
#2AACAC
#1E9090
#157474
#0D5858
#062020
50 · 100 · 200 · 300 · 400 · 500 ★ Primary · 600 · 700 · 900
Neutral — structure scale
#FFFFFF
#F7F8F8
#EDEEF0
#D8DADD
#9CA0A6
#5A5F66
#3D4148
#1F2226
#111417
0 · 50 · 100 · 200 · 400 · 600 · 700 · 800 · 900
Semantic colours
| Token | Use | Background 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">
| Token | Size | Use |
|---|---|---|
| --text-xs | 11px | Labels, eyebrows, table headers, badges |
| --text-sm | 13px | Nav items, table cells, secondary text |
| --text-base | 14px | Default body, form inputs |
| --text-md | 15px | Modal titles, slightly prominent body |
| --text-lg | 17px | Lead paragraphs, marketing sub-headlines |
| --text-xl | 20px | Page titles (app) |
| --text-2xl | 24px | Section titles |
| --text-3xl | 30px | h2 level headings |
| --text-4xl | 36px | h1 level, hero headlines |
| --text-5xl | 48px | Display / marketing hero — light weight only |
Spacing
All spacing is based on a 4px grid. Use only named spacing tokens — never arbitrary pixel values.
4px
8px
12px
16px
20px
24px
32px
40px
48px
64px
| Token | Value | Common use |
|---|---|---|
| --sp-1 | 4px | Icon-to-label gaps, tight inline spacing |
| --sp-2 | 8px | Button icon gaps, stacked form elements |
| --sp-3 | 12px | Input padding, nav item gaps |
| --sp-4 | 16px | Card padding (small), form field gaps |
| --sp-5 | 20px | Card padding (default), panel body |
| --sp-6 | 24px | Modal padding, header gaps |
| --sp-8 | 32px | Main content padding, major section gaps |
| --sp-10 | 40px | Page padding, marketing section gaps |
| --sp-12 | 48px | Doc page padding, hero top padding |
| --sp-16 | 64px | Hero sections, large vertical breathing room |
Logo System
The logo mark is a teal square containing the letter L, with a small product-specific icon in the top-right corner. Every product in the Logical Web family shares the same mark — only the corner icon changes. The parent brand uses a small dot instead.
HTML structure
<!-- Logo mark only -->
<div class="lw-logo">
<i class="ti ti-bulb"></i>
</div>
<!-- Logo + wordmark (use in top bar) -->
<div class="lw-brand">
<div class="lw-logo"><i class="ti ti-bulb"></i></div>
<div class="lw-brand-text">
<span class="lw-brand-company">Logical Web</span>
<span class="lw-brand-product">Logical Seed</span>
</div>
</div>
<!-- Parent brand (dot badge, no icon) -->
<div class="lw-logo lw-logo--parent"></div>
Sizes
24px
32px
48px
Product icon registry
| Product | Icon | Preview |
|---|---|---|
| Logical Web (parent brand) | lw-logo--parent (dot) | |
| Logical Seed | ti ti-bulb | |
| Vertex (bookmarks) | ti ti-bookmark | |
| Pulse (VPS dashboard) | ti ti-activity | |
| Vault | ti ti-lock |
Usage rules
— Always pair the mark with the wordmark in the top bar. Never the mark alone in navigation.
— The wordmark always shows "Logical Web" (small, muted) above the product name (larger, bold).
— On the marketing site, use the parent mark + "Logical Web" text only — no product name.
— Use only the three defined sizes. Never scale to an arbitrary size.
Forms
Text inputs
Input with addon / group
Select
Checkboxes & radios
Toggle
Cards & Panels
--sp-4/--sp-5 padding and a 1px border to separate zones..card--raised to lift a card with a shadow. Reserve for modals, popovers, or key callout panels.Badges
Status labels, categories, and counts. Always short — one or two words maximum.
Alerts
Inline feedback panels for persistent contextual messages. Use toasts for transient notifications.
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.
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
Confirmation dialog
This cannot be undone.
Deleting Client Portal v2 will permanently remove all ideas and attachments linked to it.
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
| Name | Status | Added | Owner | |
|---|---|---|---|---|
| Automated onboarding sequence | Review | 2 Jun 2026 | Tom W. | |
| Invoice from project hours | Promoted | 28 May 2026 | Tom W. | |
| Client portal redesign | Archived | 14 May 2026 | Tom W. |
Dropdowns
Dropdowns close on outside click or Escape key. The trigger is typically a .btn--icon.btn--ghost with a ti-dots-vertical or ti-chevron-down icon.
Tabs
Empty States
Every list, table, or content area must have an empty state. Tell the user what goes here and give one clear action.
Ideas you add will appear here. Start by capturing something your team has been talking about.
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.
Progress bar
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; }
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.
| Zone | Contents | Notes |
|---|---|---|
| Left (brand) | lw-brand (logo + product name) | Same width as sidebar: var(--sidebar-w) |
| Centre | Search input (max 360px) | Keyboard shortcut: ⌘K / Ctrl+K |
| Right | Notifications · Settings · Separator · User chip | User chip opens dropdown on click |
Page Header
Every page within a product starts with a page header: breadcrumb, title, optional description, and primary actions.
Client Portal v2
14 ideas · Last updated 3 hours ago
<div class="page-header-block">
<div class="page-header-row">
<div>
<div class="breadcrumb">
<a href="#">Product</a>
<i class="ti ti-chevron-right"></i>
<span>Current page</span>
</div>
<h1 style="font-size:var(--text-xl);font-weight:600">Page title</h1>
<p style="font-size:var(--text-sm);color:var(--neutral-500)">Optional description</p>
</div>
<div class="page-header-actions">
<button class="btn btn--secondary">Secondary action</button>
<button class="btn btn--primary">Primary action</button>
</div>
</div>
</div>
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.
<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
Hero section
Clean, considered
web services
for small business.
We build and maintain websites, tools, and digital infrastructure for businesses that want things done properly.
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 this | Not this |
|---|---|
| Save changes | Submit / Update |
| Add idea | Create new / New idea |
| Delete project | Remove / Yes, delete |
| Cancel | Close / Go back / Never mind |
| Sign in | Login / Log in |
| Sign out | Logout / Log out |
| Get in touch | Contact us / Reach out |
Error messages
| Situation | Message |
|---|---|
| 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 */
}
}
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
| Context | Size | Stroke |
|---|---|---|
| Inline in body text | 14px | 1.5 |
| Nav items (sidebar) | 15px | 1.5 |
| Buttons (default) | 14px | 1.5 |
| Buttons (large) | 16px | 1.5 |
| Top bar icon buttons | 16px | 1.5 |
| Alert / toast | 16px | 1.5 |
| Empty state icons | 22px | 1.5 |
| Logo mark icon | 10–14px (varies by mark size) | 2 |
Commonly used 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'
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).Version 1.0 · June 2026 · Internal use only