Growth

No dependencies
Code format
React + Tailwind
import { useId, useState, type CSSProperties } from 'react';

const MIN = 60;
const MAX = 200;
const y = (cm: number) => 196 - (Math.min(MAX, Math.max(MIN, cm)) - MIN) * (180 / (MAX - MIN));
const FAMILY = [
  { name: 'Leo', age: 3, cm: 92 },
  { name: 'Mia', age: 7, cm: 112 },
  { name: 'Ada', age: 11, cm: 151 },
];
const SPRING = 'linear(0, 0.111, 0.359, 0.638, 0.879, 1.047, 1.137, 1.163, 1.145, 1.105, 1.059, 1.02, 0.992, 0.978, 0.973, 0.976, 0.983, 0.99, 0.997, 1.001, 1.004, 1.004, 1.004, 1.003, 1)';

const css = `
  .field-growth { --wood: light-dark(#f3f4f7, #e5e6e9); --edge: color-mix(in oklab, var(--wood) 82%, light-dark(#6d7179, #7d8087)); --lead: light-dark(#42454c, #a7abb2); position: relative; width: 262px; height: 206px; }
  .field-growth__frame { position: absolute; left: 106px; top: 0; width: 34px; height: 206px; background: linear-gradient(90deg, var(--edge), var(--wood) 18%, var(--wood) 70%, var(--edge) 86%, color-mix(in oklab, var(--edge) 80%, light-dark(#fbfcfd, #121417))); box-shadow: 0 0 0 1px color-mix(in oklab, var(--edge) 70%, transparent), 6px 0 12px -6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.45)); }
  .field-growth__tick { position: absolute; right: 0; height: 1px; background: color-mix(in oklab, var(--lead) 70%, transparent); }
  .field-growth__cm { position: absolute; right: 13px; margin-top: -4px; color: var(--lead); font: 600 7px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
  /* Pencil marks: a line across the frame and a name beside it, in hand. */
  .field-growth__mark { position: absolute; left: 28px; width: 118px; height: 1.4px; background: linear-gradient(90deg, transparent 0 78px, var(--lead) 78px); border-radius: 1px; }
  .field-growth__who { position: absolute; left: 0; width: 98px; margin-top: -9px; color: light-dark(#5f626a, #a7abb2); transition: opacity 0.3s; font: 13px/1 Noteworthy, 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive; text-align: right; }
  .field-growth__who small { font-size: 10px; }
  .field-growth__who--near { opacity: 0.2; }
  /* Your line rides the scale while you type, dashed until Enter pencils it in. */
  .field-growth__you { position: absolute; left: 0; top: 0; width: 262px; transform: translateY(var(--y)); transition: transform 0.6s ${SPRING}; }
  .field-growth__line { position: absolute; left: 100px; top: 0; width: 46px; height: 2px; margin-top: -1px; background: repeating-linear-gradient(90deg, light-dark(#373a40, #f1f2f3) 0 4px, transparent 4px 7px); }
  .field-growth[data-set] .field-growth__line { background: light-dark(#373a40, #f1f2f3); clip-path: inset(0 0 0 0); animation: field-growth-draw 0.4s ease-out; }
  @keyframes field-growth-draw { from { clip-path: inset(0 100% 0 0); } }
  .field-growth__name { position: absolute; left: 0; width: 98px; margin-top: -9px; color: light-dark(#373a40, #f1f2f3); font: 13px/1 Noteworthy, 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive; text-align: right; opacity: 0; }
  .field-growth[data-set] .field-growth__name { opacity: 1; animation: field-growth-write 0.5s steps(8) 0.3s both; }
  @keyframes field-growth-write { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0 0 0 0); } }
  .field-growth__box { position: absolute; left: 150px; top: -14px; display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 8px; border-radius: 8px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), 0 6px 12px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); color: light-dark(#5f626a, #a7abb2); font: 500 11px system-ui, sans-serif; }
  .field-growth__box::before { content: ''; position: absolute; left: -5px; top: 50%; border: 5px solid transparent; border-right-color: light-dark(#f3f4f7, #1a1c1f); border-left: 0; margin-top: -5px; }
  .field-growth__box:has(.field-growth__input:focus-visible) { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), 0 0 0 2px light-dark(#5f626a, #a7abb2); }
  .field-growth__box[data-bad='0'] { animation: field-growth-nope-a 0.3s; }
  .field-growth__box[data-bad='1'] { animation: field-growth-nope-b 0.3s; }
  @keyframes field-growth-nope-a { 25% { translate: -3px 0; } 75% { translate: 3px 0; } }
  @keyframes field-growth-nope-b { 25% { translate: -3px 0; } 75% { translate: 3px 0; } }
  .field-growth__input { width: 34px; margin: 0; padding: 0; border: 0; background: transparent; color: light-dark(#373a40, #f1f2f3); font: 600 15px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; outline: none; }
  .field-growth__hint { position: absolute; left: 150px; top: 18px; color: light-dark(#6d7179, #7d8087); font: 500 9.5px system-ui, sans-serif; white-space: nowrap; opacity: 0; transition: opacity 0.2s; }
  .field-growth:not([data-set]) .field-growth__hint { opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    .field-growth * { animation: none !important; transition: none !important; }
  }
`;

