# Modern UX & Front-End Design Best Practices
> Reference guide for B2B SaaS dashboard applications, with emphasis on ISP/network monitoring tools.
> All examples use Tailwind CSS. Phoenix LiveView patterns included where relevant.
---
## Table of Contents
1. [Design Systems](#1-design-systems)
2. [Layout Patterns](#2-layout-patterns)
3. [Data Display](#3-data-display)
4. [Forms & Input](#4-forms--input)
5. [Navigation](#5-navigation)
6. [Accessibility](#6-accessibility)
7. [Animation & Transitions](#7-animation--transitions)
8. [Phoenix LiveView Specific](#8-phoenix-liveview-specific)
9. [Color & Typography](#9-color--typography)
10. [Anti-Patterns](#10-anti-patterns)
---
## 1. Design Systems
### Component Hierarchy
Organize components in three tiers:
| Tier | Examples | Characteristics |
|------|----------|-----------------|
| **Primitives** | Button, Badge, Input, Label | Stateless, no business logic, maximum reuse |
| **Composites** | Card, DataTable, FormGroup, Modal | Combine primitives, may have local state |
| **Features** | DevicePanel, AlertFeed, BandwidthChart | Domain-specific, connected to data |
```
components/
├── ui/ # Primitives — button, badge, input, tooltip
├── composite/ # Composites — card, data_table, form_group
└── features/ # Features — device_panel, alert_feed
```
### Design Tokens
Define tokens in `tailwind.config.js` — never use raw hex in templates:
```js
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
// Semantic tokens
surface: {
DEFAULT: 'var(--color-surface)', // white / gray-900
raised: 'var(--color-surface-raised)', // gray-50 / gray-800
overlay: 'var(--color-surface-overlay)', // white / gray-800
},
border: {
DEFAULT: 'var(--color-border)', // gray-200 / gray-700
strong: 'var(--color-border-strong)', // gray-300 / gray-600
},
status: {
healthy: '#10b981', // emerald-500
warning: '#f59e0b', // amber-500
critical: '#ef4444', // red-500
unknown: '#6b7280', // gray-500
maint: '#3b82f6', // blue-500
},
},
spacing: {
'sidebar': '16rem', // 256px collapsed
'sidebar-sm': '4rem', // 64px collapsed
'topbar': '4rem', // 64px
},
fontSize: {
'kpi': ['2rem', { lineHeight: '2.5rem', fontWeight: '700' }],
'metric': ['1.5rem', { lineHeight: '2rem', fontWeight: '600' }],
},
},
},
}
```
### Tailwind Best Practices
**Avoid class soup — extract components, not utilities:**
```html
Save
<.button variant="primary" size="md">Save
```
**The component function:**
```elixir
# core_components.ex
attr :variant, :string, default: "primary", values: ~w(primary secondary ghost danger)
attr :size, :string, default: "md", values: ~w(sm md lg)
attr :disabled, :boolean, default: false
slot :inner_block, required: true
def button(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
```
### Dark Mode
Use Tailwind's `dark:` variant with CSS custom properties for seamless theming:
```css
/* app.css */
:root {
--color-surface: theme('colors.white');
--color-surface-raised: theme('colors.gray.50');
--color-surface-overlay: theme('colors.white');
--color-border: theme('colors.gray.200');
--color-border-strong: theme('colors.gray.300');
--color-text-primary: theme('colors.gray.900');
--color-text-secondary: theme('colors.gray.500');
}
.dark {
--color-surface: theme('colors.gray.900');
--color-surface-raised: theme('colors.gray.800');
--color-surface-overlay: theme('colors.gray.800');
--color-border: theme('colors.gray.700');
--color-border-strong: theme('colors.gray.600');
--color-text-primary: theme('colors.gray.100');
--color-text-secondary: theme('colors.gray.400');
}
```
**Toggle strategy:**
```js
// Respect system preference, allow manual override
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
```
---
## 2. Layout Patterns
### App Shell
The canonical B2B SaaS layout: fixed sidebar + top bar + scrollable content area.
```html
Network
/
Devices
Search...
⌘K
```
### Collapsible Sidebar
```html
```
### Mobile Sidebar (Slide-over)
```html
```
### Dashboard Grid
Use CSS Grid with responsive breakpoints for KPI cards and widgets:
```html
Total Devices
1,247
+12 this week
```
### Card Component
```html
```
### Skeleton Loading
```html
```
**Skeleton shimmer effect (optional CSS):**
```css
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton-shimmer {
background: linear-gradient(90deg,
theme('colors.gray.200') 25%,
theme('colors.gray.100') 50%,
theme('colors.gray.200') 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.dark .skeleton-shimmer {
background: linear-gradient(90deg,
theme('colors.gray.700') 25%,
theme('colors.gray.600') 50%,
theme('colors.gray.700') 75%
);
}
```
---
## 3. Data Display
### Tables
Tables are the backbone of network monitoring UIs. Get them right.
```html
3 selected
Reboot
Export
Delete
Showing 1 to 25
of 1,247
Previous
1
2
3
...
50
Next
```
**Table design rules:**
- Left-align text, right-align numbers
- Use `font-mono text-xs` for IPs, MACs, serial numbers
- Status columns: colored badge with dot indicator
- Always provide a row hover state
- Sticky header on long tables: `sticky top-0 z-10`
- Avoid wrapping — use `truncate` and `max-w-*` with tooltips
### Charts — Which Type When
| Data Type | Chart | Example |
|-----------|-------|---------|
| **Time series** | Line/Area | Bandwidth, latency, CPU over time |
| **Composition** | Stacked area/bar | Traffic by protocol over time |
| **Comparison** | Horizontal bar | Top 10 talkers by bandwidth |
| **Distribution** | Histogram | Latency distribution |
| **Single value + trend** | KPI card + sparkline | Current throughput |
| **Part of whole** | Donut (never pie) | Device status breakdown |
| **Geographic** | Map with pins/heatmap | Tower locations, coverage |
| **Correlation** | Scatter plot | Packet loss vs. distance |
**Chart guidelines for monitoring dashboards:**
- Default to **24h** time range with **auto-refresh**
- Always show the y-axis scale and units (Mbps, ms, %)
- Use **area fills** with low opacity (`fill-opacity: 0.1`) for single-line charts
- Color thresholds: green < 70%, amber 70–90%, red > 90%
- Provide time range selector: 1h | 6h | 24h | 7d | 30d
- Tooltip should show exact timestamp + value
- Loading state: show axes + gray placeholder area
### KPI Widget
```html
```
### Empty / Error / Loading States
Every data view needs all three. **Never show a blank screen.**
```html
No devices found
Add your first device to start monitoring your network.
Add Device
Failed to load devices
Something went wrong. Please try again or contact support if the problem persists.
Retry
```
**State hierarchy:**
1. **Loading** → skeleton (content areas) or spinner (actions)
2. **Error** → message + retry action
3. **Empty** → illustration + explanation + primary action
4. **Data** → the actual content
---
## 4. Forms & Input
### Validation Patterns
**Inline validation — validate on blur, show errors immediately:**
```html
```
**Validation rules:**
- Validate on blur (not on every keystroke)
- Show success states for complex fields (IP addresses, CIDR notation)
- Group related errors at the top of the form for long forms
- Never clear the user's input on error
- For LiveView: use `phx-change` with debounce for real-time validation
### Radio Card Groups
Perfect for selecting device types, plan tiers, or monitoring profiles:
```html
Device Type
```
### Search & Filter Patterns
**Combo filter bar:**
```html
Status: Online
Type: Router
Clear all
```
### Auto-Save Indicator
```html
Saving...
Saved
Failed to save
Retry
```
### Confirmation Dialogs
Use for destructive actions only. **Never confirm routine saves.**
```html
Delete Device
Are you sure you want to delete core-router-01 ?
All monitoring data will be permanently removed. This action cannot be undone.
Cancel
Delete
```
**Rules for confirmation dialogs:**
- Destructive button on the **right**, cancel on the **left**
- Name the action specifically ("Delete Device" not "Are you sure?")
- Include the resource name in the message
- Red color for destructive action button
- For high-stakes actions, require typing the resource name
---
## 5. Navigation
### Breadcrumbs
```html
Home
Network
core-router-01
```
### Tabs
```html
```
### Command Palette (Cmd+K)
Essential for power users in network monitoring tools:
```html
ESC
Devices
core-router-01
10.0.1.1 · Router
↵
core-router-02
10.0.1.2 · Router
Actions
↑↓ Navigate
↵ Open
ESC Close
```
### Notifications
**Toast notifications** — for transient feedback:
```html
Device added
core-switch-03 is now being monitored.
Connection failed
Could not reach 10.0.1.5. Check SNMP credentials.
```
**Toast rules:**
- Auto-dismiss success toasts after 5 seconds
- Error toasts persist until dismissed
- Max 3 visible toasts; queue the rest
- Position: bottom-right for desktop, top-center for mobile
**Banner notifications** — for system-wide alerts:
```html
Scheduled maintenance:
Core network upgrade on March 15 at 02:00 UTC.
Learn more
```
**Notification badge:**
```html
Alerts
12
```
### Onboarding
**Setup checklist:**
```html
Getting Started
2 of 4 complete
```
**Progressive disclosure:** Show complexity only when needed.
```html
```
---
## 6. Accessibility
### ARIA Essentials
```html
Device core-router-01 status changed to offline
Online
Hostname
Delete Device
Overview
Interfaces
```
### Keyboard Navigation
| Component | Keys | Behavior |
|-----------|------|----------|
| **Modal** | `Escape` | Close |
| **Tabs** | `←` `→` | Switch tabs |
| **Dropdown** | `↑` `↓` `Enter` `Escape` | Navigate, select, close |
| **Table** | `Space` | Toggle row selection |
| **Command palette** | `⌘K` / `Ctrl+K` | Open |
| **Toast** | Auto-focus close button | Dismiss |
### Focus Management
```html
Skip to main content
Action
```
### Color Contrast
**WCAG 2.1 AA minimum contrast ratios:**
| Element | Ratio | Example |
|---------|-------|---------|
| Normal text (< 18px) | **4.5:1** | `gray-600` on white = 5.4:1 ✅ |
| Large text (≥ 18px bold / 24px) | **3:1** | `gray-500` on white = 4.6:1 ✅ |
| UI components & graphics | **3:1** | Border, icons, form controls |
**Don't rely on color alone:**
- ❌ Red text for errors, green text for success (color blind users lose info)
- ✅ Red text + error icon + descriptive text
- ✅ Status dot + text label ("Online", "Offline")
- ✅ Chart lines with different patterns/shapes, not just colors
```html
Critical
```
---
## 7. Animation & Transitions
### CSS Transitions
Use for state changes, not entrance animations. Keep them fast (150–200ms).
```html
Save
```
**Duration guidelines:**
- **Micro-interactions** (hover, focus): `duration-150`
- **Component transitions** (expand, collapse): `duration-200`
- **Page/panel transitions**: `duration-300`
- **Never exceed 500ms** for UI transitions
### Skeleton Screens
Prefer skeletons over spinners for content loading:
```html
```
**When to use each:**
- **Skeleton**: Loading content areas, tables, cards, pages
- **Spinner**: Submitting forms, button loading states, small inline updates
- **Progress bar**: File uploads, multi-step processes with known completion
- **Nothing**: If load time < 200ms, show nothing (avoids flash)
### Optimistic UI
Update the UI immediately, reconcile when the server responds:
```
User clicks "Acknowledge Alert"
→ Immediately: badge changes to "Acknowledged", toast shows "Alert acknowledged"
→ Server responds 200: done
→ Server responds error: revert badge, show error toast "Failed to acknowledge alert. Retry?"
```
### Micro-Interactions
```html
```
### Reduced Motion
Always respect the user preference:
```html
```
```css
/* In CSS when Tailwind classes aren't enough */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
---
## 8. Phoenix LiveView Specific
### LiveView + Tailwind Patterns
**Component structure:**
```elixir
# lib/towerops_web/components/ui/status_badge.ex
defmodule ToweropsWeb.Components.UI.StatusBadge do
use Phoenix.Component
@status_styles %{
"healthy" => "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400",
"warning" => "bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
"critical" => "bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400",
"unknown" => "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400",
"maint" => "bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400"
}
@dot_colors %{
"healthy" => "bg-emerald-500",
"warning" => "bg-amber-500",
"critical" => "bg-red-500",
"unknown" => "bg-gray-400",
"maint" => "bg-blue-500"
}
attr :status, :string, required: true, values: Map.keys(@status_styles)
attr :label, :string, default: nil
def status_badge(assigns) do
assigns =
assigns
|> assign(:style, @status_styles[assigns.status])
|> assign(:dot, @dot_colors[assigns.status])
|> assign_new(:label, fn -> Phoenix.Naming.humanize(assigns.status) end)
~H"""
<%= @label %>
"""
end
end
```
### JS Hooks for Client-Side Interactivity
```js
// assets/js/hooks/index.js
export const Hooks = {
// Chart hook — render/update charts client-side
Chart: {
mounted() {
this.chart = createChart(this.el, JSON.parse(this.el.dataset.config));
},
updated() {
const data = JSON.parse(this.el.dataset.series);
this.chart.updateSeries(data);
},
destroyed() {
this.chart?.destroy();
}
},
// Copy to clipboard
Clipboard: {
mounted() {
this.el.addEventListener("click", () => {
const text = this.el.dataset.copy;
navigator.clipboard.writeText(text).then(() => {
// Push event back to LiveView for toast
this.pushEvent("copied", { text });
});
});
}
},
// Command palette (Cmd+K)
CommandPalette: {
mounted() {
this.handleKeydown = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
this.pushEvent("toggle_command_palette", {});
}
if (e.key === "Escape") {
this.pushEvent("close_command_palette", {});
}
};
document.addEventListener("keydown", this.handleKeydown);
},
destroyed() {
document.removeEventListener("keydown", this.handleKeydown);
}
},
// Auto-resize textarea
AutoResize: {
mounted() {
this.resize = () => {
this.el.style.height = "auto";
this.el.style.height = this.el.scrollHeight + "px";
};
this.el.addEventListener("input", this.resize);
this.resize();
}
},
// Preserve scroll position on LiveView updates
ScrollPreserve: {
mounted() {
this.scrollTop = 0;
},
beforeUpdate() {
this.scrollTop = this.el.scrollTop;
},
updated() {
this.el.scrollTop = this.scrollTop;
}
},
// Dark mode toggle (persists to localStorage)
DarkMode: {
mounted() {
this.el.addEventListener("click", () => {
const isDark = document.documentElement.classList.toggle("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
this.pushEvent("theme_changed", { theme: isDark ? "dark" : "light" });
});
}
},
// Infinite scroll
InfiniteScroll: {
mounted() {
this.observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
this.pushEvent("load_more", {});
}
},
{ rootMargin: "200px" }
);
this.observer.observe(this.el);
},
destroyed() {
this.observer?.disconnect();
}
}
};
```
### Live Navigation UX
```elixir
# Use live_redirect for same-LiveView navigation (no page reload)
# Use push_navigate for cross-LiveView navigation
# In router.ex — group under live_session for shared layout
live_session :authenticated, on_mount: [ToweropsWeb.UserAuth] do
live "/dashboard", DashboardLive
live "/devices", DevicesLive.Index
live "/devices/:id", DevicesLive.Show
live "/alerts", AlertsLive.Index
end
```
```elixir
# Loading indicator during live navigation
# In root layout:
```
```js
// Navigation progress hook
NavigationProgress: {
mounted() {
this.width = 0;
window.addEventListener("phx:page-loading-start", () => {
this.width = 70;
this.el.style.width = this.width + "%";
this.el.style.opacity = "1";
});
window.addEventListener("phx:page-loading-stop", () => {
this.el.style.width = "100%";
setTimeout(() => {
this.el.style.opacity = "0";
setTimeout(() => {
this.el.style.width = "0%";
}, 300);
}, 200);
});
}
}
```
### LiveView Form Patterns
```elixir
# Form with real-time validation
def render(assigns) do
~H"""
<.form
for={@form}
phx-change="validate"
phx-submit="save"
phx-debounce="300"
class="space-y-6"
>
<.label for={@form[:hostname]}>Hostname
<.input
field={@form[:hostname]}
type="text"
placeholder="e.g., core-router-01"
phx-debounce="blur"
/>
<.label for={@form[:ip_address]}>IP Address
<.input
field={@form[:ip_address]}
type="text"
placeholder="e.g., 10.0.1.1"
phx-debounce="blur"
/>
<.label for={@form[:device_type]}>Device Type
<.input field={@form[:device_type]} type="select"
options={[{"Router", "router"}, {"Switch", "switch"}, {"AP", "ap"}]} />
<%= if @form[:device_type].value == "ap" do %>
<.label for={@form[:ssid]}>SSID
<.input field={@form[:ssid]} type="text" />
<% end %>
<.button type="button" variant="secondary" phx-click="cancel">Cancel
<.button type="submit" phx-disable-with="Saving...">Save Device
"""
end
def handle_event("validate", %{"device" => params}, socket) do
form =
%Device{}
|> Device.changeset(params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("save", %{"device" => params}, socket) do
case Devices.create_device(params) do
{:ok, device} ->
{:noreply,
socket
|> put_flash(:info, "Device #{device.hostname} created successfully.")
|> push_navigate(to: ~p"/devices/#{device}")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
```
### Real-Time Updates with PubSub
```elixir
# Subscribe on mount
def mount(_params, _session, socket) do
if connected?(socket) do
ToweropsWeb.Endpoint.subscribe("devices:status")
ToweropsWeb.Endpoint.subscribe("alerts:new")
end
{:ok, assign(socket, devices: Devices.list_devices())}
end
# Handle broadcasts
def handle_info(%{topic: "devices:status", payload: %{device_id: id, status: status}}, socket) do
devices =
Enum.map(socket.assigns.devices, fn device ->
if device.id == id, do: %{device | status: status}, else: device
end)
{:noreply, assign(socket, devices: devices)}
end
# Publish from context
def update_device_status(device, status) do
device
|> Device.status_changeset(%{status: status})
|> Repo.update()
|> case do
{:ok, device} ->
ToweropsWeb.Endpoint.broadcast("devices:status", "status_changed", %{
device_id: device.id,
status: device.status
})
{:ok, device}
error ->
error
end
end
```
**LiveView real-time update with CSS transition:**
```elixir
# Highlight row briefly when data changes
```
```js
FlashUpdate: {
updated() {
this.el.classList.add("bg-indigo-50", "dark:bg-indigo-500/5");
setTimeout(() => {
this.el.classList.remove("bg-indigo-50", "dark:bg-indigo-500/5");
}, 1500);
}
}
```
---
## 9. Color & Typography
### Gray Scales
Use a single gray scale consistently. Tailwind's `gray` (the default, formerly `coolGray`) works well for UI-heavy apps:
```
gray-50: #f9fafb — subtle backgrounds
gray-100: #f3f4f6 — hover states, skeleton fills
gray-200: #e5e7eb — borders, dividers
gray-300: #d1d5db — disabled borders, secondary icons
gray-400: #9ca3af — placeholder text, disabled text
gray-500: #6b7280 — secondary text (light mode)
gray-600: #4b5563 — primary body text
gray-700: #374151 — dark mode borders
gray-800: #1f2937 — dark mode surfaces
gray-900: #111827 — dark mode base, headings
gray-950: #030712 — darkest background
```
### Accent Colors
Pick **one primary** + **semantic status colors**:
```js
// tailwind.config.js
colors: {
primary: {
50: '#eef2ff', // indigo-50
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1', // indigo-500 — primary accent
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
950: '#1e1b4e',
},
// Status colors — use sparingly, with purpose
success: colors.emerald,
warning: colors.amber,
danger: colors.red,
info: colors.blue,
}
```
### Dark Mode Color Mapping
| Element | Light | Dark |
|---------|-------|------|
| Page background | `white` / `gray-50` | `gray-900` / `gray-950` |
| Card/surface | `white` | `gray-800` |
| Raised surface | `gray-50` | `gray-700` |
| Border | `gray-200` | `gray-700` |
| Primary text | `gray-900` | `gray-100` |
| Secondary text | `gray-500` | `gray-400` |
| Accent bg | `indigo-50` | `indigo-500/10` |
| Accent text | `indigo-700` | `indigo-400` |
| Status badges | `emerald-50` / `emerald-700` | `emerald-500/10` / `emerald-400` |
**Key principle:** In dark mode, use lower opacity backgrounds (`bg-indigo-500/10`) instead of dark shade equivalents. It adapts better.
### Typography Scale
```css
/* Font stack — system fonts for speed */
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
/* For monospace (IPs, code, configs) */
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
```
| Use | Tailwind | Size | Weight |
|-----|----------|------|--------|
| Page title | `text-xl font-semibold` | 20px | 600 |
| Section heading | `text-lg font-semibold` | 18px | 600 |
| Card heading | `text-sm font-medium` | 14px | 500 |
| Body text | `text-sm` | 14px | 400 |
| Small/caption | `text-xs` | 12px | 400 |
| KPI number | `text-kpi` (custom) | 32px | 700 |
| Metric number | `text-metric` (custom) | 24px | 600 |
| Monospace data | `font-mono text-xs` | 12px | 400 |
| Badge/label | `text-xs font-medium` | 12px | 500 |
**Rules:**
- **14px (`text-sm`) is the base** for B2B SaaS — not 16px. Dashboard density matters.
- Use `font-medium` (500) for emphasis, `font-semibold` (600) for headings
- Avoid `font-bold` (700) except for KPI numbers
- Never use ALL CAPS for more than 2-3 word labels
- `tracking-wider` for uppercase labels: `text-xs font-medium uppercase tracking-wider`
---
## 10. Anti-Patterns
### ❌ Modal Overuse
**Problem:** Using modals for everything — forms, confirmations, details, settings.
**Why it's bad:**
- Blocks the user from referencing underlying content
- Stacking modals creates confusion
- Mobile modals are painful
- Interrupts flow for routine actions
**Instead:**
| Action | Use |
|--------|-----|
| Quick edit (name, status) | Inline edit |
| Create/edit form | Slide-over panel or dedicated page |
| Details view | Expandable row or side panel |
| Confirmation | Only for **destructive** + **irreversible** actions |
| Settings | Dedicated page with sections |
```html
```
### ❌ Notification Fatigue
**Problem:** Alerting on everything, alert storms, redundant notifications.
**Symptoms:**
- Users ignore all alerts
- Alert count badge permanently shows 99+
- Multiple channels notifying for the same event
**Rules:**
- **Deduplicate**: Group related alerts (if 20 devices go down because a switch died, show 1 alert about the switch)
- **Severity levels**: Only push-notify on Critical. Show Warning in-app. Log Info.
- **Snooze/acknowledge**: Let users silence specific alerts
- **Escalation, not repetition**: Alert once, escalate if no response after N minutes
- **Smart defaults**: Don't enable all alerts by default — progressive opt-in
### ❌ Settings Bloat
**Problem:** Exposing every configuration option to every user.
**Instead:**
- **Smart defaults** that work for 80% of users
- **Role-based visibility**: Admins see advanced settings, operators see what they need
- **Progressive disclosure**: Basic settings visible, "Advanced" collapsed
- **Contextual settings**: Put them where they're relevant, not in a mega settings page
```html
General
Notifications
SNMP
Integrations
API Keys
```
### ❌ Vanity Metrics
**Problem:** Dashboards full of numbers that look impressive but don't help anyone.
**"Total API calls: 14,203,847"** — So what?
**Better:**
- Show metrics that **drive action**: "3 devices offline", "Latency > 100ms on 2 links"
- Include **context**: "Packet loss 0.5% (↑ 0.3% from baseline)"
- Add **thresholds**: Color/icon change when metrics leave normal range
- Provide **drill-down**: Click the KPI to see the underlying data
```html
Total Packets Processed
847,293,471
Devices Needing Attention
7
2 offline > 30 min
3 high packet loss
2 nearing capacity
View all →
```
### ❌ Other Common Anti-Patterns
| Anti-Pattern | Problem | Better Approach |
|-------------|---------|-----------------|
| **Loading spinner everywhere** | User has no sense of what's coming | Skeleton screens that mirror actual layout |
| **Infinite scroll on data tables** | Can't bookmark position, loses context | Paginated with page size selector |
| **Auto-refresh entire page** | Loses user state (scroll, selection) | LiveView partial updates via PubSub |
| **Hamburger menu on desktop** | Hides primary navigation | Always-visible sidebar on desktop |
| **Tooltip for essential info** | Mobile users can't hover | Show inline or in a visible label |
| **Disabled buttons with no explanation** | User doesn't know why they can't act | Tooltip explaining why, or hide the action |
| **"Are you sure?" for everything** | Confirmation blindness | Only for destructive actions; use undo for the rest |
| **Giant forms** | Abandonment, cognitive overload | Multi-step wizard or progressive disclosure |
| **Inconsistent empty states** | Some pages blank, some have messages | Every view handles empty, loading, and error |
---
## Quick Reference: Status Color System for Network Monitoring
```html
●
●
●
●
●
●
```
**Apply this everywhere:** sidebar nav icons, table rows, map pins, KPI cards, device headers, alert badges. Consistency is everything.
---
*Last updated: 2026-03-13*