04Personal · Open source2024

over-shadower - a neomorphism box-shadow playground

over-shadower is a small, focused tool for a very specific aesthetic: soft, raised UI that looks machined out of the surface it sits on. Six controls shape a live preview - size, border radius, offset distance, blur, and color - and the shadow string it produces is a real, copy-ready value.

  • React
  • TypeScript
  • useReducer
  • Vite
  • Tailwind
  • Turborepo

01The project

A neomorphic style is really two shadows in one declaration: one cast onto the surface, and one inset highlight coming back off it. over-shadower exposes every knob that builds that string:

  • Width, height, and border radius of the box itself.
  • Offset distance for the raised edge, blur radius, and color.
  • A live preview that re-renders with every slider move.
  • A generated box-shadow value you can lift straight into your own CSS.

02Tech stack

React + TypeScript
A Vite + React app whose entire state is one typed reducer.
useReducer
Six control knobs, six typed actions, one immutable state tree the preview renders from.
Tailwind CSS
Layout and the monorepo-wide look, with a shared config in packages.
Turborepo + pnpm
The app lives in apps/, with shared packages for UI, TypeScript config, and ESLint config.
RangeInput
A reusable slider component typed to a discriminated union of reducer fields.

03How the shadow is built

The preview box computes its shadow directly from reducer state:

  • Every slider dispatches a typed action: SET_RADIUS, SET_WIDTH, SET_HEIGHT, SET_BOX_SHADOW_DISTANCE, SET_BLUR_RADIUS, or SET_COLOR.
  • The reducer returns a new state object, so no two controls can fight each other - the preview always renders a consistent snapshot.
  • The final box-shadow string combines the positive and negative offsets, which is what makes the raised, neomorphic edge read as 3D.

04Challenges while building

  • Composing the dual-shadow string. Neomorphism needs the offset shadow and its inverted highlight in one declaration (+x +y blur color, -x -y blur color) - getting the sign flips right is the whole visual.
  • Six controls, one source of truth. useReducer with typed actions meant the preview could never render a half-updated state, and the handleChange plumbing stayed readable for every field.
  • Typing the slider generically. RangeInput receives a field and an onChange bound to it, while color gets its own input and action - the discriminated union keeps them from mixing.
  • Empty default color. The initial color is an empty string, which browsers ignore in the shadow value; guarding against it meant the preview never breaks on first load.
  • Monorepo structure. The app is small but intentionally lives in a Turborepo workspace with shared UI and config packages, so the foundation scales before the feature set does.

05What\u2019s next

The README lists the honest leftovers: returning the generated shadow value as copy-paste output, adding custom ESLint import-ordering rules to the shared config, and standing up a deployment.

06Links