WeightField

A React variable font component where hovering one letter ramps its weight. For display headlines.

WeightField playground

Nostalgia
Point at one letter: it takes the peak weight, a doubled stroke and a sliver of padding either side. Its immediate neighbours take 600 and the same padding, the two beyond them 400, and everything else holds at 300.

Install WeightField

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { WeightField } from '@zyncat/ui/weight-field';

Usage

tsx
import { WeightField } from '@zyncat/ui/weight-field';
 
<WeightField text="Nostalgia" />

Compose WeightField in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

WeightField props

WeightField

PropTypeDefaultDescription
...htmlAttributesHTMLAttributes—Standard <span> attributes (aria-*, data-*, title, ...) forwarded to the root.
text*string—The headline the field is built from. Every character becomes its own hover unit, spaces included.
speednumber1Multiplies the ramp rate - 2 halves the settle, 0.5 doubles it.
classNamestring—Extra class(es) merged onto the root.
styleWeightFieldStyle—Inline styles merged onto the root.

WeightFieldStyle

PropTypeDefaultDescription
--weight-field-inkstring | number—`--weight-field-ink` - Letter ink. Default: `var(--text-strong)`.
--weight-field-sizestring | number—`--weight-field-size` - Letter size. Default: `6rem`.
--weight-field-leadingstring | number—`--weight-field-leading` - Line height, unitless. Default: `1`.
--weight-field-alignstring | number—`--weight-field-align` - Where the units sit on the line - center, start or end. Default: `center`.
--weight-field-padstring | number—`--weight-field-pad` - Padding around the field. Default: `var(--space-6) var(--space-4)`.
--weight-field-trackingstring | number—`--weight-field-tracking` - Letter spacing. Default: `-0.05em`.
--weight-field-rest-weightstring | number—`--weight-field-rest-weight` - A unit's weight at rest. Default: `300`.
--weight-field-far-weightstring | number—`--weight-field-far-weight` - The weight two units from the pointer. Default: `400`.
--weight-field-near-weightstring | number—`--weight-field-near-weight` - The weight next to the pointer. Default: `600`.
--weight-field-peak-weightstring | number—`--weight-field-peak-weight` - The weight under the pointer. Default: `900`.
--weight-field-hover-paddingstring | number—`--weight-field-hover-padding` - How much a unit widens under the pointer. Default: `calc(1em / 12)`.
--weight-field-strokestring | number—`--weight-field-stroke` - The text stroke every unit carries, doubled on the peak letter. Default: `calc(1em * 125 / 6000)`.
--weight-field-durationstring | number—`--weight-field-duration` - How long a unit takes to settle at a new weight. Default: `400ms`.
--weight-field-easestring | number—`--weight-field-ease` - The settle curve. Default: `ease`.

Frequently asked questions

Import it per subpath and pass the headline: import { WeightField } from '@zyncat/ui/weight-field', then <WeightField text="Nostalgia" />. It holds no state and runs no effect, so it renders on the server in the Next.js App Router with no 'use client' boundary; link @zyncat/ui/styles.css once at the root for the tokens.

A variable font ships one file whose weight is a continuous wght axis rather than a handful of separate files, so a browser can render 437 as readily as 400. You do need one here: the ramp animates font-weight through 300, 400, 600 and 900, and on a static family the browser jumps between the installed cuts instead of gliding. The headline inherits --font-body, so setting that to a variable face - a next/font/google or next/font/local one included - is all the setup there is.

This one is CSS alone. Every character is its own span, and :hover plus the + sibling and :has() selectors pick out the hovered letter, the two beside it and the two beyond, while a transition on font-weight runs the ramp. There is no pointer listener, no requestAnimationFrame and no JavaScript per frame, so it costs nothing while the pointer sits still.

speed multiplies the ramp rate against a 400ms default - 2 halves the settle, 0.5 doubles it. The four steps are --weight-field-rest-weight (300), --weight-field-far-weight (400), --weight-field-near-weight (600) and --weight-field-peak-weight (900); flatten the near and far values onto rest and only the hovered letter answers. --weight-field-stroke sets the -webkit-text-stroke every unit carries, doubled on the peak letter, and --weight-field-duration and --weight-field-ease retime the transition itself.

No. It is a React component animating a live variable font in the DOM, so the headline stays real text rather than pixels in a video, a GIF or an After Effects, Premiere Pro or Canva preset, and you can retype it without re-rendering anything. The playground on this page is the generator: type your headline, set the speed, then copy the JSX from the Code tab.

Yes. The complete headline is rendered once into a visually hidden label for screen readers while every glyph span is aria-hidden, so assistive tech reads the word instead of spelling it out. Under prefers-reduced-motion the 400ms ramp collapses to 1ms, and because the effect is a decorative pointer hover the headline is fully legible at its rest weight whether or not anyone hovers it.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub