Skip to main content
VMSpark logo
Case study · File no. 05

from the workbench —

A Rails shell,
rebuilt for 2026 .

A shell redesign for VMSpark — the multi-tenant pipeline that normalizes thousands of VMS jobs a day and fans them out to a dozen+ ATS providers — raising the visual caliber of the admin app without touching the pipeline underneath.

Role
Design + Eng
Client
VMSpark
Stack
Rails · Hotwire
Focus
Shell · Tokens

On the docket

VMSpark normalizes thousands of VMS job listings a day, runs them through an account-specific rule engine, and fans them out to whichever ATS the customer has contracted with. The product works. The shell it lived in did not look like it. Leadership was scheduling investor demos, and the admin app — the surface those conversations lived inside — felt like a Rails scaffold from 2018. Operators noticed it daily. Investors noticed it in the first thirty seconds of a demo.

The brief I gave myself: make the shell read as a modern, enterprise-grade product that matches the sophistication of the pipeline underneath — without touching the pipeline, the rule engine, or anything an operator had muscle memory for.

Step one

Inventory before invention

Before writing a line of new CSS, I cataloged what was there. The result was a ~700-line design inventory covering every one of the 350 ERB templates — roughly 11,800 lines — across the app.

The audit wasn't a complaint list. It was the map that told me which problems were cosmetic (fixable during the redesign), which were structural (needed architectural decisions), and which were out of scope (pipeline-adjacent, don't touch). Most of what I found was UI sprawl: the same interaction rendered a dozen different ways across the app, each screen a small argument against the one before it.

A few of the findings

Button styles competing for attention

Primary, secondary, danger, and ghost variants — each implemented two or three different ways. Some leaned on Bootstrap utility classes, others on per-view custom CSS, a few were inline-styled anchors pretending to be buttons. A Save on one screen didn't match a Save two clicks away.

Form element styles, all at once

Plain Rails helpers sat next to Bootstrap form-control inputs, Select2 widgets, and hand-rolled dropdowns. Border radius, padding, focus rings, and disabled treatments all diverged. The same concept — pick one from a list — rendered five different ways across the app.

Form-rendering patterns, coexisting

form_with(model:), legacy form_for, form_with(url:), bare <form> + HTMX, and HTMX-only with hx-include.

Tables with no shared convention

Row striping in some, not in others. Headers styled four ways: dark fill, light fill, borderless, and all-caps. Pagination rendered with Kaminari on one screen and a bespoke HTMX block on the next. Density jumped from compressed to roomy between adjacent views with no underlying logic.

Labeling strategies, all coexisting

Sentence case, Title Case, ALL CAPS, and placeholder-as-label all appeared across the app. Required indicators were sometimes asterisks, sometimes parenthetical (required), sometimes absent. The same field concept carried different names on different screens (Bill Rate, Rate, Pay Rate).

Views bypassing the token system

A semantic color-token system already existed in _color_tokens.scss, but dozens of views bypassed it with inline styles, hardcoded hex literals, and raw colors in view-level <style> blocks.

Dead code in production

A zero-byte _empty.html.erb, an X-prefixed dead index file (101 lines), an ASDKAJSHDJHASD debug string committed to production, and a sticky table header with hardcoded background-color: red from an abandoned debug session.

What the sprawl actually looked like

Four screens pulled from the same app, on the same day. Each one makes its own decisions about buttons, inputs, labels, and tables — and none of them agree with the others.

VMSpark Manual Jobs screen. Three buttons sit side by side in the top bar: a filled orange Add New, an outlined Edit Selected, and an outlined Copy Selected. Search inputs appear above the table with labels placed below each field.
Manual Jobs. Three buttons in one row, three different treatments. Labels sit below inputs.
VMSpark ATS Professions screen. Five search inputs use their label text as placeholders inside the fields. A Sync with ATS outlined button sits at the top right.
ATS Professions. Labels are now placeholders inside the inputs. Different page, different rule.
VMSpark Announcements screen. Each row ends with a dark gray pencil edit icon next to a solid red trash icon. A filled orange New Announcement button sits in the top right.
Announcements. Yet another icon-button pattern — dark gray paired with solid red, nothing like the blue outlines elsewhere.
VMSpark OpenAI Usage dashboard. A data table with a solid dark-navy header bar, right-aligned numeric columns, and underlined orange account names.
OpenAI Usage. A solid dark-navy table header. Every other table in the app uses a light gray one.

With the inventory in hand, I could fix the UI sprawl at the root rather than one screen at a time. Buttons collapsed to a single set of component classes. Form inputs, selects, and labels moved onto shared primitives with consistent padding, focus states, and required-field treatment. Tables standardized on one header style, one pagination component, one density rule. Each fix cascaded across dozens of views because the semantic token layer was already in place — I just needed the views to route through it.

Step two

Three directions, one chosen

