Skip to main content

Command Palette

Search for a command to run...

Elevate Your UI with Dynamic Text Shadows in React with ShineJS

Updated
4 min read
Elevate Your UI with Dynamic Text Shadows in React with ShineJS
H
Full-stack Developer focused on the intersection of high-performance web and scalable AWS infrastructure. I write Well-Architected to share patterns for building resilient, cost-effective, and developer-friendly cloud-native web applications.

As developers, we're always looking for that extra "pop" to make our interfaces stand out. Whether you're a fan of neumorphism or just want to add a bit of tactile depth to your typography, static shadows often fall flat. They don't react to the environment, and they certainly don't feel "alive."

That’s why I built ShineJS, a modern, lightweight library designed to bring dynamic, light-reactive shadows to your React and Next.js projects.

In this article, I’ll show you how to get started with the core component and provide an interactive playground where you can experiment with neumorphic-text effects in real-time.

What is ShineJS?

ShineJS is an ESM-only TypeScript library based on the initial work from bigspaceship/shine.js that calculates and injects multi-layered shadows based on a virtual light source. By tracking the mouse position (or a fixed point), it creates a sense of physical presence for your text and UI elements.

It's particularly effective for:

  • Neumorphism aesthetics where soft, directional shadows define the UI.

  • Hero sections that need a premium, interactive feel.

  • Visualizing depth in typography-heavy designs.

Quick Start: The <Shine /> Component

The easiest way to add a shine effect is to use the high-level React component. It handles the ref management and updates automatically.

Installation

npm install @hazya/shinejs

Basic Usage

Here is a simple example of a heading that follows your mouse:

import { Shine } from "@hazya/shinejs/react";

export function HeroHeading() {
  return (
    <div className="bg-slate-100 p-20 flex justify-center">
      <Shine
        as="h1"
        className="text-6xl font-black text-slate-200 uppercase tracking-tighter"
        options={{
          light: {
            position: "followMouse",
            intensity: 1.2,
          },
          config: {
            blur: 30,
            opacity: 0.2,
            offset: 0.15,
            shadowRGB: { r: 15, g: 23, b: 42 }, // Slate-900
          },
        }}
      >
        Dynamic Depth
      </Shine>
    </div>
  );
}

Interactive Playground

I believe the best way to understand a tool is to break it. Below is a live playground that showcases the ShineJS options.

You can tweak the intensity, blur, and offset, etc., to see how the shadow layers interact. This playground uses the useShine hook under the hood to give you full control over the rendering logic.

"use client";

import { Shine } from "@hazya/shinejs/react";

export function PlaygroundStarter() {
  return (
    <Shine
      as="h1"
      options={{
        light: {
          position: "followMouse",
          intensity: 1,
        },
        config: {
          numSteps: 5,
          opacity: 0.15,
          opacityPow: 1.2,
          offset: 0.15,
          offsetPow: 1.8,
          blur: 40,
          blurPow: 1,
          shadowRGB: { r: 0, g: 0, b: 0 },
        },
      }}
    >
      Shine Playground
    </Shine>
  );
}

Things to Try:

  • Shadow Color: Change the shadowRGB to match your background for a true neumorphic look.

  • Opacity Power: Crank up the opacityPow to see how it affects the falloff of the shadow layers.

  • Light Position: Switch from followMouse to a fixed point to simulate a static light source in your UI.

Typography and Localization

One of the strengths of ShineJS is its adaptability. Because it works with standard CSS text-shadows and box-shadows, it respects your typography choices, including font-family and font-weight.

In this next example, you can see how the effect maintains its integrity across different font families and languages. Whether it's bold Sans-Serif or elegant Serif, the shadows wrap perfectly around every glyph.

"use client";

import { Shine } from "@hazya/shinejs/react";

export function TypographyExample() {
  return (
    <Shine
      as="h1"
      style={{
        fontFamily: "Georgia, 'Times New Roman', serif",
        fontWeight: 700,
        fontStyle: "italic",
      }}
      options={{
        light: { position: "followMouse", intensity: 1.15 },
        config: {
          blur: 38,
          offset: 0.12,
          opacity: 0.32,
          shadowRGB: { r: 17, g: 24, b: 39 },
        },
      }}
    >
      Typography Shine
    </Shine>
  );
}

Why this matters for Neumorphism

Neumorphic design relies heavily on the relationship between the light source and the "material" of the UI. By adjusting the typography options alongside ShineJS parameters, you can ensure that your neumorphic text remains readable and visually consistent across all device types and screen resolutions.

Built with Accessibility in Mind

Dynamic text effects can sometimes be a nightmare for screen readers if not handled correctly. When ShineJS splits your text into individual characters to apply granular shadows, it automatically manages the ARIA attributes for you.

The original element receives an aria-label containing the full text, while the internal split-text structure is marked with aria-hidden="true". This ensures that screen readers see a single, clean string of text rather than a fragmented series of letters, keeping your neumorphic text accessible to everyone.

Wrapping Up

ShineJS is designed to be unobtrusive yet impactful. It doesn't require heavy canvas rendering or complex WebGL setups. It uses smart CSS injection that follows the physics of light.

If you're building a landing page or a creative portfolio, give @hazya/shinejs a try.

Happy coding!