Reform Collective is a digital-first, full-service design and development agency. Weāve been partnering with clients of all sizes for 11 years and going strong! We work with ambitious teams building interesting things. If it doesnāt clash with our ethics and you respect our time, weāre in.
Design
Our previous site was no longer working for us. It didnāt reflect the kind of work we were doing, and more importantly, it created friction. The navigation was convoluted, the structure too deep, and the visual style didnāt align with what we were showing clients in proposals or conversations. Weād share a project we were proud of, and when people landed on the site, they either got confused trying to find it or lost interest navigating a dated UX. It was time to move on.
The redesign was a reset. We stripped the site down to the essentials. Clean layout. Wide spacing. Minimal structure. The goal was to create something that felt open, confident, and easy to move through. We wanted the experience to reflect how we approach client work: intentional, clear, and results-focused ā all while telling a strong story.
We also made a conscious decision to pull back on animation. While we still use motion to support interaction, we didnāt want it to take over the experience. Performance and clarity came first.
Sharing Our Work
One of the most deliberate changes we made was how we present our work. Traditional case studies are saturated with summaries, timelines, and process write-ups. We realized thatās not how people consume portfolio content anymore. They donāt read. They scroll. They skim. They decide quickly if youāre worth their time.
So we stopped writing to be read and started designing to be seen.
We removed all the fluff: no intro copy, no strategy breakdowns, no āhereās what we learned.ā Just clean visuals, concise project titles, and frictionless browsing. If the work canāt speak for itself, it probably isnāt strong enough to be featured.
This shift wasnāt just aesthetic. It was a strategic choice. We wanted to reduce noise and let the quality of the output stand on its own. The site isnāt there to sell. Itās there to show. And showing means getting people to the work faster, without distractions.
The end result is a portfolio that feels fast, direct, and unapologetically visual. No click tunnels. No over-explaining. Just a clear runway to the work.
The Navigation
We designed the global menu to feel structural. Instead of floating over the site or fading in as a layer, it pushes the entire layout downward, physically moving the page to make room. Itās a deliberate gesture. Spatial, not just visual.
The motion is clean and architectural: a full-width panel slides down from the top, snapping into place with precision. Thereās no blur, no parallax, no visual fluff. Just sharp contrast, bold typography, and three primary paths: Our Work, About Us, and Reform Nova. These are anchored by lean sub-labels and a strong call to action.
This isnāt a nav trying to show off. Itās built to orient you quickly, frame the experience, and get out of the way. The choice to displace the page content rather than obscure it reinforces how we think about experience design: create clarity by introducing hierarchy, not noise.
It feels tactile. It feels intentional. And it reflects how we build: structural logic, tight motion, and a clear sense of priority.
The Nerdy Tech Details from Our Lead Engineer
Webby Award Section
I started with an AI prototype in v0 for the wavy lines background. v0 is surprisingly good at interpreting vague instructions. I can literally tell it āmake it goopierā and it will spit out code that makes things feel goopier. I ended up with a pretty snazzy prototype. Because it used react-three-fiber, I could basically copy-paste it directly into our code, install dependencies, and be 80% done! Much faster and more interesting than setting up a Three.js scene by hand, in my opinion.
I will say this workflow has its quirks, though. The AI is great at the initial vibe check, but it chokes on specific feedback. Itās pretty hard to describe visual bugs in text, and since the model canāt see the output, itās basically guessing most of the time. I also noticed it tends to āover-edit,ā sometimes refactoring an entire component for a tiny change. I ended up fixing several bugs myself because v0 just couldnāt handle them.
The next part was the mouse follower. I wanted a video that follows the cursor, appearing over the wavy background but under the header text. As it passes behind the text, the textās color inverts so it remains visible.
The āfollowing the mouseā part was easy! The inversion effect was a bit trickier. My first thought was to use mix-blend-mode
paired with backdrop-filter
. It seemed like a great idea and should have worked perfectlyāor at least, thatās what Iād say if it actually had. I ended up trying all kinds of random approaches to find something that worked across every browser. Major upside: I got to justify all my monitors by putting a different browser on each while coding.
The breakthrough came when I stopped trying to make one element do everything. I split the effect into two perfectly synchronized divs:
- The
: A ghost div with no content. Its only job is to carry thebackdrop-filter: invert(1)
that flips the text color. - The
: This holds the actual video. Itās placed in a lower stacking context using
z-index: -1
, so it slides beneath the text but stays above the page background.
I used GSAPās quickTo
to animate them both in sync. To the user (thatās YOU), it appears as a single element. It feels like a bit of a hack, but it works flawlessly across all browsers.
Hereās the gist of it:
// animate both refs at the same time so they appear as one element
const moveX = gsap.quickTo([videoRef.current, inverter.current], "x", { /* ... */ });
const moveY = gsap.quickTo([videoRef.current, inverter.current], "y", { /* ... */ });
// in the JSX
{/* other content here, ofc */}
// and the styles...
const Video = styled(BackgroundVideo, {
position: "fixed",
zIndex: -1, // pushed behind the text
filter: "invert(1) contrast(0.5)",
/* ... */
});
const Inverter = styled("div", {
position: "fixed",
pointerEvents: "none", // for text selection
backdropFilter: "invert(1) contrast(2)",
/* ... */
});
The styles here use https://www.restyle.dev/, by the way ā itās a runtime-only CSS library (i.e., no bundler config required), which is pretty cool.
Nova Blocks Section
This feature is a scroll-driven animation where a grid of 3D blocks zooms past the camera. The fun part is that itās all done with pure CSS transforms
āno WebGL or threejs needed.
The setup involves a container with perspective
and a bunch of āblockā div
s, each using transform-style: preserve-3d
. Each block contains several child div
s rotated into place to form a cube. For performance, I only animate the parent blockās transform, which is more efficient than moving hundreds of individual faces. I used the MDN demo cube for inspiration on this one.
Of course, doing this led me straight into the weird world of browser bugs. (I seem to end up there a lotā¦)
1. Safariās Rendering Glitch:
Safari was z-fighting like crazy. It would randomly render faces that should have been occluded by an adjacent cube, which looked terrible. See web-bugs/issues/155416. The fix ended up being twofold:
- Manual Culling: As an optimization, I was already rendering only the faces that would be visible based on the cubeās grid quadrant. This is basically manual back-face culling, which helped reduce the number of layers Safari had to compute. It probably improves performance anyway, so⦠thanks, Safari, I guess.
- Forced Stacking: Iām assigning each cube a specific
z-index
based on its row and column. It feels a bit brute-force, but it tells Safari exactly how to stack thingsāand it completely eliminated the flicker.
Hereās the gist of the Block.tsx
component:
export default function Block({
vertical,
horizontal,
row,
column,
}: {
// vertical/horizontal basically represents the 'quadrant' on-screen
vertical: "top" | "bottom";
horizontal: "left" | "right";
row: number;
column: number;
}) {
// Explicitly set z-index based on grid position to prevent z-fighting in Safari
// This was basically trial and error to figure out
const style =
vertical === "top" && horizontal === "left"
? { zIndex: -row - column }
: vertical === "bottom" && horizontal === "right"
? { zIndex: -1 }
: horizontal === "left"
? { zIndex: -column }
: { zIndex: -row };
// Conditionally render only the necessary faces
return (
{horizontal === "left" && }
{horizontal === "right" && }
{vertical === "top" && }
{vertical === "bottom" && }
);
}
const Wrapper = styled("div", {
transformStyle: "preserve-3d", // the magic property for the cube
/* ... */
});
2. Firefoxās Pinning Problem
Our site uses CSS Subgrid for global alignment, which is awesome in my opinion because it narrows the gap between design and development. If something in the design is aligned to the grid, it can now be literally aligned to the grid in the code too.
Caveat: I found that in Firefox,
position: sticky
was completely broken inside asubgrid
container. A pinned element would start pinning but never unpin, because its positioning context was being resolved to the wrong grid container.
After I isolated it in a CodePen and reported the bug (web-bugs/issues/152027), the fix was simply to remove subgrid
from the sticky elementās parent and apply the grid styles directly.
Running into weird bugs is frustrating, but itās part of the deal when youāre building cool things. You just have to plan for it in your timeline. And if you find a bug in some strange edge case, Iām a big advocate for taking the time to create a minimal test case and report it. It helps pinpoint exactly whatās going wrong, which leads to a better solutionāand it helps make the web better for everyone.

Thanks for reading!
Ready to build something with us? Weāre always looking for great companies and individuals to partner with on new projects. Get started ā
The Reform Co. Team
P.S. Weāre also hiring, feel free to check out our careers page. ā¤ļø