modern-react-spa

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>
  <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.

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

  • Table2 virtualises rows and columns automatically. Custom cell renderers must be cheap — they run during scroll. No useEffect in cell renderers.
  • Tree with 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.memo over-use — the React 19 compiler usually handles it (Ch 2 §2.1). Profile before manually memoising.

🔹 10.7 shadcn/ui vs Blueprint — picker

Concernshadcn/uiBlueprint.js
Visual densityComfortable, modernDense, IDE-style
DistributionCopy source into your repoVersioned npm packages
CustomisationFree (you own the code)Token overrides + class targeting
AccessibilityVia Radix primitivesStrong, hand-tuned
Bundle weightPay-as-you-goHeavier baseline (CSS + icon font)
Right fitProduct UIs, marketing, contentInternal tools, IDE-style, data-dense
Hotkeys / multi-pane / context menusHand-rolledFirst-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

  1. Strict CSS scoping — the Blueprint area lives under a class boundary (bp5-root); the shadcn area under another (.app-modern).
  2. Shared tokens (Ch 8.5) feed both.
  3. One icon library wins per region — Blueprint Icons in Blueprint regions, Lucide in shadcn regions.
  4. Z-index policy: Blueprint overlays use Blueprint’s portal; shadcn uses Radix’s. Document the ordering.
  5. One source of truth for theme — both honour the same .dark class 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

  1. Using Blueprint for a public marketing page → fights the brand voice.
  2. Importing Blueprint CSS after Tailwind preflight → silent overrides.
  3. Skipping HotkeysProvideruseHotkeys no-ops silently.
  4. Two icon systems used interchangeably within a region → visual chaos.
  5. Trying to make Blueprint look “modern shadcn” → fight the library, lose.
  6. Mixing portals from Blueprint and Radix without a z-index policy.
  7. Expensive work in Table2 cell renderers → frame drops on scroll.

✅ Recap

  • Blueprint is the right pick for dense, desktop-style, keyboard-heavy apps.
  • Setup is global CSS imports + a BlueprintProvider at 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

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.

Topics in this chapter (8)
  1. 10.1 What Blueprint.js is for — and what it isn’t
  2. 10.2 Setup on React 19.2 + Vite + TS
  3. 10.3 Core components — eight that matter
  4. 10.4 Blueprint Icons
  5. 10.5 Theming and design tokens
  6. 10.6 Performance for dense data
  7. 10.7 shadcn/ui vs Blueprint — picker
  8. 10.8 The hybrid pattern — Blueprint shell + shadcn product surface