Flow

Screens / Login / Sign up

Password garden

Sign-up where each password rule is a seed in a raised garden bed: meet a rule and its flower grows and blooms, while the sun rises from dawn to bright day as the password gets stronger.

A working screen: type, make mistakes, sign in. Open it full screen.

The idea

A strong password is a garden you grow: rules are seeds, strength is sunrise.

Palette

#2e7a4b
#ffc23d
#ff6b57
#7468f0
#8d5a35
#7cc4e8

Type and motion

Fraunces bold for headings and the brand (fallback Palatino/Georgia), system sans for labels, body and buttons, mono for the wooden plant markers.

Stems draw upward with stroke-dashoffset, leaves and heads spring open in sequence, the sky blends from dawn to day as the sun rises, and on success the flowers sway and a butterfly flutters in to land.

Make it yours

  • A city skyline where each rule lights a building's windows, ending in fireworks, for a fintech or urban app.
  • An aquarium where each rule adds a fish and plants, with clearer water as strength grows, for a calm or kids' brand.
  • A rocket on a launchpad whose fuel tanks fill per rule and that lifts off on success, for a developer or startup tool.

Why it's engaging

Each rule you meet grows its own flower, so making a strong password feels like tending a garden, and finishing it fills the bed with blooms and brings in a butterfly.

Why it also works better

Every requirement is visible and checked live, the strength level is spelled out in text, and the never-disabled button names exactly which rules are still missing while their seeds wiggle.

Tested code

One self-contained HTML file, no libraries. Your AI gets it from Flow's get_pattern tool.

