Skip to main content
JoshuaBriley.
VMSpark logo

Case study

Redesigning the app on top of a system that already worked.

VMSpark takes in thousands of job listings a day and automatically sends them to more than a dozen hiring systems that companies use. The app operators worked in every day, and the one investors saw in every demo, hadn't kept up with how sophisticated that system had become. I reviewed the interface, chose a new direction, and rebuilt it in stages, without changing anything underneath it.

TL;DR

Situation
VMSpark pulls in thousands of job listings a day and sends them out to more than a dozen hiring systems. I owned the admin app operators used every day and investors saw in every demo, without touching the systems underneath it.
Problem
The app hadn't kept up with what it did. Buttons, forms, and tables were each built a different way on nearly every screen, so it looked more like an old prototype than the enterprise product it powered.
Alternatives
  • A dense, keyboard-first layout

    Built for speed once you know it, but it asks operators to learn a pattern they'd never used before.

  • A familiar two-panel layout

    The same kind of layout operators had already used at other companies, at the cost of feeling less original.

Decision
I chose the familiar layout over the more original one, since the safest way to earn trust was a pattern operators already knew.
Implementation
The rebuild shipped in four separate stages, each one safe to release on its own, ending with the app's colors matched to the same five brand colors already used on the public website.
Evidence
No pipeline problems after launch. Operators stopped noticing visual inconsistencies between screens, and leadership stopped apologizing for the app during demos.
Reflection
A manual review caught anything visible on a still screen, but every pop-up window in the app turned out to be built a different way, and that only showed up once I built a small tool to scan for it. Next time, that tool runs first.

Receipts

Skills proven
  • Design-system audit
  • UI/UX design direction
  • Accessible component APIs
  • Frontend architecture
  • Style-guide documentation
  • Product decisions
Stack
  • Ruby on Rails
  • Stimulus.js
  • Design tokens
  • Modern CSS
Quality
  • Keyboard operability
  • Focus management
  • ARIA semantics
  • Token contrast checks
Outcomes
  • Zero pipeline regressions
  • No operator retraining
  • One token layer, app and brand
  • No new JS on hot paths

Table of contents

My goal: make the admin app look like the modern, enterprise-grade product it already was underneath, without touching the systems, the client-specific rules, or anything an operator already knew by muscle memory. That meant owning it end to end: the audit, the decisions about what to build and what to drop, and the engineering to ship it.

Step 01

Taking stock before changing anything

Sprawl this wide can't be fixed one screen at a time. Restyle the buttons on one screen and seven other button styles are still waiting elsewhere, with no way to know you've found them all. So before changing a single style, I wrote down everything I found: a full inventory covering every one of the 350 page templates in the app.

That inventory worked as a map. It showed me which problems were easy fixes, which needed a real architectural decision, and which were outside the scope of the redesign entirely. Most of it was the same interaction built a dozen different ways across the app.

350

ERB templates inventoried

8+

button styles, one concept

10+

form-element styles

~40

views bypassing the tokens

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

The inventory is what made a real fix possible instead of a screen-by-screen patch. Each finding above resolved to one decision, made once, and applied everywhere it was needed.

Step 02

Choosing a direction

I wrote up three possible directions, each with its own honest tradeoffs, instead of just picking one. Writing the options down made the choice easy to question later, based on evidence rather than taste.

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. C looked nicer, and A was more sophisticated, but operators worked across many client accounts every day, and the safest way to earn their trust was a layout they'd already used at other jobs.

I also dropped a feature I'd originally planned: a global "Create" button. Most listings arrive automatically from the incoming data feed, so people rarely create one by hand. Adding the button would have copied a familiar pattern that didn't actually fit here.

Familiarity beat novelty here. Operators had already seen this layout, at other jobs.

Step 03

Rolling it out in stages

This was a live product with operators in it every working day, so an all-at-once redesign that broke their muscle memory was never on the table. Each phase below could ship on its own, and could be safely rolled back without undoing the phase before it.

Phase 1 · Baseline

Visual baseline

