Growth
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>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldGrowth(el) on the mounted element instead.
<div class="field-growth" data-set="true">
<span class="field-growth__frame" aria-hidden="true">
<span>
<i class="field-growth__tick" style="top:196px;width:11px"></i>
<b class="field-growth__cm" style="top:196px">60</b>
</span>
<span>
<i class="field-growth__tick" style="top:189.57142857142858px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:183.14285714285714px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:176.71428571428572px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:170.28571428571428px;width:11px"></i>
<b class="field-growth__cm" style="top:170.28571428571428px">80</b>
</span>
<span>
<i class="field-growth__tick" style="top:163.85714285714286px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:157.42857142857142px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:151px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:144.57142857142856px;width:11px"></i>
<b class="field-growth__cm" style="top:144.57142857142856px">100</b>
</span>
<span>
<i class="field-growth__tick" style="top:138.14285714285714px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:131.71428571428572px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:125.28571428571428px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:118.85714285714285px;width:11px"></i>
<b class="field-growth__cm" style="top:118.85714285714285px">120</b>
</span>
<span>
<i class="field-growth__tick" style="top:112.42857142857142px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:106px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:99.57142857142857px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:93.14285714285714px;width:11px"></i>
<b class="field-growth__cm" style="top:93.14285714285714px">140</b>
</span>
<span>
<i class="field-growth__tick" style="top:86.71428571428571px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:80.28571428571428px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:73.85714285714285px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:67.42857142857142px;width:11px"></i>
<b class="field-growth__cm" style="top:67.42857142857142px">160</b>
</span>
<span>
<i class="field-growth__tick" style="top:61px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:54.571428571428555px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:48.14285714285714px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:41.714285714285694px;width:11px"></i>
<b class="field-growth__cm" style="top:41.714285714285694px">180</b>
</span>
<span>
<i class="field-growth__tick" style="top:35.28571428571428px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:28.857142857142833px;width:7px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:22.428571428571416px;width:4px"></i>
</span>
<span>
<i class="field-growth__tick" style="top:16px;width:11px"></i>
<b class="field-growth__cm" style="top:16px">200</b>
</span>
</span>
<span aria-hidden="true">
<i class="field-growth__mark" style="top:154.85714285714286px"></i>
<span class="field-growth__who" style="top:154.85714285714286px">
Leo
<small>· 3</small>
</span>
</span>
<span aria-hidden="true">
<i class="field-growth__mark" style="top:129.14285714285714px"></i>
<span class="field-growth__who" style="top:129.14285714285714px">
Mia
<small>· 7</small>
</span>
</span>
<span aria-hidden="true">
<i class="field-growth__mark" style="top:78.99999999999999px"></i>
<span class="field-growth__who" style="top:78.99999999999999px">
Ada
<small>· 11</small>
</span>
</span>
<span class="field-growth__you" style="--y:104.71428571428571px">
<i class="field-growth__line" aria-hidden="true"></i>
<span class="field-growth__name" aria-hidden="true">
You
<small>· today</small>
</span>
<label class="field-growth__box">
<input id="field-growth" class="field-growth__input" inputMode="numeric" maxLength="3" aria-label="Your height in centimetres" value="131"/>
cm
</label>
<span class="field-growth__hint" aria-hidden="true">Enter to mark it</span>
</span>
</div>.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 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); }
.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; }
}function initFieldGrowth(root) {
var MIN = 60;
var MAX = 200;
var FAMILY = [92, 112, 151];
var you = root.querySelector('.field-growth__you');
var name = root.querySelector('.field-growth__name');
var who = root.querySelectorAll('.field-growth__who');
var box = root.querySelector('.field-growth__box');
var input = root.querySelector('.field-growth__input');
var bad = 0;
function y(cm) {
return 196 - (Math.min(MAX, Math.max(MIN, cm)) - MIN) * (180 / (MAX - MIN));
}
function render() {
var n = Number(input.value) || MIN;
you.style.setProperty('--y', y(n) + 'px');
// A name right next to yours steps back so the two don't print over each other.
who.forEach(function (w, i) {
w.classList.toggle('field-growth__who--near', Math.abs(y(FAMILY[i]) - y(n)) < 14);
});
// A fresh copy of "You" so its handwriting plays again when the line is pencilled in.
var fresh = name.cloneNode(true);
name.replaceWith(fresh);
name = fresh;
}
function onInput() {
input.value = input.value.replace(/\D/g, '').slice(0, 3);
root.removeAttribute('data-set');
render();
}
function onKey(e) {
if (e.key !== 'Enter') return;
e.preventDefault();
var n = Number(input.value) || MIN;
// Only heights the frame can show are pencilled in.
if (n < MIN || n > MAX) return box.setAttribute('data-bad', String(bad++ % 2));
root.setAttribute('data-set', 'true');
render();
}
input.addEventListener('input', onInput);
input.addEventListener('keydown', onKey);
return function () {
input.removeEventListener('input', onInput);
input.removeEventListener('keydown', onKey);
};
}
document.querySelectorAll('.field-growth').forEach(initFieldGrowth);Goal: Create a "Growth" input.
Visual behavior: 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.
Technique:
- A real numeric input in a small tag holds the height in centimetres; the tag and your dashed line ride to that height on the frame.
- The frame is a wood gradient with ticks every 5 cm and a number every 20. A height becomes a y position, and the line moves there with a linear() spring.
- Enter pencils the line in: the dash becomes solid by a clip-path wipe, and "You, today" writes in with a stepped clip-path. A family name too close to yours fades back so the two never print over each other.
- A height the frame cannot show is refused and the tag shakes.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Growth" input.
Visual behavior: 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.
Technique:
- A real numeric input in a small tag holds the height in centimetres; the tag and your dashed line ride to that height on the frame.
- The frame is a wood gradient with ticks every 5 cm and a number every 20. A height becomes a y position, and the line moves there with a linear() spring.
- Enter pencils the line in: the dash becomes solid by a clip-path wipe, and "You, today" writes in with a stepped clip-path. A family name too close to yours fades back so the two never print over each other.
- A height the frame cannot show is refused and the tag shakes.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
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.