export function Preview() {
  const id = useId();
  const [cm, setCm] = useState('131');
  const [set, setSet] = useState(true);
  const [bad, setBad] = useState(0);
  const n = Number(cm) || MIN;
  return (
    <>
      <style href="kit-inputs-growth" precedence="kit">
        {css}
      </style>
      <div className="field-growth" data-set={set || undefined}>
        <span className="field-growth__frame" aria-hidden="true">
          {Array.from({ length: 29 }, (_, k) => MIN + k * 5).map((c) => (
            <span key={c}>
              <i className="field-growth__tick" style={{ top: y(c), width: c % 20 === 0 ? 11 : c % 10 === 0 ? 7 : 4 }} />
              {c % 20 === 0 && (
                <b className="field-growth__cm" style={{ top: y(c) }}>
                  {c}
                </b>
              )}
            </span>
          ))}
        </span>
        {FAMILY.map((f) => (
          <span key={f.name} aria-hidden="true">
            <i className="field-growth__mark" style={{ top: y(f.cm) }} />
            {/* A name right next to yours steps back so the two don't print over each other. */}
            <span className={`field-growth__who${Math.abs(y(f.cm) - y(n)) < 14 ? ' field-growth__who--near' : ''}`} style={{ top: y(f.cm) }}>
              {f.name} <small>· {f.age}</small>
            </span>
          </span>
        ))}
        <span className="field-growth__you" style={{ '--y': `${y(n)}px` } as CSSProperties}>
          <i className="field-growth__line" aria-hidden="true" />
          <span key={`${set}${cm}`} className="field-growth__name" aria-hidden="true">
            You <small>· today</small>
          </span>
          <label className="field-growth__box" data-bad={bad ? bad % 2 : undefined}>
            <input
              id={id}
              className="field-growth__input"
              inputMode="numeric"
              maxLength={3}
              aria-label="Your height in centimetres"
              value={cm}
              onChange={(e) => {
                setCm(e.target.value.replace(/\D/g, '').slice(0, 3));
                setSet(false);
              }}
              onKeyDown={(e) => {
                if (e.key !== 'Enter') return;
                e.preventDefault();
                // Only heights the frame can show are pencilled in.
                if (n < MIN || n > MAX) return setBad((b) => b + 1);
                setSet(true);
              }}
            />
            cm
          </label>
          <span className="field-growth__hint" aria-hidden="true">
            Enter to mark it
          </span>
        </span>
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/input-growth
Source on GitHub

About this component

A height field on a door frame: the marker rides up and down the measuring scale as you type, and Enter pencils your line in beside the family's.

Dependencies
None
Difficulty
Intermediate
Triggers
state change, focus, spring, draw
Techniques
spring, css transition, clip path, state machine
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native input named "Your height in centimetres" with inputmode numeric; focus outlines the tag. The frame, the family marks and the hint are aria-hidden. Under prefers-reduced-motion the marker jumps to its height and the line is drawn at once.

More inputs

See all
Jersey
Key
Notebook

7 of 10 digits

Pager