Internal Tools
Data density over visual impact, scannability first
Why this matters
Operations teams that process 200 items per day in a well-designed internal tool move 40% faster than teams using a tool built with customer-facing design assumptions.
Internal Tools
"The person who uses your admin dashboard eight hours a day does not want it to be beautiful. They want it to be fast."
The Problem
A team builds an admin dashboard. They follow the same design principles as their customer-facing product: generous whitespace, large cards with rounded corners, big typography, beautiful illustrations for empty states. It looks stunning in the design review.
Then the operations team starts using it. They need to review 200 orders per day. Each order is a large card that takes up half the viewport. They scroll constantly. The rounded corners and shadows look great but consume precious pixels that could show more data. The empty state illustration is charming the first time and insulting the hundredth time — they're power users, not new signups.
The ops team starts opening four browser tabs because they can't see enough information on one screen. They copy IDs from the dashboard into a spreadsheet to cross-reference. They print PDF reports because the dashboard doesn't show the three numbers they need side by side. The tool that was designed to be "clean and modern" is costing them two hours per day in workarounds.
Internal tools fail when they're designed with consumer product aesthetics. Consumer products optimize for first impression — the user might leave in 30 seconds, so make those 30 seconds delightful. Internal tools optimize for the eight-hour day — the user is stuck here regardless, so make those eight hours efficient.
These are fundamentally different design problems with fundamentally different solutions. Applying one set of principles to the other produces tools that look nice in screenshots and fail in practice.
The Principle
Internal tools optimize for scannability and data density. Every pixel earns its place by conveying information or enabling action. Visual flourish that doesn't serve the operator's workflow is waste.
This doesn't mean ugly. It means purposeful. A well-designed internal tool is like a cockpit: dense with information, every element in a deliberate position, nothing decorative. The operator should be able to glance at the screen and know the state of the system without reading a single word.
The Pattern
Data density: show more, scroll less
The primary design constraint for internal tools is information per viewport. An ops team member scanning orders should see 15-20 rows without scrolling, not 5 rows in large cards.
// WRONG — consumer-style cards waste vertical space
<div className="space-y-4">
{orders.map((order) => (
<Card key={order.id} className="p-6 rounded-xl shadow-lg">
<CardHeader>
<CardTitle className="text-xl">{order.title}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground">{order.customer_name}</p>
<Badge>{order.status}</Badge>
<p className="text-2xl font-bold">${order.total}</p>
</CardContent>
</Card>
))}
</div>
// RIGHT — compact table rows maximize information density
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-24">ID</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="text-right">Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order) => (
<TableRow
key={order.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => setSelected(order.id)}
>
<TableCell className="font-mono text-xs">
{order.id.slice(0, 8)}
</TableCell>
<TableCell>{order.customer_name}</TableCell>
<TableCell>
<StatusBadge status={order.status} />
</TableCell>
<TableCell className="text-right font-mono tabular-nums">
${(order.total_cents / 100).toFixed(2)}
</TableCell>
<TableCell className="text-right text-muted-foreground">
{formatRelative(order.created_at)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
Monospace numbers: alignment is information
Numbers in tables must use tabular figures so decimal points align vertically. When a column of numbers has inconsistent digit widths, the eye can't scan for outliers. Monospace tabular numbers turn a column into a visual pattern where anomalies jump out.
// WRONG — proportional font, digits don't align
<td className="text-right">$1,234.56</td>
<td className="text-right">$987.00</td>
<td className="text-right">$12,456.78</td>
// RIGHT — monospace tabular numerals, columns align perfectly
<td className="text-right font-mono tabular-nums">$1,234.56</td>
<td className="text-right font-mono tabular-nums">$987.00</td>
<td className="text-right font-mono tabular-nums">$12,456.78</td>
Technical IDs (UUIDs, reference numbers, API keys) are always monospace:
<span className="font-mono text-xs text-muted-foreground">
{order.id.slice(0, 8)}
</span>
Dark mode: not optional for internal tools
Internal tool users spend eight hours staring at their screen. Dark mode isn't a nice-to-have — it's an ergonomic requirement. If you've built with semantic tokens, dark mode is free. If you've hardcoded colors, it's a rewrite.
// These semantic tokens automatically adapt to dark mode
<div className="bg-background text-foreground border-border">
<Table className="bg-card">
<TableRow className="hover:bg-muted/50">
<TableCell className="text-muted-foreground">
Design internal tools in dark mode first, then verify they work in light mode. The constraint of dark mode forces better contrast decisions and prevents the washed-out grays that plague light-only internal tools.
Keyboard shortcuts: the power user's interface
Mouse-driven interfaces are acceptable for customers who visit monthly. They are unacceptable for operators who process hundreds of items daily. Every frequent action needs a keyboard shortcut.
import { useEffect } from "react";
function useKeyboardShortcuts(actions: Record<string, () => void>) {
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
// Don't trigger in input fields
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
return;
}
const key = `${e.metaKey ? "cmd+" : ""}${e.key}`;
const action = actions[key];
if (action) {
e.preventDefault();
action();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [actions]);
}
// Usage in an admin page
useKeyboardShortcuts({
"n": () => openCreateDialog(), // N = new
"cmd+k": () => openCommandPalette(), // Cmd+K = search
"/": () => focusSearchInput(), // / = search (vim-style)
"Escape": () => closePanel(), // Esc = close
"j": () => selectNextRow(), // J = down (vim-style)
"k": () => selectPreviousRow(), // K = up (vim-style)
"Enter": () => openSelectedRow(), // Enter = open detail
});
Show available shortcuts in the UI. A small ? icon in the bottom corner that reveals the shortcut reference is the standard pattern:
<div className="fixed bottom-4 right-4">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full text-muted-foreground"
onClick={() => setShowShortcuts(true)}
>
?
</Button>
</div>
CSS-only transitions: no JavaScript animation libraries
Internal tools must feel instant. JavaScript animation libraries add bundle weight and frame drops. Use CSS transitions for the two interactions that benefit from animation: hover states and panel open/close.
// Hover feedback — CSS only, zero JS
<TableRow className="transition-colors duration-75 hover:bg-muted/50">
// Panel slide — CSS only
<aside
className={cn(
"fixed right-0 top-0 h-full w-[400px] bg-background border-l",
"transition-transform duration-200 ease-out",
isOpen ? "translate-x-0" : "translate-x-full"
)}
>
The duration-75 for hover states and duration-200 for panels. These are fast enough to feel responsive and slow enough to be perceptible. No other animations. No loading spinners that bounce. No skeleton pulse that draws the eye. Internal tool animations should be invisible — they smooth transitions without calling attention to themselves.
Status indicators: color-coded, scannable
Status is the most-scanned column in any internal tool. Use color-coded badges with consistent semantics across every table in the application:
const STATUS_STYLES: Record<string, string> = {
active: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
pending: "bg-amber-500/15 text-amber-700 dark:text-amber-400",
failed: "bg-red-500/15 text-red-700 dark:text-red-400",
archived: "bg-muted text-muted-foreground",
};
function StatusBadge({ status }: { status: string }) {
return (
<span
className={cn(
"inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium",
STATUS_STYLES[status] ?? STATUS_STYLES.archived
)}
>
{status}
</span>
);
}
Green means good. Amber means attention needed. Red means broken. Gray means inactive. This mapping must be consistent across every table, every page, every tool. When an operator sees red, they should know something is wrong before reading the label.
Detail panels: Sheet, not page navigation
When an operator clicks a row, the detail opens in a side panel (Sheet), not a new page. Page navigation breaks the operator's mental context — they lose their scroll position, their filters, their selection state. A side panel preserves everything.
<Sheet open={!!selectedId} onOpenChange={() => setSelectedId(null)}>
<SheetContent className="w-[500px] overflow-y-auto">
<SheetHeader>
<SheetTitle className="font-mono text-sm">
Order {selectedId?.slice(0, 8)}
</SheetTitle>
</SheetHeader>
<div className="space-y-0 divide-y">
<OrderSummarySection order={selectedOrder} />
<CustomerSection customer={selectedOrder.customer} />
<TimelineSection events={selectedOrder.events} />
<ActionsSection orderId={selectedId} />
</div>
</SheetContent>
</Sheet>
The table stays visible behind the panel. The operator can glance at the list while reviewing a detail. They close the panel and they're exactly where they were.
The Business Case
- Operator throughput. An internal tool optimized for data density and keyboard shortcuts lets an operator process 200 items in the time it takes to process 120 with a card-based, mouse-driven design. That's a 40% throughput increase with zero additional headcount.
- Reduced context-switching. When all the information fits on one screen, operators stop opening multiple tabs, copying to spreadsheets, and printing reports. Every workaround eliminated is minutes saved per day, hours saved per week.
- Retention of ops talent. Good operators leave when their tools frustrate them. An internal tool that respects the power user's workflow — keyboard shortcuts, dark mode, high density — is a retention tool disguised as a feature.
Try It
Install the Modh Playbook skills to enforce this pattern automatically:
# Add to your project
git submodule add https://github.com/modh-labs/playbook .agents/modh-playbook
./.agents/modh-playbook/install.sh