Initial draft is done! Design updated and extremely close to final; new hero artwork added; improvements to screen size handling across the board

This commit is contained in:
Andrew Gioia 2026-04-28 11:07:59 -04:00
parent 8020060405
commit 2b740a7000
Signed by: andrew
GPG Key ID: FC09694A000800C8
26 changed files with 1169 additions and 334 deletions

File diff suppressed because it is too large Load Diff

1
assets/icons/cc.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M10 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"/><path d="M17 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"/></svg>

After

Width:  |  Height:  |  Size: 377 B

1
assets/img/kangaroo.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M108.836,156.725l-26.108,-31.053l37.823,-0l0,66.284l-11.715,0l0,-35.23Z" style="fill:#1a52a3;"/><path d="M132.911,4.571l25.797,25.586l-16.538,9.548l-9.26,-35.134Z" style="fill:#1a52a3;"/><path d="M164.608,11.933l-0.865,23.009l-13.442,-4.968l14.307,-18.041Z" style="fill:#e62a1b;"/><path d="M159.495,27.851l30.384,20.669l-30.384,0.219l0,-20.888Z" style="fill:#272727;"/><path d="M120.551,125.716l-69.791,0c0,-38.519 31.272,-69.791 69.791,-69.791l0,69.791Z" style="fill:#e62a1b;"/><circle cx="155.672" cy="41.663" r="14.262" style="fill:#272727;"/><path d="M108.836,191.955l46.77,0l-35.055,-12.193l-11.715,12.193Z" style="fill:#272727;"/><rect x="133.058" y="86.516" width="8.557" height="27.359" style="fill:#272727;"/><circle cx="139.452" cy="113.875" r="6.393" style="fill:#272727;"/><path d="M120.551,55.926l37.605,0c0,20.755 -16.85,37.605 -37.605,37.605l0,-37.605Z" style="fill:#fcb70d;"/><path d="M50.76,125.716c0.073,10.842 -5.416,56.45 -48.638,66.284c35.145,-3.122 60.636,-17.345 73.575,-48.975c2.177,-5.322 3.845,-11.589 4.234,-17.309c0,0 -29.171,-0.101 -29.171,0Z" style="fill:#fcb70d;"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@ -0,0 +1,13 @@
<svg width="100%" height="100%" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<path d="M108.836,156.725l-26.108,-31.053l37.823,-0l0,66.284l-11.715,0l0,-35.23Z" style="fill:#1a52a3;"/>
<path d="M139.353,3.827l18.445,32.695l-18.445,4.942l0,-37.637Z" style="fill:#1a52a3;"/>
<path d="M158.554,13.117l-2.135,23.356l-12.411,-7.165l14.545,-16.191Z" style="fill:#e62a1b;"/>
<path d="M139.353,29.039c22.508,0.008 42.61,12.25 50.526,26.887l-50.526,-0l0,-26.887Z" style="fill:#272727;"/><path d="M120.551,125.716l-69.791,0c0,-38.519 31.272,-69.791 69.791,-69.791l0,69.791Z" style="fill:#e62a1b;"/>
<path d="M108.836,191.955l46.77,0l-35.055,-12.193l-11.715,12.193Z" style="fill:#272727;"/>
<rect x="133.058" y="86.516" width="8.557" height="27.359" style="fill:#272727;"/>
<circle cx="139.452" cy="113.875" r="6.393" style="fill:#272727;"/>
<path d="M120.551,55.926l37.605,0c0,20.755 -16.85,37.605 -37.605,37.605l0,-37.605Z" style="fill:#fcb70d;"/>
<path d="M50.76,125.716c0.073,10.842 -5.416,56.45 -48.638,66.284c35.145,-3.122 60.636,-17.345 73.575,-48.975c2.177,-5.322 3.845,-11.589 4.234,-17.309c0,0 -29.171,-0.101 -29.171,0Z" style="fill:#fcb70d;"/>
<circle id="eye" cx="147.505" cy="37.061" r="4.32" style="fill:#fff;"/>
<circle id="pupil" cx="148.576" cy="37.061" r="2.88" style="fill:#272727;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@ -7,7 +7,7 @@ draft: false
hero:
title: "Legacy app refresh"
deck: "Reshaping a critical but outdated school forms workflow into a calmer, clearer experience for administrators and families."
deck: "Reshaping a critical but outdated school forms workflow into a <strong>calmer, clearer experience</strong> for administrators and families."
tags:
- label: "Figma"
tone: "green"
@ -17,15 +17,16 @@ hero:
tone: "cyan"
- label: "Workflow UX"
tone: "blue"
inline_svg: kangaroo-eyes.svg
study:
facts:
- label: "Role"
value: "Product Design"
value: "UI/UX Design"
icon: "hat"
- label: "Timeline"
value: "Est. 6 months, expanded to 18 months"
value: "Est. 6 months, <span>expanded to 18</span>"
icon: "calendar"
class: "min"
- label: "Tools"
value: "Figma"
icon: "pen"
@ -33,7 +34,7 @@ study:
value: "K-12 education SaaS"
icon: "briefcase"
- label: "Stakeholders"
value: "Product, engineering, and school operations"
value: "Product, engineering, <span>and school operations</span>"
icon: "users"
toc:
- id: "intro"
@ -78,61 +79,341 @@ At the same time, Wallabyte was also rebranding as **Schooltastic Forms**, now a
<figure>
<img src="img/overview.png" />
<figcaption>Original app screens for Wallabyte Central on desktop and mobile.</figcaption>
<figcaption>
Original app screens for Wallabyte Central on desktop and mobile, for a user with multiple children and many form requests.
</figcaption>
</figure>
{{< /overview >}}
{{< problems id="problems" title="Core problems" >}}
We identified four main UI/UX issues to target in this refresh, following my own deepdive into the product and several meetings with the product manager.
We identified <mark>**four main UI/UX issues to target in this refresh**</mark>, following my own deepdive into the product and several meetings with the product manager.
I was fortunate to share an entirely fresh set of eyes on a mature product, and it helped question a lot of assumptions that had been taken for granted over the yers. This app necessitated multiple demo accounts with different permissions and scopes, and I spent the first week cataloguing my experiences trying to perform a number of tasks from the PM.
I was fortunate to share an entirely fresh set of eyes on a mature product, and it helped question a lot of assumptions that had been taken for granted over the years. This app necessitated multiple demo accounts with different permissions and scopes, and I spent the first week cataloguing my experiences.
These four problems became the core problem statements we used to frame the work and evaluate whether the redesign was actually solving the right things. The PM initially estimated a six-month path from early design through implementation.
{{< problem
title="Forms were too hard to find and track"
image="img/overview.png"
alt="Legacy interface showing forms split across different lists and patterns"
caption="Representative legacy flow showing how requested, in-progress, and completed forms were fragmented across the experience."
title="Forms were <mark>too hard to find</mark> and track"
>}}
This was perhaps the chief user complaint and the most immediately obvious usability issue. The app distinguished forms that were _requested_ by the school to be completed, versus those _initiated_ by the end user from a collection of available forms.
Users had to remember how a form had been started just to find it again, and unfinished work was treated differently in both sections. **These distinctions didn't matter to the user** who just wanted to get in and get out.
{{< /problem >}}
<figure>
<img src="img/problem-find.png" />
<figcaption>
<p>Users needed to remember if they responded to a form request (<i>left</i>) or if they started the form from their often massive library (<i>right</i>).</p>
</figcaption>
</figure>
{{< problem
title="The app was designed for desktop, but used primarily on mobile"
image="img/overview.png"
alt="Legacy interface showing desktop-oriented structure despite mobile-heavy usage"
caption="Representative legacy flow showing structure and density that translated poorly to mobile use."
>}}
Highlighting this issue were the three distinct paths for a user to find the forms they submitted:
Despite 94% of end users completing forms on mobile devices, the app was never thought through from a mobile perspective.
On mobile, critical actions were buried below static information or long lists, pages were never optimized and often zoomed out, 2-axis scrolling was prevalent on data-dense screens, and staff members with sometimes hundreds of forms never saw critical alerts.
* **Dashboard**: at the bottom under the "Responded" section. This only showed recently submitted forms originally requested by a school.
* **Form Library**: again at the very bottom below the long list of available forms to start, containing the users self-initiated forms. This location often prevented users from ever discovering them.
* **My Account, then "All eForm Responses"**: contained only the user's "eForm" responses, which was another word for those requested by the school. This was never clear to the user, and they often felt forms were missing from this list.
{{< /problem >}}
{{< problem
title="Navigation was complex"
image="img/overview-mobile.png"
alt="Legacy mobile flow showing navigation and terminology that did not match user mental models"
caption="Representative mobile flow showing navigation and task structure shaped more by product logic than user intent."
title="Critical information and <mark>alerts were buried</mark>"
>}}
Users needed to answer simple questions like “What needs my attention?” and “Where do I go next?”, but the navigation, terminology, and underlying data model reflected internal product logic instead of user mental models.
One of the revelations during the project's research phase was that **94% of end users completed forms on mobile devices**. Despite this overwhelming split, the app was never thought through from a mobile perspective and critical information&mdash;like alerts or approval requests&mdash;were buried at the bottom below static information or long lists.
Pages were never optimized and often zoomed out, 2-axis scrolling was prevalent on data-dense screens or those with tables, and completing forms with data inputs that were not browser-native created more problems than they solved.
<figure>
<img src="img/problem-critical.png" />
<figcaption>
<p>Certain critical action items&mdash;like pending form approvals or student safety alerts&mdash;were buried at the bottom on mobile with no indication up top.</p>
</figcaption>
</figure>
{{< /problem >}}
{{< problem
title="Managing forms across multiple children, schools, or contexts was confusing"
image="img/overview-desktop.png"
alt="Legacy desktop interface showing form management across multiple children or school contexts"
caption="Representative desktop screen showing how scope and context could become unclear across children, schools, or approval states."
title="<mark>Navigation was complex</mark> and frustrating"
>}}
Parents and staff often needed to act on behalf of multiple children, schools, or approval and reporting contexts, but the interface did not make that scope clear enough. Users could easily lose track of whose information they were viewing or updating.
Users needed to answer simple questions like "What do I need to complete?" and "What is urgent for me to do," but the navigation and terminology reflected internal product logic and developer-speak instead of what made sense to the user.
* Responders found it difficult to find forms saved for later.
* Staff found it difficult to locate self-service forms, and with lists of often 50 or more, the lack of a search or filter made this impossible.
* Parents found the mobile app generally difficult to navigate.
* Staff who needed to approve certain forms were unable to see the approval once they submitted their response, and they blamed the navigation for this failure.
{{< /problem >}}
{{< /problems >}}
{{< problem
title="Managing forms across <mark>multiple children</mark> felt confusing"
>}}
Parents and staff often needed to act on behalf of multiple children or schools, but the interface didn't make it clear enough when it mattered.
Simple things made it harder for users to quickly scan, like only showing the school or group avatar instead of the student that the form was requested for. Users could easily lose track of whose information they were viewing or updating, or bounce around the app to check first.
<figure>
<img src="img/problem-multiple.png" />
<figcaption>
<p>Parents of multiple children found it difficult to quickly identify which form was for whom. Even though the names are there, the redundant avatars and group information required extra thought.</p>
</figcaption>
</figure>
{{< /problem >}}
{{< /problems >}}
{{< section id="context" title="Delivery context" >}}
The design problems themselves were clear enough, and we estimated roughly six months from early design through implementation.
A bunch of compounding delivery constraints, however, stretched this closer to _eighteen months_: distributed collaboration across many timezones, changing design direction, parallel system work, technical debt, and a product brief that changed too many times while the work was already underway.
<details name="context-pb" open>
<summary>The original brief was not good enough</summary>
<div>
<p>
This project reinforced <strong>one of my strongest product design beliefs:</strong> <mark><strong>everything flows from the product brief</strong></mark>. A well-researched and properly scoped product brief is what gives design and development a stable framework. Had this been done, <em>all of the other issues below</em> would have been totally manageable.
</p>
<p>
The original brief was directionally right but not validated enough up front, <strong>so scope changed in <em>both directions</em> as we learned more</strong>. That forced us to revisit work that had already been designed and restart discussions that should have been settled earlier.
</p>
<p>
<strong>The biggest example was multi-student handling.</strong> NYC's model&mdash;where users switched between separate student accounts&mdash;differed from how every other other customer used the product. This was well known, but because the value of that use case was not fully understood or incorporated into the brief early enough, we spent months researching and designing for cases that ultimately did not need a universal solution or would have worked for NYC's specific case.
</p>
</div>
</details>
<details name="context-tz">
<summary>A distributed team made day-to-day collaboration slower</summary>
<div>
<p>
The PM and I were based on the US east coast, while the development team was based in Australia. We made it work with weekly status calls at 8pm eastern to create some overlap, but the timezone gap slowed normal iteration.
</p>
<p>
Questions that might have been resolved in a Slack message turned into a full-day delay, and that was amplified by the fact that the team had no dedicated frontend specialist and needed more design support than usual during implementation.
</p>
</div>
</details>
<details name="context-ds">
<summary>We were redesigning the product while also creating a larger design system</summary>
<div>
<p>
At the same time of this project, <a href="/studies/honeycomb"><strong>Honeycomb, our design system</strong></a>, was under active development and beginning to show up in other platform products. This created a recurring strategic question: should this redesign fully adopt Honeycomb, or should it act as more of a stopgap refresh that preserved more of the existing product? Because leadership never fully committed to one path early enough, the design work sometimes had to serve both goals at once.
</p>
<figure>
<img src="img/context-ds@2x.png" alt="Original form card, stopgap Wallabyte refresh card, and the final Honeycomb version" />
<figcaption>
<p>Original form card (<em>left</em>), stopgap "Wallabyte refresh" version (<em>middle</em>), and the final Honeycomb card ultimately implemented.</p>
</figcaption>
</figure>
<p>
That <strong>ambiguity led me to create a temporary bridge</strong> between the legacy Wallabyte interface and Honeycomb so the team could keep moving. It helped in the short term, but it also created rework later when parts of that hybrid approach needed to be unwound.
</p>
</div>
</details>
<details name="context-ah">
<summary>Direction changed too often inside the design process</summary>
<div>
<p>
Day-to-day design direction also caused delay as visual and IA decisions were often revisited after work had already advanced, usually in ways that <strong>reflected aesthetic preference more than product or implementation reality</strong>. In practice, that meant screens changed without a shared review process, and patterns I had already aligned with engineering or the PM sometimes had to be revisited.
</p>
<p>
<strong>The app header was a perfect example of overthinking and tinkering to a fault.</strong> We had a version of the common header design ready in our design system and it made sense to introduce that part of the design system now&mdash;it wasn't that much different than the current header.
</p>
<figure>
<img src="img/context-header.png" alt="Some of the header variations that delayed progress." />
<figcaption>
<p>Some of the header variations. The beginning and end were not far off! Many hours were spent going over color and multiple account switching that ended up outside the scope.</p>
</figcaption>
</figure>
<p>
Personal preference wedged its way in and a senior design leader felt strongly that the header's background color needed to signal the user's account type (parent, staff, approver, etc.).
</p>
<p>
We spent months alone on the header design, ultimately landing on the new Honeycomb header. This was an own-goal that could've been avoided any number of ways, and perhaps most efficiently through validating early on that <strong>user awareness of their account type was not an issue at all.</strong>
</p>
</div>
</details>
<details name="context-td">
<summary>Technical debt and platform changes were taken on in parallel</summary>
<div>
<p>
The codebase itself was old and carried substantial technical debt. This alone is not a huge issue! <strong>I've spent years working with developers on old tech stacks and sometimes even prefer it.</strong>
</p>
<p>
The team, however, chose to address some foundational frontend concerns at the same time as this refresh, including introducing Tailwind to make future UI work faster and more consistent. While I understood the logic, it meant the redesign was competing with platform modernization work rather than simply being layered onto a stable front end.
</p>
</div>
</details>
{{< /section >}}
{{< section id="response" title="Response" >}}
This project did include a substantial visual refresh, and that was one of the goals outside of the usability issues. We moved the app meaningfully **closer to Honeycomb by updating larger, easier components first**, like the header, left navigation, and typography, along with some of the easier atomic elements like buttons, cards, inputs, and dropdowns.
But this was not a full design-system conversion. More complex patterns like tables and modals kept their legacy treatment, both because the project was dragging on and we didn't want to introduce too much change at once. What we did do was thoughtful and moved the app forward visually.
For this case study, the more important story is <mark><strong>how we responded to the specific UX problems</strong></mark> above. The redesign was not structured around blind design-system adoption, and it was certainly **not driven only by my own visual taste**. In a few key places&mdash;like the notifications-and-navigation widget on mobile and desktop&mdash;we introduced patterns created specifically for this product because **user testing supported them so strongly**. NYC users in particular responded extremely well, and keeping that work was a clear example of <mark><strong>putting user feedback ahead of aesthetic instinct</strong></mark>.
<div class="responses">
{{< response
title="Reorganized forms around status instead of origin"
lede="To remove the biggest source of confusion, I grouped forms by **what state they were in**, not by how they entered the system."
>}}
#### What changed
I removed the distinction between requested forms and self-started forms as the main way the app was organized. Instead, users saw a simpler set of _states_&mdash;**Requests**, **In Progress**, **Pending Approval**, **Completed**&mdash;and a simpler **Form Library** for all of the available forms they could initiate themselves. Students also moved into a dedicated section instead of being mixed into form-related navigation.
#### Why it mattered
Users no longer had to remember how a form started just to find it again, **forms instead fit their mental model**. Just as important, it let us use the dashboard as an inbox as the app finally matched the question they were actually asking: _What needs my attention, what have I started, and what have I already finished?_
#### What this solved specifically
This change addressed the <mark color="yellow">fragmented findability problem</mark> at the center of the legacy experience and gave both parents and staff a more predictable place to return to unfinished or completed work.
<figure>
<img src="img/response-forms@2x.png" alt="Revised navigation showing the new forms organization by status" />
<figcaption>
<p>Forms are now organized by their status as opposed to separating them based on type.</p>
</figcaption>
</figure>
{{< /response >}}
{{< response
title="Urgent information moved to the top on mobile"
lede="Everything had to work well on mobile, so we designed for it first. And the most important mobile intervention was bringing critical alerts and next actions to the top of the experience instead of burying them beneath static content."
>}}
#### What changed:
I designed a mobile widget that surfaced alerts, approvals, and other to-do items immediately, before the user had to scroll through the rest of the page. It doubled as static nagivation to the form statuses and had a desktop variation as well.
We played around with a more traditional tab bar at the bottom or a simpler alert/notification icon in the header, but this new widget consistently tested well.
#### Why it mattered:
This is one of the clearest examples in the project of <strong>designing for the user rather than for my own aesthetic preference</strong>. I hated the way this looked on the screen and how much space it took up! **It felt clunky and non-standard, but it tested very well with customers**, helped users notice important items faster, and ended up creating real goodwill because the product finally felt more helpful.
<figure>
<img src="img/response-widget-desktop@2x.png" alt="Mobile screens detailing the new alert + navigation widget." />
<figcaption>
<p>Card-style buttons mirrored the mobile navigation + alert display for parents on desktop.</p>
</figcaption>
</figure>
#### What this solved specifically:
It addressed the mobile-heavy reality of the product and <mark color="red">eliminated all possibility that users would miss approvals, alerts, or other high-priority tasks</mark> simply because they were visually buried.
<figure>
<img src="img/response-widget-mobile@2x.png" alt="Mobile screens detailing the new alert + navigation widget." />
<figcaption>
<p>New alert + navigation widget for mobile solved 2 issues: alerts are always at the top now, and users can easily jump by form status.</p>
</figcaption>
</figure>
{{< /response >}}
{{< response
title="Removed navigation noise and clarified destinations"
lede="Once urgent tasks were surfaced more clearly, I simplified the rest of the app so each major destination had a more obvious purpose."
>}}
#### What changed:
I removed the unused Recent destination, simplified Form Library so it was only a place to browse and start forms, and made Students and Groups more consistent as sections. The old "Groups and eForms" section may have made sense for a backend engineer, but users no longer needed that unintuitive grouping.
I also reduced ambiguity between task-based navigation and account/settings navigation so users were not bouncing between unrelated menus.
#### Why it mattered:
This was less about drawing attention to a single urgent item and more about reducing background confusion. <mark color="green">Users no longer had to interpret vague labels or remember locations</mark> of certain type of information.
#### What this solved specifically:
This work reduced the IA mismatch between how the product was structured internally and how users actually approached their tasks. It also made common destinations more visible and reduced wasted taps.
{{< /response >}}
{{< response
title="Made student context visible wherever it mattered"
lede="Instead of inventing a universal context-switching solution, I focused on making the relevant student context obvious at the moments where users actually needed it."
>}}
#### What changed:
I standardized form cards so they consistently showed the student avatar, group name, and student name, making each request easier to scan and distinguish. I also improved the student profile experience on mobile so teachers and staff could access important student information more easily without bouncing through the interface or piecing context together from multiple screens.
#### Why it mattered:
Users dealing with multiple children, groups, or school contexts needed clarity more than novelty. Making context explicit in the card pattern and profile views reduced hesitation, made the interface more trustworthy, and solved the problem in a way that respected the product's actual data model.
#### What this solved specifically:
This was a more practical and effective answer to the multi-student problem than the broader universal switcher concept we explored earlier in the project. It improved scanability without adding a heavier interaction model the product did not really support.
<figure>
<img src="img/response-users@2x.png" alt="Various UI components showing the different user avatar treatments" />
<figcaption>
<p>Whenever students or teachers were presented, we included their name, group, and avatar in a consistent way. We also introduced standard sets of avatars with a high-contrast initial fallback.</p>
</figcaption>
</figure>
{{< /response >}}
</div>
{{< /section >}}
{{< section id="impact" title="Impact" >}}
I wasn't able to uncover the kind of clean post-launch measurement I would ideally want here. We didn't run a formal NPS follow-up for this work, and I haven't been able to locate retention reporting detailed enough to isolate the redesign's exact effect on churn.
Still, the signals around this release were strong enough that I feel comfortable describing its impact at a high level.
### The churn risk that started the project did not materialize
The most important outcome is the simplest one: the redesign addressed a product experience that had become serious enough to threaten major customer relationships. <mark>NYC DOE not only stayed, but expanded its substantial subscription</mark> of Schooltastic Forms. For a project that began as a usability response to dissatisfaction, that is the clearest business signal I can point to.
### The product continued to grow commercially after release
Forms ARR increased year over year in both 2024, when this redesign was released, and again in 2025. At the time of preparing this, early 2026 is also continuing in the same direction.
I would love to supplement this case study with more specific revenue figures, but even without them, the broader trend matters: the responder experience did not become a drag on the product's growth, and Schooltastic continued to invest in it.
### Customer response was consistently positive
While I also don't have a nice headline metric to quote, I do have repeated qualitative feedback from the people closest to customers. **Parents and staff consistently responded well to the updates**, and follow-up conversations with the PM and account team reflected a lot of positive buzz around the improvements. The mobile alert and navigation widget, in particular, generated much stronger support than I would have predicted from my own design instincts alone.
That matters to me because it reinforces one of the central lessons of this project: <mark>the right interaction is not always the one I find most visually elegant</mark>. In this case, the version that felt most helpful to users was also the one that built the most goodwill.
### The redesign created momentum for broader platform work
This project didn't end with the initial release! Since then, the responder-facing side completed its Honeycomb transition and the remaining areas of that experience have been brought forward as well. Just as importantly, **Schoolstastic has now moved on to the much larger administrative half** of the original Wallabyte product.
That next phase includes a full rethink of form creation and administration workflows, including a new editor experience. This was only prioritized because the responder-facing redesign succeeded well enough to build confidence, reduce risk, and justify tackling the larger half of the platform.
Taken together, I think the impact of this work is best understood less as a single KPI spike and more as a chain of outcomes: a major customer stayed, the product continued to grow, customers responded positively, and the company gained enough confidence to invest in the much larger redesign that came next.
{{< /section >}}
{{< section id="reflection" title="Reflection" >}}
The biggest lesson I carried forward from this project is that <mark color="yellow">large customers do not all experience "good design" the same way</mark>. Because I had already spent years working at a company with NYC as a major customer, I knew they were especially resistant to drastic interface change once their admins and staff were comfortable with a tool that met their needs.
They were also quick to escalate around downtime, missing data, or broken workflows. That context shaped several of my decisions here: **I advocated against a full Honeycomb rollout in one pass**, pushed to keep the updated color palette closer to Wallabyte's original purple, and was extremely cautious about disrupting account-switching behavior that users had already learned. The right move was not to freeze the product, but to improve it progressively without breaking trust.
More broadly, this project reinforced what I believe about designing admin software, whether in edtech or any other B2B environment. <mark>A modern or trendy UI can never be the primary goal in B2B; stability, continuity, and a logical UX matter more.</mark> But that does not mean these products have to stay clunky! The opportunity is to improve UX and UI in every release in a way that is <strong>thoughtful, cumulative, and respectful</strong> of how people actually work. In practice, that often means choosing the clearest solution over the prettiest one, and accepting that consistency for the user matters more than novelty for the designer.
Finally, this project strengthened my belief that <mark color="red">everything starts with the product brief</mark>, and that the designer should be involved in shaping it early. Many of the delays, reversals, and unnecessary explorations in this work can be traced back to a brief that was not tight enough at the start. When the problem definitions are clear, design and development can move with much more confidence. When it is not, even good teams spend energy solving the wrong thing.
{{< /section >}}

