Chapter 10
Desktop & Shell UIs with Blueprint.js
Blueprint.js for desktop-style React SPAs — when it fits, setup on Vite + React 19.2, core components, theming, and the hybrid Blueprint + shadcn pattern.
Published 2026-05-23
🎯 Chapter Goal — After this chapter you can recognise when an app is “Blueprint-shaped” vs “shadcn-shaped”; set up Blueprint.js on React 19.2 + Vite; build a desktop-style shell (Navbar, Tree sidebar, Table2 main panel, ContextMenu, HotkeysProvider); and combine a Blueprint shell with a shadcn product surface in the same SPA without visual chaos.
🧭 Prerequisites — Ch 7 (shadcn) and Ch 8 (tokens) for the hybrid pattern at the end. Familiarity with React 19.2 (Ch 2).
Canonical reference: https://blueprintjs.com/ (Palantir-maintained).
🔹 10.1 What Blueprint.js is for — and what it isn’t
Picking the wrong library wastes weeks. Blueprint is a category of UI, not a competitor to shadcn.
Blueprint is for:
- Dense, IDE-style internal tools (Foundry, observability consoles, BI tools, admin panels with 50+ columns).
- “Desktop on the web” — keyboard-first workflows, multi-pane, context menus, hotkeys.
- Apps where the user spends 4+ hours per day in front of the UI.
Blueprint is not for:
- Marketing pages, e-commerce, content sites — too dense.
- Mobile-first product UIs — Blueprint’s density doesn’t translate.
- Apps that want a distinctive brand voice — Blueprint has a strong, opinionated look that resists rebranding.
Reference customers: Palantir Foundry, Hex, Mode, many internal-only ops consoles you’ve never seen.
┌─ Acme Ops Console (Blueprint-shaped) ─────────────────────────────┐
│ ☰ File Edit View Run Help [ ⚙ shr ▾]│
├────────────┬──────────────────────────────────────────────────────┤
│ ▾ Projects │ Query: SELECT * FROM events WHERE … [ Run ] [ ⏵ ] │
│ ▾ acme │ ┌──────────────────────────────────────────────────┐ │
│ ▸ etl │ │ id │ ts │ tenant │ event │ │
│ ▸ api │ │ 10421 │ 2026-05-22 12:00:01 │ acme │ login │ │
│ ▾ shared │ │ 10422 │ 2026-05-22 12:00:03 │ foo │ purchase │ │
│ │ └──────────────────────────────────────────────────┘ │
├────────────┴── ⓘ 1,402 rows · 18 ms · ⌘K for actions ─────────────┤
└───────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
<Tree> <Table2> <HotkeysProvider>
🔹 10.2 Setup on React 19.2 + Vite + TS
npm install @blueprintjs/core @blueprintjs/icons @blueprintjs/table @blueprintjs/select
📄 src/main.tsx
import 'normalize.css/normalize.css';
import '@blueprintjs/core/lib/css/blueprint.css';
import '@blueprintjs/icons/lib/css/blueprint-icons.css';
import '@blueprintjs/table/lib/css/table.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BlueprintProvider } from '@blueprintjs/core';
import { App } from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BlueprintProvider>
<App />
</BlueprintProvider>
</StrictMode>,
);
<BlueprintProvider> is the umbrella that wires up HotkeysProvider, OverlaysProvider, the toaster, and the portal target. Use it at the root unless you have a specific reason to compose providers individually.
⚠️ Blueprint imports global CSS. If you also use Tailwind, load Blueprint’s CSS first, then Tailwind preflight — otherwise Tailwind’s reset overrides Blueprint defaults. The order matters.
🔹 10.3 Core components — eight that matter
Navbar
<Navbar>
<NavbarGroup align="left">
<NavbarHeading>Acme Ops</NavbarHeading>
<NavbarDivider />
<Button minimal icon="folder-open">Projects</Button>
</NavbarGroup>
<NavbarGroup align="right">
<Button minimal icon="cog" />
<Button minimal icon="user" />
</NavbarGroup>
</Navbar>
Card
<Card elevation={1} interactive onClick={openInvoice}>
<H4>INV-1042 · Acme Co.</H4>
<p>Amount: $1,290</p>
</Card>
Tree — lazy-loaded children
const [nodes, setNodes] = useState<TreeNodeInfo[]>(initialNodes);
const onExpand = async (node: TreeNodeInfo) => {
if (!node.childNodes) {
const children = await fetchChildren(node.id);
node.childNodes = children;
}
node.isExpanded = true;
setNodes([...nodes]);
};
<Tree contents={nodes} onNodeExpand={onExpand} onNodeCollapse={(n) => { n.isExpanded = false; setNodes([...nodes]); }} />
Table2 — the crown jewel for dense data
import { Table2, Column, Cell } from '@blueprintjs/table';
<Table2 numRows={data.length} enableRowResizing enableColumnResizing>
<Column name="ID" cellRenderer={(r) => <Cell>{data[r].id}</Cell>} />
<Column name="Tenant" cellRenderer={(r) => <Cell>{data[r].tenant}</Cell>} />
<Column name="Amount" cellRenderer={(r) => <Cell>${data[r].amount.toLocaleString()}</Cell>} />
</Table2>
Virtualises rows and columns automatically. Region selection (Excel-style click + drag). Resizable columns. Edit-in-place via custom cell renderers.
Dialog and Drawer
<Dialog isOpen={open} onClose={() => setOpen(false)} title="Confirm action">
<div className={Classes.DIALOG_BODY}><p>Are you sure?</p></div>
<div className={Classes.DIALOG_FOOTER}>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button intent="danger" onClick={proceed}>Delete</Button>
</div>
</Dialog>
Drawer for side panels. Different mental model from shadcn Sheet — Blueprint’s drawer takes the whole screen height.
Menu and ContextMenu
const Items = (
<Menu>
<MenuItem icon="document-open" text="Inspect" onClick={inspect} />
<MenuItem icon="duplicate" text="Copy ID" onClick={copy} />
<MenuDivider />
<MenuItem icon="trash" intent="danger" text="Delete" onClick={del} />
</Menu>
);
<ContextMenu content={Items}>
<div className="row">…</div>
</ContextMenu>
Right-click menus the desktop-app way. Submenus via nested <Menu> inside <MenuItem>.
HotkeysProvider + useHotkeys
const hotkeys = useMemo(() => [
{ combo: 'mod+k', label: 'Command palette', onKeyDown: openCmdK, global: true },
{ combo: 'mod+f', label: 'Find', onKeyDown: openFind, global: true },
{ combo: 'mod+r', label: 'Refresh', onKeyDown: refresh, global: true },
], []);
const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys);
<div tabIndex={0} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp}>{children}</div>
? opens the hotkeys-help dialog automatically. Keyboard-first apps live or die by this provider.
🔹 10.4 Blueprint Icons
500+ icons in the Blueprint Icons set. Two delivery modes:
- Icon font (legacy default) —
<Icon icon="folder-open" />. Loads the whole font file. - SVG components (preferred for tree-shaking) —
import { FolderOpen } from '@blueprintjs/icons';.
Pairing strategy: Blueprint icons in Blueprint chrome regions; Lucide / Phosphor inside content regions (cross-link Ch 8.1). Don’t mix within a region.
🔹 10.5 Theming and design tokens
Toggle dark mode by adding bp5-dark to the root element:
<html class="bp5-dark">
Blueprint 6+ exposes CSS variables for the core palette. Override them in your global CSS:
:root {
--pt-intent-primary: var(--color-primary); /* from your shared tokens (Ch 8.5) */
--pt-intent-danger: var(--color-danger);
}
The bridge to your shared token system (Ch 8.5): one --color-primary feeds Tailwind utilities, shadcn components, and Blueprint intents. Same value, three consumers.
Custom intents: Blueprint doesn’t have a first-class “brand” intent; emulate by overriding --pt-intent-primary and styling the few non-token-driven exceptions (focus rings, badge text colours) manually.
🔹 10.6 Performance for dense data
Table2virtualises rows and columns automatically. Custom cell renderers must be cheap — they run during scroll. NouseEffectin cell renderers.Treewith thousands of nodes: lazy-load children (10.3 above); for truly large trees, flatten and virtualise with@tanstack/react-virtual(cross-link Ch 32.3).- Avoid
React.memoover-use — the React 19 compiler usually handles it (Ch 2 §2.1). Profile before manually memoising.
🔹 10.7 shadcn/ui vs Blueprint — picker
| Concern | shadcn/ui | Blueprint.js |
|---|---|---|
| Visual density | Comfortable, modern | Dense, IDE-style |
| Distribution | Copy source into your repo | Versioned npm packages |
| Customisation | Free (you own the code) | Token overrides + class targeting |
| Accessibility | Via Radix primitives | Strong, hand-tuned |
| Bundle weight | Pay-as-you-go | Heavier baseline (CSS + icon font) |
| Right fit | Product UIs, marketing, content | Internal tools, IDE-style, data-dense |
| Hotkeys / multi-pane / context menus | Hand-rolled | First-class |
Pick by category, not by aesthetics. Density and workflow tell you the answer.
🔹 10.8 The hybrid pattern — Blueprint shell + shadcn product surface
Real-world internal SaaS often has both:
- An operator console (dense, keyboard-heavy) — Blueprint.
- A customer-facing settings & billing UI embedded in the same SPA — shadcn.
The rules to make this work
- Strict CSS scoping — the Blueprint area lives under a class boundary (
bp5-root); the shadcn area under another (.app-modern). - Shared tokens (Ch 8.5) feed both.
- One icon library wins per region — Blueprint Icons in Blueprint regions, Lucide in shadcn regions.
- Z-index policy: Blueprint overlays use Blueprint’s portal; shadcn uses Radix’s. Document the ordering.
- One source of truth for theme — both honour the same
.darkclass on<html>.
┌─ Acme Ops ────────────────────────────────────────────────────────┐
│ Console │ Settings │
│ (Blueprint region) │ (shadcn region) │
│ ┌──────────────────┐ │ ┌─────────────────────────────────┐ │
│ │ Table2: events │ │ │ Profile Tenants Billing │ │
│ │ ⌘K ⌘P ⌘F │ │ │ │ │
│ └──────────────────┘ │ │ [shadcn Form + Dialog] │ │
│ │ └─────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
The pattern survives the long term only if the two regions stay visually distinct. Don’t try to make Blueprint look like shadcn or vice versa — you’ll fight the library and lose.
🪤 Common Pitfalls
- Using Blueprint for a public marketing page → fights the brand voice.
- Importing Blueprint CSS after Tailwind preflight → silent overrides.
- Skipping
HotkeysProvider→useHotkeysno-ops silently. - Two icon systems used interchangeably within a region → visual chaos.
- Trying to make Blueprint look “modern shadcn” → fight the library, lose.
- Mixing portals from Blueprint and Radix without a z-index policy.
- Expensive work in
Table2cell renderers → frame drops on scroll.
✅ Recap
- Blueprint is the right pick for dense, desktop-style, keyboard-heavy apps.
- Setup is global CSS imports + a
BlueprintProviderat the root. - Table2, Tree, HotkeysProvider, ContextMenu are the differentiators.
- shadcn/ui and Blueprint can coexist with strict scoping, shared tokens, and one icon system per region.
- Pick by category, not by aesthetics.
🔗 Further Reading
- https://blueprintjs.com/ — Blueprint v5.x docs.
- https://blueprintjs.com/docs/#table — Table2 reference.
- https://blueprintjs.com/docs/#core/components/hotkeys-dialog2 — HotkeysProvider.
- https://github.com/palantir/blueprint — repo, issues, release notes.
- Ch 7 (shadcn), Ch 8.5 (shared tokens), Ch 32.3 (virtualisation).
In the book — not on the site
Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.