npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-laminar

v1.2.0

Published

A React Native component for morphing text and numbers with character-level identity.

Downloads

124

Readme

Laminar

React Native morphing text and number animation.

import { Laminar } from "react-native-laminar";

Laminar animates changes to a string or number value. Matching characters hold position across transitions. New characters enter. Removed characters exit. Numbers align from the right so place-value columns stay in stable lanes.


Installation

npm install react-native-laminar

Laminar requires React Native Reanimated. Follow its installation guide before using this package.


Quick Start

Text

import React, { useState } from "react";
import { Button, View } from "react-native";
import { Laminar } from "react-native-laminar";

export default function Example() {
  const [word, setWord] = useState("Laminar");

  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Laminar
        text={word}
        fontSize={40}
        style={{ color: "#000000", fontFamily: "System" }}
      />
      <Button
        title="Morph"
        onPress={() => setWord((w) => (w === "Laminar" ? "Linear" : "Laminar"))}
      />
    </View>
  );
}

Number

import React, { useState } from "react";
import { Button, View } from "react-native";
import { Laminar } from "react-native-laminar";

export default function Counter() {
  const [value, setValue] = useState("$1,234");

  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Laminar
        text={value}
        variant="number"
        fontSize={40}
        animationPreset="snappy"
        style={{ color: "#000000", fontVariant: ["tabular-nums"] }}
      />
      <Button title="Change" onPress={() => setValue("$12,345")} />
    </View>
  );
}

Inside a button with auto-sizing

<Pressable style={{ paddingHorizontal: 24, paddingVertical: 12, borderRadius: 36, backgroundColor: "#007aff" }}>
  <Laminar
    text={label}
    autoSize
    fontSize={18}
    style={{ color: "#ffffff" }}
  />
</Pressable>

Centered text in a fixed-width container

Use align when the parent owns the width and Laminar should place the animated row inside that space.

<Pressable
  style={{
    width: "100%",
    height: 50,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: 25,
    backgroundColor: "#000000",
  }}
>
  <Laminar
    text={isFinished ? "Continue and Finish" : "Continue"}
    align="center"
    autoSize={false}
    containerStyle={{ width: "100%" }}
    style={{ color: "#ffffff", fontSize: 24, fontWeight: "700" }}
  />
</Pressable>

autoSize (default true) animates the outer container width toward the measured final text width. The button grows and shrinks without layout feedback loops.

Standalone word without auto-sizing

<Laminar
  text={word}
  align="center"
  autoSize={false}
  fontSize={40}
  style={{ color: "#000000" }}
/>

Use autoSize={false} when the parent already defines the space and you only want the glyph animation.


Props

type LaminarProps = {
  text: string | number;
  variant?: "text" | "number";
  fontSize?: number;
  color?: string;
  align?: "left" | "center" | "right";
  style?: StyleProp<TextStyle>;
  containerStyle?: StyleProp<ViewStyle>;
  fontStyle?: StyleProp<TextStyle>;
  animationDuration?: number;
  animationPreset?: "default" | "smooth" | "snappy" | "bouncy";
  stagger?: number;
  autoSize?: boolean;
  clipToBounds?: boolean;
};

| Prop | Default | Description | | ------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------- | | text | Required | The value to display. Numbers are converted to strings internally. | | variant | "text" | "text" uses LCS glyph reconciliation. "number" uses right-aligned digit lanes. | | fontSize | Undefined | Convenience prop merged into the text style. | | color | Undefined | Convenience prop merged into the text style. | | align | "left" | Visual alignment for Laminar's viewport and animated glyph row. | | style | Undefined | Text style applied after fontSize and color. | | containerStyle | Undefined | Advanced style override for the outer viewport shell. | | fontStyle | Undefined | Additional text style merged before style. | | animationDuration | Preset default | Duration override in milliseconds. | | animationPreset | "default" for text, "snappy" for number | Named motion recipe. | | stagger | 0.02 | Delay in seconds between numeric lane animations. | | autoSize | true | Animate the outer width to the measured final text width. | | clipToBounds | false | Clip animated overflow to the viewport bounds. |


Animation Presets

| Preset | Character | Default Duration | | --------- | -------------------------- | ---------------- | | default | Smooth cubic-bezier timing | 380ms | | smooth | Spring with no bounce | 400ms | | snappy | Spring with light bounce | 350ms | | bouncy | Spring with more bounce | 500ms |

<Laminar text={word} animationPreset="smooth" />
<Laminar text={count} variant="number" animationPreset="snappy" />

// Override duration
<Laminar text={word} animationPreset="default" animationDuration={520} />

Best Practices

Keep text stable — don't change key.

// Wrong — forces remount, loses glyph identity
<Laminar key={label} text={label} />

// Correct
<Laminar text={label} />

Choose autoSize based on layout role.

  • Buttons, chips, badges → autoSize={true}
  • Standalone centered words → autoSize={false}
  • Fixed-width counters → autoSize={false}

Use align for visual alignment.

<Laminar text={word} align="center" autoSize={false} />
<Laminar text={price} variant="number" align="right" autoSize={false} />

Use containerStyle only when you need lower-level control over the outer viewport.

Match formatting across renders for numbers.

Switching between $1,234 and 1234 mid-session breaks lane alignment. Keep the format consistent.

// Consistent formatting keeps lanes stable
<Laminar text={`$${value.toLocaleString()}`} variant="number" />

Duration guidelines.

| Range | Best for | | ------------- | ------------------------------- | | 180ms – 260ms | Rapid counters, live data | | 300ms – 450ms | Normal labels, button text | | 500ms – 700ms | Expressive or teaching moments |


Troubleshooting

Animations not playing.

  1. Verify Reanimated is installed and configured.
  2. Check that the component is not remounting due to a changing key.
  3. Check that updates are not arriving faster than the animation duration.

Glyphs clipped.

  1. Set clipToBounds={false} if overflow should be visible.
  2. Verify the parent does not have overflow: "hidden" set.

Numbers misaligned or jumpy.

  1. Use variant="number".
  2. Keep formatting consistent between renders.
  3. Add fontVariant: ["tabular-nums"] if your font supports it.

Auto-sizing not working.

  1. Check that autoSize is not explicitly set to false.
  2. Verify the parent allows its child to define the width — a fixed-width parent overrides the animated child width.

License

See the repository root for license information.