View File

@ -26,7 +26,7 @@
<script src="{{ "js/main.js" | relURL }}" defer></script>
{{ end }}
</head>
<body>
<body id='{{ with .Slug }}{{ . }}{{ else }}default{{ end }}'>
<header>
<div class="title">
<a class="logo" href="{{ "/" | relURL }}">
@ -39,10 +39,14 @@
</a>
{{ end }}
</div>
<nav aria-label="Primary">
<a href="{{ "/studies/" | relURL }}">Case studies</a>
<a href="{{ "/contact/" | relURL }}">Get in touch</a>
</nav>
<menu aria-label="Primary">
<li>
<a href="{{ "/studies/" | relURL }}">Case studies</a>
</li>
<li>
<a href="{{ "/contact/" | relURL }}">Get in touch</a>
</li>
</menu>
</header>
<main>
{{ block "main" . }}{{ end }}
@ -51,4 +55,4 @@
{{ partial "footer.html" . }}
</footer>
</body>
</html>
</html>

View File

@ -1,40 +1,64 @@
{{- define "main" -}}
<header>
{{- with .Params.hero.tags -}}
<!--<ul class="tag-list" aria-label="Case study tags">
{{- range . -}}
<li>
<span class="tag{{ with .tone }} tag-{{ . }}{{ end }}">{{ .label }}</span>
</li>
{{- end -}}
</ul>-->
{{- $heroImage := "" -}}
{{- $heroInlineSvg := "" -}}
{{- with .Params.hero.image -}}
{{- $match := $.Resources.GetMatch . -}}
{{- if not $match -}}
{{- $match = $.Resources.GetMatch (printf "img/%s" .) -}}
{{- end -}}
<h1>{{ with .Params.hero.title }}{{ . }}{{ else }}{{ .Title }}{{ end }}</h1>
{{- with .Params.hero.deck -}}
<p class="lede">{{ . }}</p>
{{- with $match -}}
{{- $heroImage = .RelPermalink -}}
{{- end -}}
{{- if and (not .Params.hero.deck) .Summary -}}
<p class="lede">{{ .Summary }}</p>
{{- end -}}
{{- with .Params.hero.inline_svg -}}
{{- $match := $.Resources.GetMatch . -}}
{{- if not $match -}}
{{- $match = $.Resources.GetMatch (printf "img/%s" .) -}}
{{- end -}}
</header>
{{- with .Params.study -}}
<aside aria-label="Study metadata">
{{- with .facts -}}
{{- range . -}}
<figure>
{{- with .icon -}}
{{ partial "icon.html" (dict "name" .) }}
{{- end -}}
<label>{{ .label }}</label>
<figcaption>{{ .value }}</figcaption>
</figure>
{{- with $match -}}
{{- $heroInlineSvg = .Content -}}
{{- end -}}
{{- end -}}
</aside>
{{- end -}}
<div style="width: 100%; height: 50dvh; border-bottom: 2px solid #171717;"></div>
<section{{ with $heroImage }} style="--hero-image: url('{{ . }}');"{{ end }}>
<header>
{{- with .Params.hero.tags -}}
<!--<ul class="tag-list" aria-label="Case study tags">
{{- range . -}}
<li>
<span class="tag{{ with .tone }} tag-{{ . }}{{ end }}">{{ .label }}</span>
</li>
{{- end -}}
</ul>-->
{{- end -}}
<h1>{{ with .Params.hero.title }}{{ . }}{{ else }}{{ .Title }}{{ end }}</h1>
{{- with .Params.hero.deck -}}
<p class="lede">{{ . | markdownify }}</p>
{{- end -}}
{{- if and (not .Params.hero.deck) .Summary -}}
<p class="lede">{{ .Summary | markdownify }}</p>
{{- end -}}
</header>
{{- with .Params.study -}}
<aside aria-label="Study metadata">
{{- with .facts -}}
{{- range . -}}
<figure>
{{- with .icon -}}
{{ partial "icon.html" (dict "name" .) }}
{{- end -}}
<label>{{ .label }}</label>
<figcaption {{ with .class }}class='{{ . }}'{{ end }}>{{ .value | markdownify }}</figcaption>
</figure>
{{- end -}}
{{- end -}}
</aside>
{{- end -}}
{{- with $heroInlineSvg -}}
{{ . | safeHTML }}
{{- end -}}
</section>
<article>
{{- with .Params.study.toc -}}
@ -46,9 +70,8 @@
</ol>
</nav>
{{- end -}}
<div id="content">
{{ .Content }}
</div>
</article>
{{- end -}}
{{- end -}}