Show the code
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Password garden sign-up | Flow pattern</title> <!-- Flow login pattern: Password garden (https://flow.donta.io/screens/login/password-garden) Sign-up where every password rule is a seed in a raised garden bed. Meet a rule and its plant sprouts, grows and blooms into its own flower; the sun rises from dawn to bright day as the password gets stronger. The button is never silently disabled: submitting with something missing says exactly what, and the seeds still waiting give a little wiggle. Success brings every flower to full bloom and a butterfly drifts in. The garden is decorative (aria-hidden); every rule's state is in the form as text. Under reduced motion the garden simply shows each state without animating. Demo: try "tidepool7harbor" for a strong password; "ada123" shows what's missing. --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&display=swap" rel="stylesheet"> <style> :root { --ink: #1b2a21; --ink-soft: #4d5e53; --line: #d9e3da; --card: #ffffff; --green: #2e7a4b; --green-deep: #215c38; --sun: #ffc23d; --coral: #ff6b57; --violet: #7468f0; --err: #b3261e; --serif: "Fraunces", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif; --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; } * { box-sizing: border-box; } html, body { margin: 0; min-height: 100%; } body { min-height: 100vh; font-family: var(--sans); color: var(--ink); background: #e9f3ea; overflow-x: hidden; } .scene { position: fixed; inset: 0; } .scene svg { width: 100%; height: 100%; display: block; } .layout { position: relative; min-height: 100vh; display: grid; grid-template-columns: 1fr minmax(360px, 440px); align-items: center; gap: 40px; padding: 40px clamp(20px, 6vw, 96px); } .brand { position: absolute; top: 28px; left: clamp(20px, 6vw, 96px); display: flex; align-items: center; gap: 10px; font: 700 21px/1 var(--serif); color: #fff; text-shadow: 0 1px 8px rgba(20,30,60,.35); transition: color .8s; } .brand svg { width: 26px; height: 26px; } .day .brand { color: var(--ink); text-shadow: none; } /* Card */ .card { grid-column: 2; position: relative; background: rgba(255,255,255,.96); border-radius: 24px; padding: 32px 32px 26px; box-shadow: 0 30px 70px -28px rgba(25,45,35,.55), 0 2px 0 rgba(255,255,255,.6) inset, 0 0 0 1px rgba(30,60,40,.06); } h1 { margin: 0 0 8px; font: 700 clamp(30px, 3.4vw, 38px)/1.02 var(--serif); letter-spacing: -.02em; } .sub { margin: 0 0 22px; color: var(--ink-soft); font-size: 15.5px; line-height: 1.5; } form { display: grid; gap: 16px; } label { display: grid; gap: 7px; font-size: 14px; font-weight: 650; } .field { position: relative; } input { width: 100%; min-height: 50px; padding: 0 16px; border-radius: 14px; border: 1.5px solid var(--line); background: #fbfdfb; color: var(--ink); font: 16px var(--sans); transition: border-color .15s, box-shadow .15s; } input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 4px rgba(46,122,75,.18); background: #fff; } input[aria-invalid="true"] { border-color: var(--err); } .pw input { padding-right: 84px; } .show { position: absolute; right: 5px; top: 5px; bottom: 5px; min-width: 70px; border: 0; border-radius: 10px; background: #eef5ef; color: var(--green-deep); font: 600 14px var(--sans); cursor: pointer; } .show:hover { background: #e2eee4; } .show:focus-visible, .btn:focus-visible, a:focus-visible { outline: 3px solid var(--green); outline-offset: 2px; } .strength { display: flex; align-items: center; gap: 10px; margin: -4px 0 0; font-size: 13px; color: var(--ink-soft); min-height: 18px; } .meter { flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; } .meter i { height: 6px; border-radius: 99px; background: #e5ece6; transition: background-color .35s; } .lv1 .meter i:nth-child(1) { background: #f08a5d; } .lv2 .meter i:nth-child(-n+2) { background: #f2b63d; } .lv3 .meter i { background: var(--green); } .strength b { font-weight: 650; color: var(--ink); min-width: 92px; text-align: right; } .rules { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; font-size: 14.5px; } .rules li { display: flex; align-items: center; gap: 11px; color: var(--ink-soft); transition: color .2s; } .rules li .bud { flex: none; width: 22px; height: 22px; border-radius: 50%; border: 2px dashed #c7d3c9; display: grid; place-items: center; transition: background-color .25s, border-color .25s, transform .4s cubic-bezier(.3,1.8,.5,1); } .rules li .bud svg { width: 12px; height: 12px; opacity: 0; transition: opacity .2s; } .rules li.met { color: var(--ink); } .rules li.met .bud { border-style: solid; transform: scale(1.1); } .rules li.met .bud svg { opacity: 1; } .rules li[data-rule="len"].met .bud { background: var(--sun); border-color: #e3a51f; } .rules li[data-rule="mix"].met .bud { background: var(--coral); border-color: #e2503d; } .rules li[data-rule="name"].met .bud { background: var(--violet); border-color: #5b50d8; } .rules li.missing { color: var(--err); } .rules li.missing .bud { border-color: var(--err); } .btn { min-height: 52px; border: 0; border-radius: 14px; cursor: pointer; margin-top: 4px; background: var(--green); color: #fff; font: 650 16px/1 var(--sans); box-shadow: 0 2px 0 var(--green-deep), 0 12px 24px -14px rgba(33,92,56,.8); transition: background-color .2s, transform .1s; } .btn:hover { background: #2a6f44; } .btn:active { transform: translateY(1px); } .btn[aria-busy="true"] { opacity: .8; cursor: progress; } .msg { margin: 0; min-height: 20px; font-size: 14px; line-height: 1.45; } .msg.err { color: var(--err); } .foot { margin: 2px 0 0; font-size: 14px; color: var(--ink-soft); } .foot a { color: var(--green-deep); font-weight: 650; } .hint { margin: 0; font-size: 12px; color: #7d8b81; } .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .done { display: none; text-align: left; } .card.is-done form, .card.is-done .intro { display: none; } .card.is-done .done { display: grid; gap: 14px; animation: rise .6s cubic-bezier(.2,.9,.3,1.2) both; } .done h2 { margin: 0; font: 700 36px/1.02 var(--serif); letter-spacing: -.02em; } .done h2:focus { outline: none; } .done p { margin: 0; color: var(--ink-soft); line-height: 1.5; } .done b { color: var(--ink); overflow-wrap: anywhere; } .done .btn { display: grid; place-items: center; text-decoration: none; } .badge { width: 54px; height: 54px; border-radius: 16px; background: #eaf6ee; display: grid; place-items: center; } @keyframes rise { from { opacity: 0; translate: 0 14px; } } /* Garden art */ .stem { fill: none; stroke: #3f8f4f; stroke-width: 5; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset .7s cubic-bezier(.4,0,.2,1); } .leaf { transform-box: fill-box; transform: scale(0); transition: transform .45s cubic-bezier(.3,1.6,.5,1) .4s; } .leaf.l { transform-origin: right center; } .leaf.r { transform-origin: left center; } .head { transform-box: fill-box; transform-origin: center; transform: scale(0) rotate(-40deg); transition: transform .6s cubic-bezier(.3,1.6,.5,1) .55s; } .seed { transition: opacity .3s .1s, transform .3s; transform-box: fill-box; transform-origin: center bottom; } .stem { opacity: 0; } .plant.sprout .stem, .plant.grown .stem { opacity: 1; } .plant.sprout .stem { stroke-dashoffset: .86; } .plant.grown .stem { stroke-dashoffset: 0; } .plant.grown .leaf { transform: scale(1); } .plant.grown .head { transform: scale(1) rotate(0); } .plant.grown .seed { opacity: 0; } .plant.nudge .seed { animation: nudge .5s ease 2; } @keyframes nudge { 25% { transform: rotate(-12deg) translateY(-2px); } 75% { transform: rotate(12deg) translateY(-2px); } } .bloom .plant .head { transform: scale(1.14) rotate(0); transition-delay: 0s; } .bloom .plant .sway { animation: sway 3.2s ease-in-out infinite alternate; transform-box: fill-box; transform-origin: center bottom; } .bloom .plant:nth-child(2) .sway { animation-delay: -1.1s; } .bloom .plant:nth-child(3) .sway { animation-delay: -2.2s; } @keyframes sway { from { rotate: -2.2deg; } to { rotate: 2.2deg; } } .sun { transition: transform 1.1s cubic-bezier(.3,1.2,.4,1); } .sun-rays { transform-box: fill-box; transform-origin: center; animation: spin 40s linear infinite; } @keyframes spin { to { rotate: 360deg; } } .cloud { animation: drift 60s linear infinite; } .cloud.c2 { animation-duration: 85s; animation-delay: -40s; } @keyframes drift { from { translate: -260px 0; } to { translate: 1060px 0; } } .butterfly { opacity: 0; } .bloom .butterfly { opacity: 1; animation: flutter-path 5.5s cubic-bezier(.45,.05,.4,1) forwards; } .wing { transform-box: fill-box; animation: flap .18s ease-in-out infinite alternate; } .wing.l { transform-origin: right center; } .wing.r { transform-origin: left center; } @keyframes flap { to { scale: .25 1; } } @keyframes flutter-path { 0% { transform: translate(-200px, 40px); } 30% { transform: translate(30px, -50px); } 55% { transform: translate(140px, 30px); } 80% { transform: translate(200px, -30px); } 100% { transform: translate(236px, 44px); } } .sparkle { opacity: 0; transform-box: fill-box; transform-origin: center; } .bloom .sparkle { animation: twinkle 1.6s ease-in-out infinite; } .bloom .sparkle:nth-of-type(2) { animation-delay: .5s; } .bloom .sparkle:nth-of-type(3) { animation-delay: 1s; } @keyframes twinkle { 0%, 100% { opacity: 0; scale: .4; } 50% { opacity: 1; scale: 1; } } @media (max-width: 860px) { .scene { position: absolute; inset: 0 0 auto 0; height: 360px; } .layout { grid-template-columns: 1fr; padding: 300px 16px 32px; } .card { grid-column: 1; padding: 26px 22px 22px; } body { background: linear-gradient(#e9f3ea, #dcecdf); } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; } .bloom .butterfly { transform: translate(236px, 44px); } } </style> </head> <body> <div class="scene" aria-hidden="true"> <svg id="art" viewBox="0 0 800 600" preserveAspectRatio="xMidYMax slice"> <defs> <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> <stop id="skyTop" offset="0" stop-color="#33416e"/> <stop id="skyMid" offset=".55" stop-color="#b98aa0"/> <stop id="skyLow" offset="1" stop-color="#f4b393"/> </linearGradient> <radialGradient id="sunGlow"> <stop offset="0" stop-color="#fff3c4" stop-opacity=".95"/> <stop offset=".35" stop-color="#ffd76a" stop-opacity=".45"/> <stop offset="1" stop-color="#ffd76a" stop-opacity="0"/> </radialGradient> <linearGradient id="wood" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#b47a4c"/><stop offset="1" stop-color="#8d5a35"/> </linearGradient> <linearGradient id="soil" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#7a5038"/><stop offset="1" stop-color="#5c3a28"/> </linearGradient> </defs> <rect width="800" height="600" fill="url(#sky)"/> <g class="sun" id="sun" style="transform: translateY(300px)"> <circle cx="300" cy="190" r="120" fill="url(#sunGlow)"/> <g class="sun-rays" opacity=".55"> <g stroke="#ffd76a" stroke-width="4" stroke-linecap="round"> <line x1="300" y1="122" x2="300" y2="138"/><line x1="300" y1="242" x2="300" y2="258"/> <line x1="232" y1="190" x2="248" y2="190"/><line x1="352" y1="190" x2="368" y2="190"/> <line x1="252" y1="142" x2="263" y2="153"/><line x1="337" y1="227" x2="348" y2="238"/> <line x1="348" y1="142" x2="337" y2="153"/><line x1="263" y1="227" x2="252" y2="238"/> </g> </g> <circle cx="300" cy="190" r="38" fill="#ffcf4a"/> <circle cx="292" cy="182" r="30" fill="#ffdf7a" opacity=".6"/> </g> <g class="cloud" opacity=".85"><ellipse cx="120" cy="170" rx="58" ry="17" fill="#fff"/><ellipse cx="150" cy="158" rx="32" ry="19" fill="#fff"/><ellipse cx="102" cy="160" rx="24" ry="14" fill="#fff"/></g> <g class="cloud c2" opacity=".7"><ellipse cx="80" cy="235" rx="44" ry="12" fill="#fff"/><ellipse cx="104" cy="226" rx="24" ry="14" fill="#fff"/></g> <path id="hillBack" d="M0 420 C 120 360, 230 372, 340 404 S 560 350, 800 392 V600 H0Z" fill="#7aa58a"/> <path id="hillFront" d="M0 462 C 140 420, 300 440, 430 458 S 660 430, 800 452 V600 H0Z" fill="#8fbf7a"/> <g fill="#6f9c63"> <circle cx="600" cy="440" r="22"/><circle cx="626" cy="446" r="16"/><circle cx="700" cy="436" r="26"/><circle cx="732" cy="444" r="18"/> </g> <!-- picket fence --> <g fill="#f3efe4" stroke="#d9d2bf" stroke-width="1"> <rect x="40" y="452" width="430" height="6" rx="2"/> <rect x="40" y="470" width="430" height="6" rx="2"/> <g id="pickets"></g> </g> <!-- Raised bed --> <path d="M60 476 Q250 462 440 476 L440 492 Q250 480 60 492Z" fill="url(#soil)"/> <g id="plants"> <!-- Plant 1: sunflower (length) --> <g class="plant" id="p-len" transform="translate(140 484)"> <g class="sway"> <path class="stem" pathLength="1" d="M0 0 C -8 -60, 10 -120, 0 -196"/> <path class="leaf l" d="M-2 -78 C -20 -96, -44 -94, -56 -80 C -42 -70, -20 -66, -2 -78Z" fill="#4fa35d"/> <path class="leaf r" d="M2 -122 C 20 -138, 42 -136, 54 -122 C 40 -112, 20 -110, 2 -122Z" fill="#4fa35d"/> <g transform="translate(0 -196)"><g class="head"> <g fill="#ffc23d" stroke="#e9a91c" stroke-width="1"> <ellipse cx="0" cy="-24" rx="9" ry="20"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(30)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(60)"/> <ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(90)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(120)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(150)"/> <ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(180)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(210)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(240)"/> <ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(270)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(300)"/><ellipse cx="0" cy="-24" rx="9" ry="20" transform="rotate(330)"/> </g> <circle r="17" fill="#6b3e1f"/> <g fill="#8a5428"><circle cx="-6" cy="-5" r="2"/><circle cx="5" cy="-7" r="2"/><circle cx="7" cy="4" r="2"/><circle cx="-4" cy="7" r="2"/><circle cx="0" cy="0" r="2"/></g> </g></g> </g> <g class="seed"><ellipse cx="0" cy="2" rx="14" ry="6" fill="#6a4430"/><ellipse cx="0" cy="-2" rx="4" ry="5.5" fill="#efe1c3" transform="rotate(20)"/></g> </g> <!-- Plant 2: tulip (letters + number) --> <g class="plant" id="p-mix" transform="translate(250 482)"> <g class="sway"> <path class="stem" pathLength="1" d="M0 0 C 6 -50, -6 -96, 0 -146"/> <path class="leaf l" d="M-1 -10 C -20 -40, -26 -78, -16 -104 C -8 -76, -4 -44, -1 -10Z" fill="#58ad66"/> <path class="leaf r" d="M1 -6 C 22 -30, 30 -60, 24 -86 C 14 -60, 8 -32, 1 -6Z" fill="#4c9d5a"/> <g transform="translate(0 -150)"><g class="head"> <path d="M-20 -6 C -22 -26, -14 -40, -8 -44 L0 -26 L8 -44 C 14 -40, 22 -26, 20 -6 C 14 8, -14 8, -20 -6Z" fill="#ff6b57"/> <path d="M0 -26 L-8 -44 C -3 -48, 3 -48, 8 -44Z" fill="#e2503d"/> <path d="M-12 -2 C -14 -16, -10 -28, -6 -32" stroke="#ff9a7c" stroke-width="3" fill="none" stroke-linecap="round"/> </g></g> </g> <g class="seed"><ellipse cx="0" cy="2" rx="14" ry="6" fill="#6a4430"/><ellipse cx="0" cy="-2" rx="4" ry="5.5" fill="#efe1c3" transform="rotate(-15)"/></g> </g> <!-- Plant 3: cosmos (not your email name) --> <g class="plant" id="p-name" transform="translate(360 484)"> <g class="sway"> <path class="stem" pathLength="1" d="M0 0 C -10 -50, 8 -110, -2 -170"/> <path class="leaf l" d="M-4 -60 C -18 -74, -34 -76, -46 -68 C -34 -58, -18 -54, -4 -60Z" fill="#4fa35d"/> <path class="leaf r" d="M2 -98 C 16 -112, 32 -114, 44 -106 C 32 -96, 16 -92, 2 -98Z" fill="#4fa35d"/> <g transform="translate(-2 -172)"><g class="head"> <g fill="#7468f0" stroke="#5b50d8" stroke-width="1"> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(45)"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(90)"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(135)"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(180)"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(225)"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(270)"/> <path d="M-7 -6 L-9 -30 L-3 -26 L0 -33 L3 -26 L9 -30 L7 -6Z" transform="rotate(315)"/> </g> <circle r="9" fill="#ffd23f"/><circle r="4" fill="#f2a516"/> </g></g> </g> <g class="seed"><ellipse cx="0" cy="2" rx="14" ry="6" fill="#6a4430"/><ellipse cx="0" cy="-2" rx="4" ry="5.5" fill="#efe1c3" transform="rotate(10)"/></g> </g> </g> <!-- bed front planks --> <rect x="52" y="488" width="396" height="58" rx="5" fill="url(#wood)"/> <g stroke="#7a4a2a" stroke-width="2" opacity=".55"> <line x1="52" y1="517" x2="448" y2="517"/> <path d="M90 500 q30 -4 60 0 M220 528 q40 -5 80 0 M330 500 q25 -3 50 0 M120 536 q20 -3 40 0" fill="none"/> </g> <rect x="48" y="484" width="10" height="66" rx="3" fill="#9a6640"/><rect x="442" y="484" width="10" height="66" rx="3" fill="#9a6640"/> <!-- plant markers --> <g font-family="ui-monospace, Menlo, monospace" font-size="12" font-weight="700" text-anchor="middle" fill="#5a3a22"> <g transform="translate(172 498) rotate(4)"><rect x="-2" y="-4" width="4" height="34" fill="#c99a6a"/><rect x="-22" y="-26" width="44" height="22" rx="4" fill="#f4e6cc" stroke="#c99a6a"/><text y="-11">10+</text></g> <g transform="translate(282 498) rotate(-3)"><rect x="-2" y="-4" width="4" height="34" fill="#c99a6a"/><rect x="-22" y="-26" width="44" height="22" rx="4" fill="#f4e6cc" stroke="#c99a6a"/><text y="-11">a·1</text></g> <g transform="translate(392 498) rotate(5)"><rect x="-2" y="-4" width="4" height="34" fill="#c99a6a"/><rect x="-26" y="-26" width="52" height="22" rx="4" fill="#f4e6cc" stroke="#c99a6a"/><text y="-11">≠ name</text></g> </g> <!-- foreground grass --> <path d="M0 548 C 150 536, 320 552, 500 542 S 700 538, 800 546 V600 H0Z" fill="#6fae5f"/> <g id="blades" stroke="#5c9a4f" stroke-width="2.5" stroke-linecap="round" fill="none"></g> <!-- butterfly --> <g class="butterfly" id="butterfly"> <g transform="translate(120 250)"> <path class="wing l" d="M0 0 C -14 -18, -30 -16, -26 -2 C -30 8, -14 14, 0 2Z" fill="#ff8fb0" stroke="#d9587f" stroke-width="1"/> <path class="wing r" d="M0 0 C 14 -18, 30 -16, 26 -2 C 30 8, 14 14, 0 2Z" fill="#ffb0c8" stroke="#d9587f" stroke-width="1"/> <rect x="-1.6" y="-10" width="3.2" height="16" rx="1.6" fill="#3b2a3a"/> </g> </g> <g fill="#fff6c7"> <path class="sparkle" d="M120 270 l3 9 9 3 -9 3 -3 9 -3 -9 -9 -3 9 -3z"/> <path class="sparkle" d="M300 300 l2.5 7 7 2.5 -7 2.5 -2.5 7 -2.5 -7 -7 -2.5 7 -2.5z"/> <path class="sparkle" d="M410 280 l3 8 8 3 -8 3 -3 8 -3 -8 -8 -3 8 -3z"/> </g> </svg> </div> <div class="layout" id="layout"> <div class="brand" aria-hidden="true"> <svg viewBox="0 0 32 32"><path d="M16 30 V14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M16 18 C 8 18, 5 12, 5 6 C 12 6, 16 10, 16 18Z M16 15 C 22 15, 27 11, 27 5 C 20 5, 16 8, 16 15Z" fill="currentColor"/></svg> Greenhouse </div> <main class="card" id="card"> <div class="intro"> <h1>Grow your account</h1> <p class="sub">Make a strong password and watch the garden come up. Every rule you meet is a flower.</p> </div> <form id="form" novalidate> <label>Email <input id="email" name="email" type="email" autocomplete="username" inputmode="email" required> </label> <label>Password <span class="field pw"> <input id="password" name="password" type="password" autocomplete="new-password" required aria-describedby="rules strength"> <button class="show" id="show" type="button" aria-pressed="false" aria-controls="password">Show</button> </span> </label> <p class="strength" id="strength"><span class="meter" aria-hidden="true"><i></i><i></i><i></i></span><b id="strengthText">Not started</b></p> <ul class="rules" id="rules"> <li data-rule="len"><span class="bud" aria-hidden="true"><svg viewBox="0 0 12 12"><path d="M2 6.5 5 9 10 3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="txt">At least 10 characters</span><span class="sr"> (not yet)</span></li> <li data-rule="mix"><span class="bud" aria-hidden="true"><svg viewBox="0 0 12 12"><path d="M2 6.5 5 9 10 3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="txt">Letters and at least one number</span><span class="sr"> (not yet)</span></li> <li data-rule="name"><span class="bud" aria-hidden="true"><svg viewBox="0 0 12 12"><path d="M2 6.5 5 9 10 3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="txt">Doesn't contain your email name</span><span class="sr"> (not yet)</span></li> </ul> <button class="btn" id="submit" type="submit">Create account</button> <p class="msg" id="msg" role="status" aria-live="polite"></p> <p class="foot">Already have an account? <a href="#">Sign in</a></p> <p class="hint">Demo: "tidepool7harbor" is strong; "ada123" shows what's missing.</p> </form> <section class="done" aria-live="polite"> <div class="badge" aria-hidden="true"><svg width="30" height="30" viewBox="0 0 32 32"><path d="M16 30 V14" stroke="#2e7a4b" stroke-width="2.6" stroke-linecap="round"/><path d="M16 18 C 8 18, 5 12, 5 6 C 12 6, 16 10, 16 18Z M16 15 C 22 15, 27 11, 27 5 C 20 5, 16 8, 16 15Z" fill="#2e7a4b"/></svg></div> <h2 id="doneTitle" tabindex="-1">Account created</h2> <p>Your garden is in full bloom. We sent a confirmation link to <b id="doneEmail"></b>.</p> <a class="btn" href="#">Continue</a> </section> </main> </div> <script> (() => { const $ = (id) => document.getElementById(id); const email = $("email"), password = $("password"), show = $("show"), form = $("form"); const submit = $("submit"), msg = $("msg"), strength = $("strength"), strengthText = $("strengthText"); const items = [...document.querySelectorAll("#rules li")]; const art = $("art"), sun = $("sun"), layout = $("layout"); const plants = { len: $("p-len"), mix: $("p-mix"), name: $("p-name") }; const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; // Decorative details generated once: fence pickets and grass blades. const ns = "http://www.w3.org/2000/svg"; for (let x = 46; x < 470; x += 22) { const p = document.createElementNS(ns, "path"); p.setAttribute("d", `M${x} 486 V444 l6 -8 6 8 V486Z`); $("pickets").append(p); } for (let x = 6; x < 800; x += 9) { const h = 8 + ((x * 37) % 13), lean = ((x * 13) % 9) - 4; const p = document.createElementNS(ns, "path"); p.setAttribute("d", `M${x} 556 q${lean / 2} ${-h / 2} ${lean} ${-h}`); $("blades").append(p); } // Frame the garden bed on narrow screens. const frame = () => art.setAttribute("viewBox", innerWidth < 860 ? "20 170 470 420" : "0 0 800 600"); frame(); addEventListener("resize", frame); const RULES = { len: (p) => p.length >= 10, mix: (p) => /[a-z]/i.test(p) && /\d/.test(p), name: (p) => { const n = email.value.split("@")[0].toLowerCase().replace(/[^a-z0-9]/g, ""); return p.length > 0 && (n.length < 3 || !p.toLowerCase().includes(n)); }, }; // Sky goes from dawn to bright day as rules are met. const DAWN = { top: [51, 65, 110], mid: [185, 138, 160], low: [244, 179, 147], back: [96, 128, 120], front: [118, 156, 112] }; const DAY = { top: [124, 196, 232], mid: [190, 228, 238], low: [234, 246, 233], back: [122, 165, 138], front: [143, 191, 122] }; const mix = (a, b, t) => `rgb(${a.map((v, i) => Math.round(v + (b[i] - v) * t)).join(",")})`; let skyT = 0, skyTarget = 0, raf = 0; function paintSky(t) { $("skyTop").setAttribute("stop-color", mix(DAWN.top, DAY.top, t)); $("skyMid").setAttribute("stop-color", mix(DAWN.mid, DAY.mid, t)); $("skyLow").setAttribute("stop-color", mix(DAWN.low, DAY.low, t)); $("hillBack").setAttribute("fill", mix(DAWN.back, DAY.back, t)); $("hillFront").setAttribute("fill", mix(DAWN.front, DAY.front, t)); document.body.classList.toggle("day", t > .55); } function animateSky() { cancelAnimationFrame(raf); if (reduced) { skyT = skyTarget; paintSky(skyT); return; } const step = () => { skyT += (skyTarget - skyT) * .06; paintSky(skyT); if (Math.abs(skyTarget - skyT) > .002) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); } const LEVELS = ["Not started", "Weak", "Getting there", "Strong"]; function check() { const p = password.value; let met = 0; for (const li of items) { const key = li.dataset.rule, ok = RULES[key](p); li.classList.toggle("met", ok); li.querySelector(".sr").textContent = ok ? " (done)" : " (not yet)"; plants[key].classList.toggle("grown", ok); plants[key].classList.toggle("sprout", !ok && p.length > 0); if (ok) met++; } const lvl = p ? Math.max(1, met) : 0; strength.className = "strength lv" + lvl; strengthText.textContent = LEVELS[lvl]; sun.style.transform = `translateY(${p ? 210 - met * 70 : 300}px)`; skyTarget = p ? .12 + met * .29 : 0; animateSky(); return met === items.length; } password.addEventListener("input", () => { const allMet = check(); items.forEach((li) => li.classList.remove("missing")); if (allMet && msg.classList.contains("err")) { msg.className = "msg"; msg.textContent = ""; password.removeAttribute("aria-invalid"); } }); email.addEventListener("input", () => { if (password.value) check(); email.removeAttribute("aria-invalid"); }); show.addEventListener("click", () => { const visible = password.type === "password"; password.type = visible ? "text" : "password"; show.textContent = visible ? "Hide" : "Show"; show.setAttribute("aria-pressed", String(visible)); password.focus(); }); form.addEventListener("submit", async (e) => { e.preventDefault(); if (submit.getAttribute("aria-busy") === "true") return; msg.className = "msg"; msg.textContent = ""; email.removeAttribute("aria-invalid"); password.removeAttribute("aria-invalid"); if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.value.trim())) { email.setAttribute("aria-invalid", "true"); msg.className = "msg err"; msg.textContent = "Enter your email address, like name@example.com."; email.focus(); return; } if (!check()) { const missing = items.filter((li) => !li.classList.contains("met")); missing.forEach((li) => { li.classList.add("missing"); const pl = plants[li.dataset.rule]; pl.classList.remove("nudge"); void pl.getBoundingClientRect(); pl.classList.add("nudge"); }); password.setAttribute("aria-invalid", "true"); msg.className = "msg err"; msg.textContent = "Your password still needs: " + missing.map((li) => li.querySelector(".txt").textContent.toLowerCase()).join("; ") + "."; password.focus(); return; } submit.setAttribute("aria-busy", "true"); submit.disabled = true; submit.textContent = "Planting…"; await new Promise((r) => setTimeout(r, 750)); // your real request goes here submit.removeAttribute("aria-busy"); submit.disabled = false; submit.textContent = "Create account"; $("doneEmail").textContent = email.value.trim(); art.classList.add("bloom"); sun.style.transform = "translateY(-10px)"; skyTarget = 1; animateSky(); $("card").classList.add("is-done"); $("doneTitle").focus({ preventScroll: true }); }); check(); })(); </script> </body> </html>

Guardrails

  • Rule state is in the form as text (with visually hidden done / not yet labels), so the garden is purely decorative and aria-hidden.
  • Use autocomplete="new-password" with a Show/Hide toggle so password managers can suggest a strong password, which then blooms all three flowers at once.
  • Never gate sign-in behind the interaction: the form works the moment the page loads, and any focus or typing skips the show.
  • A real <form> with <label>s, type="email"/"password", and autocomplete="username" + "current-password" (or "new-password" on sign-up, "one-time-code" for codes) so password managers, passkeys and SMS autofill work.
  • Keyboard path: Tab moves email, password, submit in order; Enter submits.
  • Errors in plain words next to the field, announced (aria-live), with focus moved to the field to fix. Don't clear what they typed.
  • Answer the submit within 100ms (pending state, button disabled while the request runs).
  • Respect prefers-reduced-motion: the delight becomes a still state, never a blocker.
  • Any character or effect is decorative: aria-hidden, and text contrast never depends on it.