---
title: "Bottom Up Letters"
description: "Letters that rise out of their own baseline, one after another."
canonical_url: "https://nuxt-motion-block.pages.dev/components/text/bottom-up-letters"
---
# Bottom Up Letters

> Letters that rise out of their own baseline, one after another.

Each letter starts `46px` below its resting position and fully transparent, then
rises into place and fades up. The letters do not move together: every one waits
a fixed stagger behind the one before it, so the string reads as being lifted
into existence from left to right.

The travel is short on purpose. `46px` is roughly the height of a large line of
text, which means each letter starts just below where it belongs rather than
sometime off-screen — the effect is a letter rising to a mark, not a letter
flying in from elsewhere.

The stagger does most of the work. At the default `88ms` the whole word takes
about `1s` to finish, and the last letter is still moving when the first has
already settled. That overlap is what makes it read as a cascade rather than a
queue.

Useful for a headline that should feel physical, a nav brand, or any short
string where the entrance is the point.

## Usage

<code-preview label="Preview">
<div className="flex,justify-center,w-full,h-full">
<component-example name="bottom-up-letters-basic">



</component-example>
</div>

<template v-slot:code="">

```vue
<template>
  <MBottomUpLetters
    class="text-3xl font-bold"
    text="Rise up"
  />
</template>
```

</template>
</code-preview>

## Examples

### Stagger width

<code-preview label="Preview">
<div className="flex,justify-center,w-full,h-full">
<component-example name="bottom-up-letters-stagger">



</component-example>
</div>

<template v-slot:code="">

```vue
<template>
  <div class="flex flex-col items-center gap-4">
    <MBottomUpLetters
      class="text-2xl font-semibold"
      :stagger="0"
      text="Without stagger"
    />
    <MBottomUpLetters
      class="text-2xl font-semibold"
      :stagger="88"
      text="Default stagger"
    />
    <MBottomUpLetters
      class="text-2xl font-semibold"
      :stagger="220"
      text="Slow cascade"
    />
  </div>
</template>
```

</template>
</code-preview>

Set `stagger` to `0` and every letter rises as one object; raise it and the
cascade widens until each letter reads as its own event.

### On scroll

<code-preview label="Preview">
<div className="flex,justify-center,w-full,h-full">
<component-example name="bottom-up-letters-on-scroll">



</component-example>
</div>

<template v-slot:code="">

```vue
<template>
  <div class="text-center">
    <MBottomUpLetters
      class="text-3xl font-bold"
      :delay="200"
      text="Rises in on scroll"
      trigger-on-view
    />
    <p class="mt-3 text-sm text-muted">
      200ms after the headline enters view, then 88ms per letter.
    </p>
  </div>
</template>
```

</template>
</code-preview>

Set `trigger-on-view` to hold the reveal until the element enters the viewport,
which is what you want for a headline that sits below the fold. `delay` then
holds the start a further 200ms so the entrance does not collide with whatever
scrolled it into view. Observation stops after the first intersection, so the
effect never replays on the way back up.

## Customize

<code-preview label="Preview">
<div className="w-full">
<component-example name="bottom-up-letters-custom">



</component-example>
</div>

<template v-slot:code="">
<playground-code name="BottomUpLetters">



</playground-code>
</template>
</code-preview>

## Props

<component-props>
<component-prop name="text" type="string" :required="true">

The string to reveal. Split into one span per character, so a string prop is
required; a slot would hand back VNodes that cannot be split.

</component-prop>

<component-prop name="stagger" type="number" default="88">

Milliseconds added per letter. `0` reveals every letter at once. Above roughly
`200` a long string takes seconds to finish.

</component-prop>

<component-prop name="delay" type="number" default="0">

Milliseconds to wait before the first letter rises. Useful when several
components should not all begin on the same frame.

</component-prop>

<component-prop name="triggerOnView" type="boolean" default="false">

Wait until the element scrolls into view before revealing. Ignored under
reduced motion, which always reveals immediately — see below.

</component-prop>

<component-prop name="class" type="string">

Classes for the root span, which is also the element carrying the
`aria-label`. Type styles set here are inherited by every letter.

</component-prop>
</component-props>

## Accessibility

The root span carries `aria-label="<text>"` and every letter span is
`aria-hidden="true"`. Announcing the individual letters would mean hearing the
word spelled out, one letter per announcement, while the effect plays out over
the same letters.

Under `prefers-reduced-motion` the rise is not skipped — it is run with
`duration: 0`, which lands every letter at its resting value within a frame.
Dropping the animation instead would leave the hidden styles stranded, because
motion-v treats "nothing to animate" as "write nothing", and the lowered,
transparent initial state would never be replaced.

`triggerOnView` is also ignored under reduced motion. A reduced-motion visitor
should not have to scroll a headline into view to be allowed to read it.

The initial state is identical on the server and on the client for the same
reason the transition is suppressed rather than the animation: the server cannot
know the motion preference, so deriving it there would ship hidden styles to
everyone.

## Performance

- Each letter animates `opacity` and `y` only, so the browser can run the reveal
on the compositor and never touches layout.
- The cost scales with the length of `text`: one animated span per character,
each with its own transition. A long headline is a lot of nodes, so keep this
effect on short strings rather than paragraphs.
- The whole string is a single `once` `IntersectionObserver` (used by
`trigger-on-view`); nothing is scheduled after the last letter lands and there
is no scroll listener.

## How the timing works

Letter *i* waits `delay + i × stagger` milliseconds, then animates for `400ms`.
Each letter is independent, so the total time is not the sum of anything — the
string is finished when the *last* letter's `400ms` is over, which on a
seven-letter word is `6 × 88 + 400 = 928ms`.

The ease is `(0.18, 1, 0.32, 1)`: almost no time on the way out, then a long
settle. A letter leaves its starting offset quickly and spends most of its
`400ms` creeping the last few pixels, which is why the letters appear to *arrive*
rather than to glide.

Set `stagger` to `0` and the letters move as one object — a valid look, but a
different component behaviour. The per-letter split is still there; only the
sequencing is gone.

## Notes

- The component reveals once. `triggerOnView` observes with `once`, and there is
no replay prop — remount with a changing `:key` to run it again.
- This is the mirror of [Top Down Letters](https://nuxt-motion-block.pages.dev/components/text/top-down-letters), which
starts each letter the same distance *above* its resting position. Same
duration, same stagger, same ease, opposite direction.
- For a whole string that moves as one object rather than per letter, see
[Micro Scale Fade](https://nuxt-motion-block.pages.dev/components/text/micro-scale-fade).
- Letter spans are `white-space: pre`, which keeps runs of spaces in the string
from collapsing. Leading and trailing whitespace in `text` is therefore visible
as space, which is usually what you want but not always.
- The root span is `display: inline-block`, so the component participates in
surrounding layout as a single inline box. It will not stretch to fill a
block-level slot on its own.


## Sitemap

See the full [sitemap](https://nuxt-motion-block.pages.dev/sitemap.md) for all pages.