View File

@ -9,6 +9,7 @@
{{- if gt (len $footnotes) 0 -}}
<section id="footnotes">
<big>Ag</big>
<ul aria-label="Footnotes">
{{- range $footnotes -}}
<li class="footnote">
@ -25,5 +26,8 @@
{{- end -}}
<section id="end">
<p>Footer.</p>
<div class="text">
<p>This is the design portfolio for <a href="https://andrewgioia.com">Andrew Gioia</a>, co-founder of TeachBoost and current Staff Designer at SchoolStatus.</p>
<div>{{ partial "icon.html" (dict "name" "cc") }} 2026 BY-NC-ND. Please share this responsibly.</div>
</div>
</section>

View File

@ -1,8 +1,5 @@
{{- $id := .Get "id" | default "overview" -}}
{{- $title := .Get "title" | default "Project overview" -}}
{{- $businessReasonTitle := .Get "business_reason_title" | default "Business reason for the work" -}}
{{- $mediaCaption := .Get "media_caption" -}}
{{- $mediaPlaceholder := .Get "media_placeholder" | default "Original product screenshot placeholder" -}}
<section id="{{ $id }}">
<h2>{{ $title }}</h2>

View File

@ -1,32 +1,9 @@
{{- $color := .Get "color" -}}
{{- $title := .Get "title" -}}
{{- $image := .Get "image" -}}
{{- $alt := .Get "alt" | default $title -}}
{{- $caption := .Get "caption" -}}
{{- $placeholder := .Get "placeholder" | default "Problem screenshot placeholder" -}}
{{- with $title }}
<h3>{{ . }}</h3>
{{- end }}
{{ .Inner | markdownify }}
<!--
<figure class="problem-figure">
{{- if $image -}}
{{- with $.Page.Resources.GetMatch $image -}}
<img src="{{ .RelPermalink }}" alt="{{ $alt }}">
{{- else -}}
<div class="problem-image-placeholder">
<p>{{ $placeholder }}</p>
</div>
{{- end -}}
{{- else -}}
<div class="problem-image-placeholder">
<p>{{ $placeholder }}</p>
</div>
<div class='problem{{ with $color }} problem-color {{ . }}{{ end }}'>
{{- with $title }}
<h3><span>{{ . | markdownify }}</span></h3>
{{- end }}
{{- with $caption }}
<figcaption>{{ . }}</figcaption>
{{- end }}
</figure>
-->
{{ .Inner | markdownify }}
</div>