Rather than design in a vacuum, I spec'd three mutually exclusive directions with honest tradeoffs. Writing the options down — with their reasons and risks — is what lets the choice get overturned on evidence later, not preference.

A Command Shell

Dense · Keyboard-first

56 px icon rail, tight 13/14 px type, hairline borders, ⌘K palette as the centerpiece, orange reserved for exactly one thing per screen.

Signals “scales to power users.”

Chosen
B Two-Tier Platform

Enterprise-familiar

48 px product rail plus 260 px project nav — the real two-tier pattern. App-switcher waffle, breadcrumbed page headers, right-side inspector panel. Brand orange on active rail state, soft navy-tinted elevation.

Every enterprise buyer has used this pattern before.

C Modern Workspace

Spacious · Document-forward

270 px soft sidebar, no persistent top bar, ⌘K by shortcut only, generous spacing.

Risk: reads as lightweight for a dense ops tool.

I chose B. Not because it was the prettiest (C was), and not because it was the most sophisticated (A was). I chose it because the product's audience was multi-tenant operators, and the lowest-risk path to both user and investor trust was the pattern those operators had already used at every previous job.

I also dropped a feature I'd originally scoped in: a global "Create" button. Most creates in this product are feed-driven, not user-initiated. Adding one would have been copying a convention without earning it.

Pattern familiarity compounds trust faster than novelty does.

Step three

Phased rollout

Each phase was shippable on its own, behind no flags, with the previous phase's work still intact if I needed to roll back.

Phase 1 · Baseline

Visual baseline

Built a _page_header partial and page_header helper with meta/tabs/actions slots, backfilled ~30 pages silently by having the legacy partial delegate to the new helper. Added an environment/account badge in the top bar — an explicit multi-tenant signal at a glance.

Phase 2 · The shell

Two-tier shell

Replaced a single always-expanded 300 px sidebar with a 56 px product rail + 280 px contextual panel keyed off controller_name via a SidebarHelper mapping. Crucially: server-rendered active state — no client JS decides which section is open on first paint.

Phase 3 · ⌘K

Command palette

A read-only CommandSearchController#index scoped to current_account, grouped (Custom Rules, Rate Plans, Facilities, Accounts, Jump-to), debounced at 150 ms, rendered via a native <dialog> with HTMX. Deliberate constraint: the palette does not replace per-page search. It's additive.

Phase 4 · Polish

Density, shortcuts, inspector

  • Density toggle persisted on the user record, read pre-paint from <head> to avoid FOUC.
  • Shortcut overlay (?) gated to skip when a form field has focus.
  • Inspector slide-over built with HTMX to match the rest of the app.
  • Skeleton loader primitive honoring prefers-reduced-motion.

Step four

Outcome

The app now reads as the same caliber product it always was. The pipeline, the rule engine, the ATS integrations, the account scoping — none of that changed. A user who logged in the day of the cutover saw their data, their filters, their scroll position, and their workflows intact. They just saw them inside a shell that looks like it belongs in 2026.

That shift paid off on both sides of the business. Operators who spent every working day inside the tool stopped mentally filtering out visual noise — buttons, forms, tables, and labels finally behaved the same from screen to screen, which made the product feel deliberate and trustworthy rather than patched together. Leadership stopped apologizing for the admin UI in investor demos. The audit is what made that possible: a map of every inconsistency first, a disciplined fix second, confidence on both sides of the table third.

Zero pipeline regressions

No regressions reported against the ingest or fan-out paths after cutover.

Consumer confidence

Operators use a tool that looks like one product, not ten. The UI stopped fighting them at every screen.

Investor confidence

Demos open with the product, not with caveats about the surface it lives on.

No new JS on hot paths

Palette and inspector load on demand. First paint got lighter, not heavier.

A detail worth calling out

Aligning the app with the marketing site

Partway through Phase 2 I pulled the marketing site's :root custom properties — five brand colors, pinned — and re-pointed the app's semantic token layer at them. The semantic layer didn't change shape; I re-pointed five values and added three new tokens. Every existing consumer got the new look for free. Two non-obvious behaviors made it across intentionally: primary button hover swaps orange to steel blue rather than darkening, and link hover goes near-black instead of lighter — both inherited from the marketing brand.

The shell is the surface operators use every day and the one investors see in a demo. The audit is what let me rebuild it without breaking the product underneath.

from the desk of —

What Jon said

Josh inventoried 350 ERB templates before he wrote a line of new CSS, then turned that audit into a phased rollout we could ship behind no flags. The shell finally reads like the enterprise product the pipeline always was — and operators didn't lose a single piece of muscle memory in the move.
Jon Sturm

Jon Sturm

Director of Product & Customer Success · VMSpark

Got a shell that looks older than the product?

Start with an audit. End with a shell that earns the product.

A 2-week UI audit gives you the map. The redesign comes after, not before. Start with a free scorecard, or grab 30 minutes.