Built one reusable page-header component that every screen could use for its title, tabs, and buttons, then quietly rolled it out across roughly 30 existing pages without anyone needing to touch each page by hand. Also added a badge in the top bar showing which environment and which client account you're in, so it's obvious at a glance.

Phase 2 · The shell

Two-tier shell

Replaced one large, always-open sidebar with a slim icon rail plus a second panel that automatically shows the options relevant to whatever section you're in. Crucially, the page already knows which section is highlighted the instant it loads. There's no flash or delay while extra code figures that out afterward.

Phase 3 · ⌘K

Command palette

A search box you can summon from anywhere with a keyboard shortcut, scoped to whichever client account you're currently working in, with results grouped into categories (custom rules, rate plans, facilities, accounts, jump-to) and updating smoothly as you type. Deliberate constraint: the palette does not replace the search box already on each page. It's an extra option, not a replacement.

Phase 4 · Polish

Density, shortcuts, inspector

  • A compact-or-roomy layout setting, saved to your account and applied before the page even finishes loading, so there's no visible jump.
  • A shortcut cheat-sheet (press "?") that only appears when you're not currently typing in a form field.
  • A slide-out detail panel built to match the look and feel of the rest of the app.
  • Loading placeholders that respect a visitor's "reduce motion" setting, so they don't shimmer or pulse for anyone who's asked their device to limit animation.

What it looks like now

One system, documented in the app

Buttons collapsed down to one consistent style instead of eight competing ones. Forms, tables, and labels all moved onto the same shared building blocks. All of it lives in a style guide that ships with the app, so the next person to build a screen inherits those decisions instead of reinventing them.

Plate 01

VMSpark admin UI playground: the style-guide overview indexing every primitive
Full page Style guide: overview

The admin-only style guide: every building block, button, table, and form pattern documented in one place.

Plate 02

VMSpark UI primitives: color ramps, type scale, spacing, and elevation
Full page Primitives

Color, type, spacing, and shadow depth: the basic building blocks every screen now draws from.

Plate 03

VMSpark button system: variants, sizes, states, and density from one set of classes
Full page Buttons

The eight-plus button styles from the audit, resolved into one consistent set covering every type, size, and state.

Plate 04

VMSpark table system: one header style, one density rule, one pagination component
Full page Tables

One header style, one spacing rule, one page-navigation control, standardized across every table.

Screens and flows

What actually got built

Eleven screens and five flows, with the reasoning written onto each one. These are reconstructions, drawn afterward to document the decisions.

How a screen got built

App shell, rough hand-drawn concept sketch.

Sketch

Arguing about the shape.

App shell, clean wireframe without annotations.

Wireframe

Fixing it.

Annotated design sheet of the VMSpark app shell: icon rail, nav sidebar, workspace switcher, command palette and impersonation control, with callouts explaining why tenant identity lives in the chrome and why the token path had to be the shortest one.

Annotated

The reasoning written on top.

The passes run left to right: a rough sketch, a wireframe, then the real screen in color, since arriving at one visual language was the whole point of this project.
Annotated design sheet of the VMSpark app shell: icon rail, nav sidebar, workspace switcher, command palette and impersonation control, with callouts explaining why tenant identity lives in the chrome and why the token path had to be the shortest one.
01App shell

One shell applied to every authenticated screen. The sprawl came from every view having somewhere of its own to decide.

Annotated design sheet of the job listings grid: per-column filter inputs, status tabs carrying queue depth, and a tinted failed row among the healthy ones, with callouts on queued reprocessing and per-user column persistence.
02Job listings

The primary triage surface. Status is a tab count, and a failed row stays in the list rather than being exiled to another screen.

Annotated design sheet of the listing inspector: an overlay opening over the filtered grid, showing the mapped result beside the raw VMS payload with the unresolved facility field marked on both sides, plus a timestamped process history.
03Inspector

Why one listing ended up in the state it did. Raw payload beside mapped result, so a failure is self-explaining rather than a support ticket.

Annotated design sheet of the data mapping screen: entity-type tabs carrying unmapped counts, and two-column resolution cards pairing each inbound VMS value with its ATS candidates, with callouts on auto-mapping, focus advance, and why Ignore is a real decision.
04Data mapping