View File

@ -0,0 +1,18 @@
{{- $title := .Get "title" -}}
{{- $label := .Get "label" -}}
{{- $lede := .Get "lede" -}}
{{- $body := .Inner -}}
<div class="response">
{{- with $label }}
<label>{{ . }}</label>
{{- end -}}
{{- with $title }}
<h3>{{ . }}</h3>
{{- end }}
{{- with $lede }}
<div class="lede">{{ . | markdownify }}</div>
{{- end }}
<div class="response-body">
{{ $body | .Page.RenderString }}
</div>
</div>

View File

@ -0,0 +1,9 @@
{{- $id := .Get "id" -}}
{{- $title := .Get "title" -}}
{{- $body := .Inner -}}
<section{{ with $id }} id="{{ . }}"{{ end }}>
{{- with $title }}
<h2>{{ . }}</h2>
{{- end }}
{{ $body | .Page.RenderString }}
</section>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<path d="M108.836,156.725l-26.108,-31.053l37.823,-0l0,66.284l-11.715,0l0,-35.23Z" style="fill:#1a52a3;"/>
<path d="M139.353,3.827l18.445,32.695l-18.445,4.942l0,-37.637Z" style="fill:#1a52a3;"/>
<path d="M158.554,13.117l-2.135,23.356l-12.411,-7.165l14.545,-16.191Z" style="fill:#e62a1b;"/>
<path d="M139.353,29.039c22.508,0.008 42.61,12.25 50.526,26.887l-50.526,-0l0,-26.887Z" style="fill:#272727;"/><path d="M120.551,125.716l-69.791,0c0,-38.519 31.272,-69.791 69.791,-69.791l0,69.791Z" style="fill:#e62a1b;"/>
<path d="M108.836,191.955l46.77,0l-35.055,-12.193l-11.715,12.193Z" style="fill:#272727;"/>
<rect x="133.058" y="86.516" width="8.557" height="27.359" style="fill:#272727;"/>
<circle cx="139.452" cy="113.875" r="6.393" style="fill:#272727;"/>
<path d="M120.551,55.926l37.605,0c0,20.755 -16.85,37.605 -37.605,37.605l0,-37.605Z" style="fill:#fcb70d;"/>
<path d="M50.76,125.716c0.073,10.842 -5.416,56.45 -48.638,66.284c35.145,-3.122 60.636,-17.345 73.575,-48.975c2.177,-5.322 3.845,-11.589 4.234,-17.309c0,0 -29.171,-0.101 -29.171,0Z" style="fill:#fcb70d;"/>
<circle id="eye" cx="147.505" cy="37.061" r="4.32" style="fill:#fff;"/>
<circle id="pupil" cx="148.576" cy="37.061" r="2.88" style="fill:#272727;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
static/img/kangaroo.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M108.836,156.725l-26.108,-31.053l37.823,-0l0,66.284l-11.715,0l0,-35.23Z" style="fill:#1a52a3;"/><path d="M139.353,3.827l18.445,32.695l-18.445,4.942l0,-37.637Z" style="fill:#1a52a3;"/><path d="M158.554,13.117l-2.135,23.356l-12.411,-7.165l14.545,-16.191Z" style="fill:#e62a1b;"/><path d="M139.353,29.039c22.508,0.008 42.61,12.25 50.526,26.887l-50.526,-0l0,-26.887Z" style="fill:#272727;"/><path d="M120.551,125.716l-69.791,0c0,-38.519 31.272,-69.791 69.791,-69.791l0,69.791Z" style="fill:#e62a1b;"/><path d="M108.836,191.955l46.77,0l-35.055,-12.193l-11.715,12.193Z" style="fill:#272727;"/><rect x="133.058" y="86.516" width="8.557" height="27.359" style="fill:#272727;"/><circle cx="139.452" cy="113.875" r="6.393" style="fill:#272727;"/><path d="M120.551,55.926l37.605,0c0,20.755 -16.85,37.605 -37.605,37.605l0,-37.605Z" style="fill:#fcb70d;"/><path d="M50.76,125.716c0.073,10.842 -5.416,56.45 -48.638,66.284c35.145,-3.122 60.636,-17.345 73.575,-48.975c2.177,-5.322 3.845,-11.589 4.234,-17.309c0,0 -29.171,-0.101 -29.171,0Z" style="fill:#fcb70d;"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
static/img/nyc.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 192 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect x="8.693" y="167.817" width="86.4" height="72" style="fill:#fcb70d;"/><rect x="100.268" y="129.523" width="86.4" height="110.477" style="fill:#e62a1b;"/><rect x="100.268" y="96.682" width="86.4" height="32.84" style="fill:#1a52a3;"/><rect x="100.268" y="53.482" width="43.2" height="43.2" style="fill:#272727;"/><rect x="144.859" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="147.739" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="150.619" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="153.499" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="156.379" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="159.259" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="162.139" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="165.019" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="167.907" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="170.787" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="173.667" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="176.547" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="179.439" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="182.319" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="185.199" y="53.482" width="1.448" height="43.2" style="fill:#272727;"/><rect x="21.173" y="152.457" width="61.44" height="15.36" style="fill:#1a52a3;"/><rect x="49.985" y="138.83" width="3.816" height="13.627" style="fill:#272727;"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,44 +1,148 @@
document.addEventListener("DOMContentLoaded", () => {
const toc = document.querySelector("article > nav#toc");
const content = document.querySelector("article > div#content");
const setupKangarooEye = () => {
const heroGraphic = document.querySelector("main > section > svg");
const eye = heroGraphic?.querySelector("#eye");
const pupil = heroGraphic?.querySelector("#pupil");
if (!toc || !content) {
return;
}
const links = Array.from(toc.querySelectorAll('a[href^="#"]'));
const sections = Array.from(content.querySelectorAll("section[id]"));
if (!links.length || !sections.length) {
return;
}
const linksById = new Map(
links.map((link) => [decodeURIComponent(link.getAttribute("href").slice(1)), link]),
);
const setActive = (id) => {
links.forEach((link) => {
link.classList.toggle("active", linksById.get(id) === link);
});
};
const updateActive = () => {
const threshold = window.innerHeight * 0.25;
let current = sections[0];
sections.forEach((section) => {
if (section.getBoundingClientRect().top <= threshold) {
current = section;
}
});
if (current?.id) {
setActive(current.id);
if (!heroGraphic || !eye || !pupil) {
return;
}
const limits = {
left: 4,
right: 2,
vertical: 4,
};
let frame = null;
let pointer = null;
const render = () => {
frame = null;
if (!pointer) {
pupil.setAttribute("transform", "translate(0 0)");
return;
}
const rect = heroGraphic.getBoundingClientRect();
const eyeRect = eye.getBoundingClientRect();
if (!rect.width || !rect.height || !eyeRect.width || !eyeRect.height) {
return;
}
const eyeCenterX = eyeRect.left + (eyeRect.width / 2);
const eyeCenterY = eyeRect.top + (eyeRect.height / 2);
const normalizedX = (pointer.x - eyeCenterX) / (rect.width / 2);
const normalizedY = (pointer.y - eyeCenterY) / (rect.height / 2);
const clampedX = Math.max(-1, Math.min(1, normalizedX));
const clampedY = Math.max(-1, Math.min(1, normalizedY));
const moveX = clampedX < 0 ? clampedX * limits.left : clampedX * limits.right;
const moveY = clampedY * limits.vertical;
pupil.setAttribute("transform", `translate(${moveX.toFixed(2)} ${moveY.toFixed(2)})`);
};
const requestRender = () => {
if (frame !== null) {
return;
}
frame = window.requestAnimationFrame(render);
};
window.addEventListener("mousemove", (event) => {
pointer = { x: event.clientX, y: event.clientY };
requestRender();
}, { passive: true });
window.addEventListener("mouseleave", () => {
pointer = null;
requestRender();
});
requestRender();
};
updateActive();
window.addEventListener("scroll", updateActive, { passive: true });
window.addEventListener("resize", updateActive);
const setupToc = () => {
const toc = document.querySelector("article > nav#toc");
const content = document.querySelector("article > div#content");
const list = toc?.querySelector("ol");
if (!toc || !content || !list) {
return;
}
const links = Array.from(toc.querySelectorAll('a[href^="#"]'));
const sections = Array.from(content.querySelectorAll("section[id]"));
let activeId = null;
if (!links.length || !sections.length) {
return;
}
const linksById = new Map(
links.map((link) => [decodeURIComponent(link.getAttribute("href").slice(1)), link]),
);
const scrollActiveItemIntoView = (link, behavior = "auto") => {
const item = link?.closest("li");
if (!item) {
return;
}
const hasHorizontalOverflow = list.scrollWidth > list.clientWidth;
if (!hasHorizontalOverflow) {
return;
}
const itemLeft = item.offsetLeft;
const itemCenter = itemLeft + (item.offsetWidth / 2);
const maxScrollLeft = list.scrollWidth - list.clientWidth;
const targetLeft = Math.max(0, Math.min(maxScrollLeft, itemCenter - (list.clientWidth / 2)));
list.scrollTo({
behavior,
left: targetLeft,
});
};
const setActive = (id, behavior = "auto") => {
const activeLink = linksById.get(id);
links.forEach((link) => {
link.closest("li")?.classList.toggle("active", activeLink === link);
});
if (activeLink && activeId !== id) {
activeId = id;
scrollActiveItemIntoView(activeLink, behavior);
}
};
const updateActive = (behavior = "auto") => {
const threshold = window.innerHeight * 0.25;
let current = sections[0];
sections.forEach((section) => {
if (section.getBoundingClientRect().top <= threshold) {
current = section;
}
});
if (current?.id) {
setActive(current.id, behavior);
}
};
updateActive();
window.addEventListener("scroll", () => updateActive("smooth"), { passive: true });
window.addEventListener("resize", updateActive);
};
setupKangarooEye();
setupToc();
});