独家

Wind Text <mv-wind-text>

字母像草(或旗帜)一样在整页共享的单一风场中摆动的文字:滚动会掀起阵风,风的前沿沿滚动方向扫过文字,光标在一个会逐渐消散的平流速度网格上局部吹风,每个字母依靠自身的弹簧围绕基线弯曲,风吹过的地方还会飘起几粒花粉。

分类动画文字
类型Web Component(<mv-wind-text>)
状态稳定版
Keywordsexclusive, wind, text, typography, scroll, cursor, spring, fluid, particles, pollen, editorial, headline

When to use

  • An editorial headline should sway like grass or a flag in response to scroll and cursor
  • An organic or nature brand wants a gentle ambient breeze with drifting pollen

Avoid when

  • The text contains links or buttons; they are flattened to plain text
  • Long paragraphs at full strength; swaying letters hurt reading, so keep strength low or skip it

安装

node scripts/add.mjs wind-text --out ./src/marvelous

使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["wind-text"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css、core/base.css、core/canvas.js、core/dom.js、core/element.js、core/motion.js、core/observe.js、components/wind-text/wind-text.js、components/wind-text/wind-text.css。

用法

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<section id="wind-demo" class="wt-hero" lang="en">
  <style>
    .wt-hero { position: relative; overflow: hidden; width: min(100%, 64rem); margin-inline: auto; padding: clamp(2rem, 1rem + 5vw, 4.5rem) clamp(1.25rem, 1rem + 4vw, 4rem) 3rem; border-radius: var(--mv-radius-2xl); border: 1px solid var(--mv-border);
      background:
        radial-gradient(120% 70% at 85% 110%, color-mix(in oklch, var(--mv-success) 16%, transparent), transparent 60%),
        radial-gradient(90% 60% at 0% 0%, color-mix(in oklch, var(--mv-warning) 12%, transparent), transparent 65%),
        var(--mv-surface);
      font-family: var(--mv-font-sans); }
    .wt-kicker { margin: 0 0 1.25rem; display: flex; gap: .625rem; align-items: center; font-size: var(--mv-text-xs); letter-spacing: .14em; text-transform: uppercase; color: var(--mv-fg-muted); font-weight: var(--mv-weight-semibold); }
    .wt-kicker::before { content: ""; width: 1.75rem; height: 1px; background: currentColor; opacity: .6; }
    .wt-title { margin: 0; max-width: 15ch; font-size: clamp(2.75rem, 1.5rem + 5.2vw, 5.25rem); line-height: 1.02; letter-spacing: -0.035em; font-weight: 800; color: var(--mv-fg); text-wrap: balance; }
    .wt-title em { font-style: normal; color: color-mix(in oklch, var(--mv-success) 78%, var(--mv-fg)); }
    .wt-lede { margin: 1.5rem 0 0; max-width: 40rem; font-size: var(--mv-text-lg); line-height: 1.6; color: var(--mv-fg-muted); }
    .wt-actions { margin-top: 2rem; display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; }
    .wt-hint { margin-left: auto; font-size: var(--mv-text-xs); color: var(--mv-fg-subtle); }
    .wt-meadow { position: absolute; inset: auto 0 0 0; height: 5rem; pointer-events: none; opacity: .5;
      background: repeating-linear-gradient(90deg, color-mix(in oklch, var(--mv-success) 30%, transparent) 0 1px, transparent 1px 9px);
      mask-image: linear-gradient(to top, #000, transparent); }
  </style>
  <p class="wt-kicker">Highland Seed Co. · Spring 2026 catalog</p>
  <h2 class="wt-title"><mv-wind-text strength="1.25" pollen="26">Sow to every <em>wind.</em></mv-wind-text></h2>
  <p class="wt-lede"><mv-wind-text strength="0.55" pollen="8">Open-pollinated heirloom seeds, grown without chemicals in the Vermont hills. Every packet saves an old variety from oblivion: Brandywine tomato, scarlet runner bean, bearded Red Fife wheat.</mv-wind-text></p>
  <div class="wt-actions">
    <a class="mv-button" href="#wind-demo">Browse the catalog</a>
    <button class="mv-button" data-variant="outline" type="button" data-gust>Make it blow</button>
    <span class="wt-hint">Move your mouse or scroll</span>
  </div>
  <div class="wt-meadow" aria-hidden="true"></div>
</section>
<script type="module">
  const root = document.getElementById("wind-demo");
  const { gust } = await customElements.whenDefined("mv-wind-text");
  let dir = 1;
  root.querySelector("[data-gust]").addEventListener("click", () => { gust(1.3, dir); dir = -dir; });
  // A first gust on arrival so the idea reads instantly.
  setTimeout(() => gust(1.1, 1), 350);
</script>

API

Attributes

Name类型DefaultDescription
variantgrass | flaggrassgrass: each letter bends and sways around its base; flag: a wave runs along the line like a pennant.
strengthnumber1Letter flexibility (0 to 3): 0.5 for a paragraph, 1.2 for a big headline.
pollennumber18Maximum number of pollen/dust grains in flight (0 = none).
breezenumber0Constant ambient breeze (0 to 1). Above 0, the animation runs as long as the text is visible.

Methods

NameDescription
gust(strength = 1, direction = 1)Sends a gust across every instance (direction 1 → right, −1 → left). Also available as a static: MvWindText.gust().

Content structure

NameDescription
(text)Plain text; inline tags (em, strong, span) are kept visually, links and buttons are flattened to text.

CSS classes

NameDescription
mv-wind-text-word / -charGenerated words (non-breaking) and letters; letters pivot near the baseline (transform-origin 50% 84%).
mv-wind-text-canvasPollen canvas, overflowing by 64 px.

CSS variables

NameDefaultDescription
--mv-wind-text-pollenvar(--mv-warning)Pollen grain color.

Accessibility

The text is exposed only once, through a visually hidden copy; the animated letters and the canvas are aria-hidden, purely decorative, with no interaction required. One wind field, one loop and passive listeners for the whole page; the loop stops as soon as the air is calm and the letters are upright, and only computes visible instances. On touch devices, only scrolling blows. Reduced motion: no animation, letters stay still and perfectly readable. Forced colors: the pollen canvas is hidden. Known limits: the text is read once, on first connection (to change it, render a new element, for example with a key in your framework); links and buttons inside are flattened to plain text, and inline formatting is not announced by screen readers (the hidden copy is plain text).

本页面由 AI 翻译。报告翻译问题