The operational centre. Unmapped values are a revenue problem, not a tidiness problem, so the queue is built to visibly empty.

The rest of the system

Seven more screens, ending with the inventory that replaced eight button styles, ten form styles, and six table conventions with one of each.

  • Annotated design sheet of the custom rules screen.
    Custom rules
  • Annotated design sheet of the candidate profiles screen.
    Candidate profiles
  • Annotated design sheet of the applications & matches screen.
    Applications & matches
  • Annotated design sheet of the settings screen.
    Settings
  • Annotated design sheet of the candidate portal screen.
    Candidate portal
  • Annotated design sheet of the ai usage screen.
    AI usage
  • Annotated design sheet of the the design system screen.
    The design system

User flows

Five flows covering the paths the screens sit on, each one splitting what a person does from what the system does with it.

User flow diagram: ingestion. The pipeline the redesign was forbidden from touching. Processed, failed and skipped are all first-class outcomes.
Ingestion

The pipeline the redesign was forbidden from touching. Processed, failed and skipped are all first-class outcomes.

User flow diagram: mapping resolution & fan-out. What an operator does to clear one unmapped facility, and what that single save costs downstream.
Mapping resolution & fan-out

What an operator does to clear one unmapped facility, and what that single save costs downstream.

User flow diagram: rule engine. Per-account rules as the escape hatch that keeps client special cases out of a shared pipeline.
Rule engine

Per-account rules as the escape hatch that keeps client special cases out of a shared pipeline.

User flow diagram: candidate matching. The system proposes, a recruiter disposes. Submission is the only externally visible step, so it is the only one that asks.
Candidate matching

The system proposes, a recruiter disposes. Submission is the only externally visible step, so it is the only one that asks.

User flow diagram: user journeys. Three people with opposite needs, one shell, and a mapping queue upstream of all of them.
User journeys

Three people with opposite needs, one shell, and a mapping queue upstream of all of them.

Step 04

What changed after launch

None of the systems underneath changed: the pipeline, the client-specific rules, the connections to outside recruiting systems. A user who logged in the day the new version went live saw their data and their workflows intact, just inside a shell that finally looked current. Operators stopped noticing visual inconsistency between screens, and leadership stopped apologizing for the app in investor demos.

Zero pipeline regressions

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

Operator 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 two, I pointed the admin app's color system at the same five brand colors already used on the public website, instead of its own separate set. Every screen that used that system picked up the new look automatically. Two small details carried over on purpose: the main button turns steel blue on hover instead of just getting darker, and a link goes nearly black, matching how the website already behaved.

Plate 01

The VMSpark marketing site, the source of the five pinned brand colors the admin app's token layer was re-pointed at
Live site vmspark.com

What Jon said

Jon Sturm

Jon Sturm

Director of Product & Customer Success · VMSpark

Josh inventoried 350 ERB templates before writing 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.

The edges

Both of these are the kind of problem a page-by-page review, done by hand, is likely to miss: patterns that only show up when you interact with something, not on a screen you can just look at.

  • Pop-up windows the inventory missed

    The audit counted button styles, forms, and tables because those repeat across screens you can scan by eye. Pop-up windows don’t: each one only appears when triggered. Every pop-up in the app turned out to be built a different way, with its own custom code. Tooltips had the same problem. Both got folded into the rebuild, but neither showed up in the original inventory.

  • Finding the rest took a tool, not just eyes

    A manual pass through 350 page templates finds the patterns that repeat often enough to notice, but misses a one-off pop-up or an abandoned approach hiding in a screen nobody opens often. I built a small tool that scans the app for what each piece of the interface is trying to do, rather than just its code, and that's what actually found the dialogs and tooltips the manual audit missed. Next audit, that tool runs first.

Product design engineering

A full rebuild, shipped in stages, with nothing broken along the way.

Design and the interface are where I add the most value, backed by enough hands-on engineering to ship the whole thing finished. That combination is what let this rebuild land on a live product, serving many client companies at once, without ever needing a plan to undo it. If that's the bar you're hiring for, let's talk.