orn-ui
Atom

ShinyText

Requires <UIProvider> — Getting Started →

Installation

Install just this component (copies the source into your project, no npm dependency)

npx orn-ui add shiny-text

Or install the whole package and import it

pnpm add orn-ui
import { ShinyText } from 'orn-ui/shiny-text';
Depends on:core (theme + icons)transition

Usage

  • when to use — A band of light sweeps the text to say "look here" without a badge or an icon: an upgrade card, a beta label, the one headline on a landing screen. One per screen — two competing sweeps say nothing.
  • how it works — The text is split into characters and each one's opacity is driven from a single shared value, so the whole effect runs on the native thread. It needs the characters: text takes a string, not nodes.
  • color / shineColor — The resting colour and the band that travels across it. Both default to theme roles, so the effect follows a custom brand without being told.
  • spread — How wide the bright band is, as a fraction of the text. Small is a glint on one or two characters; large lights most of the line at once and reads as a pulse.
  • animated — False renders plain text in shineColor — the escape hatch for a user who asked for less motion, and the state to use in a snapshot test.
Demo clip not recorded yet — see MEDIA.md

Variants

align="center" — wraps by word on several lines

<ShinyText text="A headline long enough to wrap onto more than one line" align="center" {...strong} />

duration and delay — slow sweep, long pause

<ShinyText text="Taking its time" duration={4000} delay={1200} {...strong} />

animated={false} — plain text in shineColor, same box

<VariantRow>
  <ShinyText text="No sweep at all" animated={false} {...strong} />
</VariantRow>
Full demo source
function ColorPairs() {
  const colors = useColors();

  return (
    <View style={{ gap: 16 }}>
      <View style={{ gap: 4 }}>
        <Caption>default — textLight resting, primary shining</Caption>
        <ShinyText text="Default pair" />
      </View>
      <View style={{ gap: 4 }}>
        <Caption>border → text — the widest pair the theme has</Caption>
        <ShinyText text="Maximum contrast" color={colors.border} shineColor={colors.text} />
      </View>
    </View>
  );
}

function Spreads() {
  const colors = useColors();

  return (
    <View style={{ gap: 12 }}>
      <ShinyText text="Narrow glint" spread={0.08} color={colors.border} shineColor={colors.text} />
      <ShinyText text="Soft wide wave" spread={0.6} color={colors.border} shineColor={colors.text} />
    </View>
  );
}

function UpgradeCard() {
  const colors = useColors();

  return (
    <Card>
      <ShinyText text="Pro plan" color={colors.primarySoft} shineColor={colors.primary} />
      <Body style={{ marginTop: 4 }}>Unlimited invoices · priority support</Body>
    </Card>
  );
}

export function ShinyTextDemo() {
  const colors = useColors();
  // El par por defecto sólo cambia luminancia; para mirar *cómo* se mueve la
  // banda conviene el par más separado del tema, que cambia tono también.
  const strong = { color: colors.border, shineColor: colors.text };

  const variants: VariantDef[] = [
    {
      label: 'colors — the resting/shine pair is what makes it readable',
      content: <ColorPairs />,
    },
    {
      label: 'align="center" — wraps by word on several lines',
      content: (
        <ShinyText text="A headline long enough to wrap onto more than one line" align="center" {...strong} />
      ),
    },
    {
      label: 'spread — narrow glint vs. soft wave',
      content: <Spreads />,
    },
    {
      label: 'duration and delay — slow sweep, long pause',
      content: <ShinyText text="Taking its time" duration={4000} delay={1200} {...strong} />,
    },
    {
      label: 'animated={false} — plain text in shineColor, same box',
      content: (
        <VariantRow>
          <ShinyText text="No sweep at all" animated={false} {...strong} />
        </VariantRow>
      ),
    },
    {
      label: 'in a card, with the brand accent',
      content: <UpgradeCard />,
    },
  ];
  return <VariantList variants={variants} />;

Props

Also accepts testID, forwarded to the root node. It exists for end-to-end tests (Maestro drives the app by the accessibility tree) and has no effect on how the component looks or behaves.

NameTypeDefaultDescription
textstringThe text to animate. Only a string: the effect needs the characters.
duration?number2400Milliseconds a single sweep takes end to end.
delay?number600Pause between sweeps, in ms.
spread?number0.28Width of the bright band as a fraction of the text, from 0 to 1. Small values give a narrow glint, large ones a soft wave.
animated?booleantrueWhen false it renders plain text in `shineColor`.
color?stringtheme.colors.textLightResting color of the characters.
shineColor?stringtheme.colors.primaryColor of the band that travels across.
align?'left' | 'center' | 'right'leftHorizontal alignment of the line.
style?StyleProp<TextStyle>Typography only: `color` and `shineColor` win over any color set here.