本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
フォーム

入力欄にフォーカスするとラベルが縮んで上に移り、下線が伸びる

デモ

操作:クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
フォーム
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

ラベルを、入力欄の値が並ぶ行にぴったり重ねて置きます。欄にフォーカスが入ると、ラベルは縮みながら行の上へ移り、空いた場所に値を打てるようになります。同時に、欄の下の細い罫線の上を、太い線が中央から左右へ伸びていきます。

ラベルは 0.35 秒の power3.out で、欄の高さの 0.4 倍だけ持ち上がり、大きさは 0.72 倍(18px なら 13px)になります。下線はラベルと同時に動き出しますが、端まで届くのに 0.45 秒かかるので、ラベルが止まったあとも少しだけ伸び続けます。フォーカスが外れたときは、同じ線を逆にたどって中央へ閉じます。

ラベルを縮めるときは、transform-origin を左端にします。既定の中央のままだと、縮んだぶんラベルが右へ寄り、下に並ぶ値の左端とそろいません。上がりきった位置がひと文字ぶんずれるので、並んだ欄では目立ちます。

フォーカスが外れるたびにラベルを必ず戻すと、入力済みの値の上にラベルが重なって、どちらも読めなくなります。値が空のときだけ中へ戻し、値があれば上に残して色だけ戻します。自動入力で最初から値が入っている欄も、動かさずに上げた状態から始めます。

下線は clip-path の切り取る量を動かして開きます。scaleX で伸ばすと、補間の途中で線の端がにじむことがあります。開く向きを左からに変える、ラベルの縮尺を浅くするなど、つまみは多くありません。会員登録、ログイン、お問い合わせ、決済のフォームの入力欄に向いています。

コード

HTML
<form class="signup" action="/signup/" method="post">
  <!-- ラベルは for で欄に結ぶ。CSS で値の行に重ねるので、読む順は label → input のままでよい -->
  <div class="field">
    <label class="field__label" for="signup-name">お名前</label>
    <input class="field__input" id="signup-name" type="text" name="name" autocomplete="name">
    <span class="field__line" aria-hidden="true"></span>
  </div>
  <div class="field">
    <label class="field__label" for="signup-email">メールアドレス</label>
    <input class="field__input" id="signup-email" type="email" name="email" autocomplete="email">
    <span class="field__line" aria-hidden="true"></span>
  </div>
  <div class="field">
    <label class="field__label" for="signup-password">パスワード</label>
    <input class="field__input" id="signup-password" type="password" name="password" autocomplete="new-password">
    <span class="field__line" aria-hidden="true"></span>
  </div>
  <button class="signup__submit" type="submit">無料ではじめる</button>
</form>
CSS
.signup {
  --ink: #262626;                 /* 文字と、フォーカスしたラベル・下線の色 */
  --mute: #6b6b6b;                /* 休んでいるラベルの色 */
  --rule: rgba(38, 38, 38, 0.12); /* 休んでいる下線の色 */
  --field-height: 64px;
  --row-height: 24px;             /* 値の行の高さ */
  --row-gap: 12px;                /* 値の行と下線のあいだ */
  --font-size: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  color: var(--ink);
}

.field {
  position: relative;
  height: var(--field-height);
}

/* 休んでいる下線。フォーカスの線はこの上に重なる */
.field::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--rule);
}

/* ラベルは値の行にぴったり重ねる。左端を起点に縮めるので、原点を左に寄せる */
.field__label {
  position: absolute;
  left: 0;
  bottom: var(--row-gap);
  height: var(--row-height);
  line-height: var(--row-height);
  color: var(--mute);
  font-size: var(--font-size);
  font-weight: 600;
  letter-spacing: 0.04em;
  transform-origin: 0 50%;
  pointer-events: none;
}

.field__input {
  position: absolute;
  left: 0;
  bottom: var(--row-gap);
  width: 100%;
  height: var(--row-height);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--font-size);
  font-weight: 500;
  -webkit-appearance: none;
  appearance: none;
}

/* 下線とラベルの色がフォーカスを示すので、既定の輪は消す */
.field__input:focus {
  outline: none;
}

/* フォーカスで伸びる線。中央だけに縮めておき、左右へ開く */
.field__line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background-color: var(--ink);
  clip-path: inset(0% 50% 0% 50%);
}

.signup__submit {
  margin-top: 32px;
  height: 48px;
  border: 0;
  border-radius: 999px;
  background-color: var(--ink);
  color: #ffffff;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
}

.signup__submit:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}
JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const FLOAT_SCALE = 0.72; // 上に移ったラベルの大きさ(18px なら 13px になる)

document.querySelectorAll(".field").forEach((field) => {
  const input = field.querySelector(".field__input");
  const label = field.querySelector(".field__label");
  const line = field.querySelector(".field__line");
  const styles = getComputedStyle(field);
  // 色は CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const mute = styles.getPropertyValue("--mute").trim();
  const ink = styles.getPropertyValue("--ink").trim();
  // 持ち上げる量も、組み上がった欄の高さから出す
  const lift = Math.round(field.offsetHeight * 0.4);

  const float = gsap
    .timeline({ paused: true })
    .fromTo(label, { y: 0, scale: 1 }, { y: -lift, scale: FLOAT_SCALE, duration: 0.35, ease: "power3.out" });

  const focus = gsap
    .timeline({ paused: true })
    // 値の数をそろえないと、途中の形が作れずに一瞬で切り替わる
    .fromTo(line, { clipPath: "inset(0% 50% 0% 50%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.45, ease: "power3.out" }, 0)
    .fromTo(label, { color: mute }, { color: ink, duration: 0.2, ease: "none" }, 0);

  // 動きを減らす設定では、ほぼ一瞬で切り替える
  if (reduceMotion) {
    float.timeScale(20);
    focus.timeScale(20);
  }

  // 自動入力などで最初から値があるときは、動かさずに上げておく
  if (input.value) float.progress(1);

  input.addEventListener("focus", () => {
    float.play();
    focus.play();
  });

  input.addEventListener("blur", () => {
    focus.reverse();
    if (input.value) return; // 値があればラベルは上に残す
    float.reverse();
  });
});

同じ用途のインタラクション

フォームをすべて見る

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。