iOS sign-up form + keyboardLayoutGuide
A UIKit-style sign-up form built as a constraint chain, with the primary button anchored data-⚓️-side="above" a simulated keyboardLayoutGuide so it rides the keyboard up.
<header class="page-intro">
<h1>Sign-up form + keyboardLayoutGuide</h1>
<p>
Every field is anchored below the previous one (<code>data-⚓️-below</code>, 8pt standard spacing), with
equal widths via <code>data-⚓️-sync-width</code>. The primary button hangs off a simulated
<code>keyboardLayoutGuide</code> with <code>data-⚓️-side="above"</code>, so it rides the keyboard up.
Wide screens switch to the regular size class: two columns, <code>--⚓️-mult-x: 0.5</code>, hardware keyboard.
</p>
</header>
<div class="screen" id="scr" data-⚓️-root data-⚓️-name="--scr" data-kb="hidden" data-mode="signup">
<!-- safeAreaLayoutGuide (top): the status bar region -->
<div class="status-bar" id="safe" data-⚓️-name="--safe" data-⚓️-pin-top data-⚓️-pin-x aria-hidden="true">
<span class="time">9:41</span>
<span class="island"></span>
<span class="glyphs"><i class="sig"></i><i class="wifi"></i><i class="batt"></i></span>
</div>
<!-- keyboardLayoutGuide: top == safeArea.bottom when hidden, == keyboard.top when shown -->
<div class="kbd-guide" id="kbd" data-⚓️-name="--kbd" data-⚓️-pin-bottom data-⚓️-pin-x aria-hidden="true"></div>
<!-- the simulated software keyboard (keys are built by script.js) -->
<div class="keyboard" id="kb" data-⚓️-pin-bottom data-⚓️-pin-x aria-hidden="true">
<div class="kb-inner">
<div class="kb-suggest" id="kbSuggest"></div>
<div class="kb-keys" id="kbKeys"></div>
</div>
</div>
<!-- navigation bar: navBar.top = safeArea.bottom -->
<nav class="nav-bar" id="nav" data-⚓️-name="--nav" data-⚓️-to="--safe" data-⚓️-below data-⚓️-pin-x>
<button class="bar-btn" id="cbtn" data-⚓️-name="--cbtn" type="button" aria-pressed="false" aria-controls="inspector">Constraints</button>
<span class="nav-title" id="navTitle">Create account</span>
<button class="bar-btn trailing" id="kbtn" type="button" aria-pressed="false" title="Software keyboard (regular width only)">Keyboard</button>
</nav>
<!-- scrollView.top = safeArea.bottom, scrollView.bottom = keyboardLayoutGuide.top -->
<form class="scroll" id="sv" data-⚓️-below="--safe" data-⚓️-above="--kbd" data-⚓️-pin-x novalidate>
<!-- layoutMarginsGuide (20pt each side, zero height) -->
<div class="guide" id="margins" data-⚓️-name="--margins" data-⚓️-pin-top data-⚓️-pin-x></div>
<h2 class="large-title" id="title" data-⚓️-name="--title" data-⚓️-pin-top data-⚓️-align-leading="--margins">Create account</h2>
<p class="subtitle col" id="sub" data-⚓️-name="--sub"
data-⚓️-below="--title" data-⚓️-align-leading="--title" data-⚓️-sync-width="--margins" data-⚓️-transition="glide">
One login for every dashboard. Tap a field to raise the keyboard.
</p>
<div class="segmented col" id="seg" role="radiogroup" aria-label="Mode" data-⚓️-name="--seg"
data-⚓️-below="--sub" data-⚓️-align-leading="--margins" data-⚓️-sync-width="--margins" data-⚓️-transition="glide">
<span class="seg-pill" id="pill" data-⚓️-fill="--seg-a" data-⚓️-transition="glide" aria-hidden="true"></span>
<button type="button" class="seg-btn" id="segA" data-⚓️-name="--seg-a" role="radio" aria-checked="true" data-mode="signup">Sign up</button>
<button type="button" class="seg-btn" id="segB" data-⚓️-name="--seg-b" role="radio" aria-checked="false" data-mode="login">Log in</button>
</div>
<h3 class="section-header" id="hdrp" data-⚓️-name="--hdrp" data-⚓️-below="--seg" data-⚓️-align-leading="--margins" data-⚓️-transition="glide">Profile</h3>
<label class="field col" id="fName" data-⚓️-name="--name"
data-⚓️-below="--hdrp" data-⚓️-align-leading="--margins" data-⚓️-sync-width="--margins" data-⚓️-transition="glide">
<span class="field-label">Name</span>
<input name="name" autocomplete="name" placeholder="Required" enterkeyhint="next">
</label>
<label class="field" id="fEmail" data-⚓️-name="--email"
data-⚓️-below="--name" data-⚓️-align-leading="--name" data-⚓️-sync-width="--name" data-⚓️-transition="glide">
<span class="field-label">Email</span>
<input name="email" type="email" autocomplete="email" placeholder="you@example.com" enterkeyhint="next">
</label>
<label class="field" id="fPw" data-⚓️-name="--pw"
data-⚓️-below="--email" data-⚓️-align-leading="--email" data-⚓️-sync-width="--email" data-⚓️-transition="glide">
<span class="field-label">Password</span>
<input name="password" type="password" autocomplete="new-password" placeholder="8+ characters" enterkeyhint="go">
</label>
<p class="footnote" id="hint" data-⚓️-name="--hint"
data-⚓️-below="--pw" data-⚓️-align-leading="--pw" data-⚓️-sync-width="--pw" data-⚓️-transition="glide">
Use 8 or more characters with a mix of letters, numbers and symbols.
</p>
<h3 class="section-header" id="hdra" data-⚓️-name="--hdra" data-⚓️-below="--hint" data-⚓️-align-leading="--margins" data-⚓️-transition="glide">Account preview</h3>
<div class="group" id="summary" data-⚓️-name="--summary"
data-⚓️-below="--hdra" data-⚓️-align-leading="--hdra" data-⚓️-sync-width="--margins" data-⚓️-transition="glide">
<!-- detail precedes label in the DOM so the label may anchor to it when the value wins -->
<div class="row" data-⚓️-root data-⚓️-scope="all">
<span class="detail" data-⚓️-name="--val" data-⚓️-after="--lbl" data-⚓️-pin-trailing data-⚓️-pin-y id="dName">Not set</span>
<span class="lbl" data-⚓️-name="--lbl">Display name</span>
</div>
<div class="row" data-⚓️-root data-⚓️-scope="all">
<span class="detail" data-⚓️-name="--val" data-⚓️-after="--lbl" data-⚓️-pin-trailing data-⚓️-pin-y id="dEmail">Not set</span>
<span class="lbl" data-⚓️-name="--lbl">Email</span>
</div>
<div class="row" id="rowTz" data-⚓️-root data-⚓️-scope="all">
<span class="detail" data-⚓️-name="--val" data-⚓️-after="--lbl" data-⚓️-pin-trailing data-⚓️-pin-y>Pacific Daylight Time (Los Angeles, United States)</span>
<span class="lbl" data-⚓️-name="--lbl">Time zone for reminders</span>
</div>
<label class="row" data-⚓️-root>
<span class="lbl">Value resists compression</span>
<input type="checkbox" role="switch" class="switch" id="prio" data-⚓️-pin-trailing>
</label>
</div>
<p class="footnote" id="footer" data-⚓️-name="--footer"
data-⚓️-below="--summary" data-⚓️-align-leading="--summary" data-⚓️-sync-width="--summary" data-⚓️-transition="glide">
Off: the label has the higher compression resistance (751), so long values truncate first.
On: the value wins and the label truncates.
</p>
<!-- contentInset.bottom: keeps the last rows scrollable clear of the floating button -->
<div class="spacer" data-⚓️-below="--hint" data-⚓️-pin-leading></div>
<div class="spacer" data-⚓️-below="--footer" data-⚓️-pin-leading></div>
</form>
<!-- the button bar and button: bottom = keyboardLayoutGuide.top (− 8 for the button) -->
<div class="cta-bar" id="ctabar" data-⚓️-to="--kbd" data-⚓️-side="above" data-⚓️-pin-x aria-hidden="true"></div>
<button class="cta" id="cta" type="submit" form="sv" data-⚓️-name="--cta" data-⚓️-to="--kbd" data-⚓️-side="above" data-⚓️-pin-x>Create account</button>
<!-- home indicator: centerX = superview.centerX, bottom = superview.bottom − 8 -->
<span class="home-indicator" data-⚓️-pin-bottom data-⚓️-pin-x aria-hidden="true"></span>
</div>
<!-- Constraints inspector: a sheet on compact width, a popover off the bar button on regular -->
<aside class="inspector" id="inspector" popover="manual" aria-labelledby="inspTitle"
data-⚓️-to="--scr" data-⚓️-align-bottom data-⚓️-stretch-x>
<header class="insp-head">
<h2 id="inspTitle">Constraints</h2>
<button type="button" class="bar-btn" id="inspClose">Done</button>
</header>
<p class="insp-note">Hover or focus a group to highlight the view. Dashed outlines are the named anchors.</p>
<ul class="insp-list" id="inspList"></ul>
</aside>/* Demo styling only. Every view's position and size comes from data-⚓️-*
attributes (the "constraints"); this file sets visuals, the Auto Layout
constants (margins = constraint constants), the keyboardLayoutGuide's
height per state, and the size-class multipliers. */
/* ---------- iOS system colours ---------- */
:root {
color-scheme: light dark;
--bg: #f2f2f7; /* systemGroupedBackground */
--cell: #fff; /* secondarySystemGroupedBackground */
--fill: rgb(118 118 128 / 0.12);
--label: #000;
--label2: rgb(60 60 67 / 0.6);
--label3: rgb(60 60 67 / 0.3);
--sep: rgb(60 60 67 / 0.29);
--tint: #007aff;
--bar: rgb(249 249 249 / 0.78);
--pill: #fff;
--kb-bg: rgb(209 212 219 / 0.94);
--key: #fff;
--key2: #abb0bc;
--key-ink: #000;
--page: #e5e5ea;
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, "Segoe UI", sans-serif;
--kb-ease: cubic-bezier(0.38, 0.7, 0.125, 1);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #000;
--cell: #1c1c1e;
--fill: rgb(118 118 128 / 0.24);
--label: #fff;
--label2: rgb(235 235 245 / 0.6);
--label3: rgb(235 235 245 / 0.3);
--sep: rgb(84 84 88 / 0.65);
--tint: #0a84ff;
--bar: rgb(22 22 24 / 0.78);
--pill: #636366;
--kb-bg: rgb(43 43 45 / 0.96);
--key: #6b6b6e;
--key2: #464649;
--key-ink: #fff;
--page: #111113;
}
}
*, ::before, ::after { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--label);
font: 17px/1.29 var(--font);
letter-spacing: -0.41px;
-webkit-font-smoothing: antialiased;
}
button, input { font: inherit; letter-spacing: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
/* ---------- the "device" ---------- */
.page-intro { display: none; }
.screen {
/* safeAreaInsets: real env() values win, otherwise a simulated iPhone */
--safe-t: max(env(safe-area-inset-top), 47px);
--safe-b: max(env(safe-area-inset-bottom), 34px);
--kb-h: calc(262px + var(--safe-b)); /* keyboard frame incl. home area */
--margin: 20px; /* layoutMargins */
--inset-top: 44px; /* contentInset.top (nav bar) */
inline-size: 100vw;
block-size: 100dvh;
overflow: hidden;
background: var(--bg);
timeline-scope: --sv;
}
/* regular width: an iPad-style form sheet with a hardware keyboard */
@media (min-width: 760px) {
body {
background: var(--page);
min-block-size: 100dvh;
display: grid;
place-content: center;
gap: 18px;
padding: 24px;
}
.page-intro {
display: block;
max-inline-size: 72ch;
margin: 0 auto;
text-align: center;
}
.page-intro h1 { font-size: 22px; letter-spacing: 0.35px; margin: 0 0 6px; }
.page-intro p { margin: 0; font-size: 14px; color: var(--label2); letter-spacing: -0.08px; }
.page-intro code { font: 12.5px ui-monospace, "SF Mono", monospace; color: var(--label); }
.screen {
--safe-t: 24px;
--safe-b: 20px;
--kb-h: 300px;
--margin: 40px;
inline-size: min(1000px, 100vw - 48px);
block-size: clamp(560px, 100dvh - 170px, 760px);
border-radius: 28px;
box-shadow: 0 0 0 1px var(--sep), 0 30px 80px -20px rgb(0 0 0 / 0.35);
}
/* proportional widths: column = 0.5 × margins.width − 18 */
.col { --⚓️-mult-x: 0.5; --⚓️-offset-x: -18px; }
.status-bar .island { display: none; }
}
/* ---------- guides ---------- */
.guide { block-size: 0; margin-inline: var(--margin); }
.kbd-guide {
block-size: var(--safe-b);
transition: block-size 0.42s var(--kb-ease);
pointer-events: none;
}
.screen[data-kb="shown"] .kbd-guide { block-size: var(--kb-h); }
/* ---------- status bar ---------- */
.status-bar {
block-size: var(--safe-t);
z-index: 5;
display: flex;
align-items: end;
justify-content: space-between;
padding: 0 26px 12px 34px;
font: 600 15px/1 var(--font);
pointer-events: none;
animation: bar-in linear both;
animation-timeline: --sv;
animation-range: 0px 40px;
}
@media (min-width: 760px) {
.status-bar { padding: 0 22px 5px; font-size: 12px; align-items: center; }
}
.island {
position: absolute;
inset-block-start: 11px;
inset-inline: 0;
margin-inline: auto; /* centerX = superview.centerX */
inline-size: 124px;
block-size: 36px;
border-radius: 20px;
background: #000;
}
.glyphs { display: flex; gap: 6px; align-items: center; }
.glyphs i { display: block; background: currentColor; border-radius: 2px; }
.sig { inline-size: 17px; block-size: 11px; clip-path: polygon(0 100%, 100% 0, 100% 100%); }
.wifi { inline-size: 15px; block-size: 11px; border-radius: 15px 15px 2px 2px !important; clip-path: polygon(50% 100%, 0 20%, 100% 20%); }
.batt { inline-size: 25px; block-size: 12px; border-radius: 4px !important; box-shadow: inset 0 0 0 2px var(--label2); background: linear-gradient(to right, currentColor 72%, transparent 0) content-box !important; padding: 3px; }
/* ---------- navigation bar ---------- */
.nav-bar {
block-size: 44px;
z-index: 5;
display: flex;
align-items: center;
justify-content: space-between;
padding-inline: 8px;
animation: bar-in linear both;
animation-timeline: --sv;
animation-range: 0px 40px;
}
@keyframes bar-in {
from { background: transparent; box-shadow: none; backdrop-filter: none; }
to {
background: var(--bar);
box-shadow: 0 0.5px 0 var(--sep);
backdrop-filter: saturate(180%) blur(20px);
}
}
.nav-bar .nav-title {
position: absolute;
inset-inline: 0;
margin-inline: auto;
inline-size: fit-content;
font-weight: 600;
pointer-events: none;
opacity: 0; /* shown once the large title scrolls away */
animation: fade-in linear both;
animation-timeline: --sv;
animation-range: 40px 64px;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.bar-btn {
background: none;
border: 0;
color: var(--tint);
padding: 8px;
border-radius: 8px;
cursor: pointer;
}
.bar-btn[aria-pressed="true"] { font-weight: 600; }
.bar-btn:active { opacity: 0.4; }
.bar-btn.trailing { visibility: hidden; }
@media (min-width: 760px) { .bar-btn.trailing { visibility: visible; } }
/* ---------- scroll view ---------- */
.scroll {
margin: 0;
overflow: auto;
overscroll-behavior: contain;
scroll-timeline: --sv block;
/* keep a focused field clear of the nav bar and the floating button */
scroll-padding-block: calc(var(--inset-top) + 8px) 82px;
scrollbar-width: none;
}
.large-title {
margin: calc(var(--inset-top) + 4px) 0 0;
font-size: 34px;
line-height: 41px;
font-weight: 700;
letter-spacing: 0.37px;
white-space: nowrap;
}
.subtitle { margin: 6px 0 0; color: var(--label2); font-size: 15px; letter-spacing: -0.24px; }
/* segmented control: the pill is data-⚓️-fill on the selected segment */
.segmented {
margin-block-start: 20px;
block-size: 32px;
display: grid;
grid-template-columns: 1fr 1fr; /* distribution = fillEqually */
padding: 2px;
border-radius: 9px;
background: var(--fill);
}
.seg-pill {
border-radius: 7px;
background: var(--pill);
box-shadow: 0 3px 8px rgb(0 0 0 / 0.12), 0 3px 1px rgb(0 0 0 / 0.04);
}
.seg-btn {
position: relative;
z-index: 1;
border: 0;
background: none;
font-size: 13px;
font-weight: 500;
letter-spacing: -0.08px;
border-radius: 7px;
cursor: pointer;
}
.seg-btn[aria-checked="true"] { font-weight: 600; }
.section-header {
margin: 24px 0 0 16px;
font-size: 13px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.08px;
text-transform: uppercase;
color: var(--label2);
white-space: nowrap;
}
#hdrp { margin-block-start: 28px; }
.field {
margin-block-start: 8px; /* standard spacing */
block-size: 50px;
display: flex;
align-items: center;
gap: 8px;
padding-inline: 16px;
border-radius: 12px;
background: var(--cell);
cursor: text;
}
#fName { margin-block-start: 6px; }
.field-label { flex: none; inline-size: 88px; }
.field input {
flex: 1;
min-inline-size: 0;
border: 0;
background: none;
outline: 0;
padding: 0;
caret-color: var(--tint);
}
.field input::placeholder { color: var(--label3); }
.field:focus-within { box-shadow: 0 0 0 2px var(--tint); }
.footnote {
margin: 7px 0 0 16px;
font-size: 13px;
line-height: 18px;
letter-spacing: -0.08px;
color: var(--label2);
--⚓️-offset-x: -32px;
}
#footer { --⚓️-offset-x: -32px; }
/* login: the name field and hint are hidden; neighbours re-anchor */
.screen[data-mode="login"] :is(#fName, #hint) { opacity: 0; visibility: hidden; }
/* glide's geometry transition (from the library) plus the fade */
#fName, #hint {
transition:
inset 0.25s cubic-bezier(0.25, 1, 0.5, 1),
width 0.25s cubic-bezier(0.25, 1, 0.5, 1),
opacity 0.2s, visibility 0.2s;
}
/* ---------- grouped inset table ---------- */
#hdra { margin-block-start: 28px; }
@media (min-width: 760px) {
#hdra { margin-block-start: 0; margin-inline-start: 52px; } /* name.trailing + 36 + 16 */
}
.group {
margin-block-start: 6px;
margin-inline-start: -16px; /* table.leading = header.leading − 16 */
border-radius: 12px;
background: var(--cell);
overflow: hidden;
}
.row {
--row-h: 44px;
display: flex;
align-items: center;
min-block-size: var(--row-h);
padding-inline: 16px;
white-space: nowrap;
}
.row + .row::before {
content: "";
position: absolute;
inset: 0 0 auto 16px; /* separatorInset.leading = 16 */
block-size: 0.5px;
background: var(--sep);
}
.lbl, .detail {
overflow: hidden;
text-overflow: ellipsis;
line-height: var(--row-h);
}
.lbl { flex: none; }
.detail { color: var(--label2); text-align: end; margin-inline-end: 16px; }
/* constraint constants, only while that constraint is active */
.detail[data-⚓️-after] { margin-inline-start: 8px; }
.lbl[data-⚓️-before] { margin-inline: 16px 8px; }
/* value wins: detail.width ≤ row.width − 104 so the label keeps a sliver */
.detail:not([data-⚓️-after]) { max-inline-size: calc(100% - 104px); }
.switch {
appearance: none;
margin: 0 16px 0 0;
inline-size: 51px;
block-size: 31px;
border-radius: 16px;
background: var(--fill);
cursor: pointer;
transition: background 0.2s;
}
.switch::after {
content: "";
display: block;
inline-size: 27px;
block-size: 27px;
margin: 2px;
border-radius: 50%;
background: #fff;
box-shadow: 0 3px 8px rgb(0 0 0 / 0.15), 0 3px 1px rgb(0 0 0 / 0.06);
transition: translate 0.25s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.switch:checked { background: #34c759; }
.switch:checked::after { translate: 20px; }
:dir(rtl) .switch:checked::after { translate: -20px; }
.spacer { block-size: 96px; inline-size: 1px; pointer-events: none; }
/* ---------- primary button riding the keyboardLayoutGuide ---------- */
.cta-bar {
z-index: 3;
block-size: 66px;
background: linear-gradient(to top, var(--bg) 60%, transparent);
pointer-events: none;
}
.cta {
z-index: 4;
margin: 0 var(--margin) 8px; /* bottom = guide.top − 8, leading/trailing = margins */
block-size: 50px;
border: 0;
border-radius: 14px;
background: var(--tint);
color: #fff;
font-weight: 600;
cursor: pointer;
}
/* regular width: button.leading = name.leading, button.width = name.width */
.cta[data-⚓️-sync-width] { margin-inline: 0; }
.cta:active { filter: brightness(0.85); }
.cta[data-done] { background: #34c759; }
.home-indicator {
z-index: 9;
margin: 0 auto 8px; /* centerX = superview.centerX */
inline-size: 134px;
block-size: 5px;
border-radius: 3px;
background: var(--label);
pointer-events: none;
}
@media (min-width: 760px) { .home-indicator { inline-size: 200px; margin-block-end: 6px; } }
/* ---------- simulated keyboard ---------- */
.keyboard {
z-index: 6;
block-size: 0;
overflow: hidden;
background: var(--kb-bg);
backdrop-filter: blur(30px);
transition: block-size 0.42s var(--kb-ease);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.screen[data-kb="shown"] .keyboard { block-size: var(--kb-h); }
.kb-inner { block-size: var(--kb-h); padding: 0 3px; }
.kb-suggest {
block-size: 44px;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
font-size: 16px;
color: var(--key-ink);
}
.kb-suggest button {
border: 0;
background: none;
block-size: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.kb-suggest button + button { box-shadow: -0.5px 0 0 var(--label3); }
.kb-keys { display: grid; gap: 11px; }
.kb-row { display: flex; gap: 6px; justify-content: center; }
.key {
flex: 1;
max-inline-size: 64px;
block-size: 42px;
border: 0;
border-radius: 5px;
background: var(--key);
color: var(--key-ink);
box-shadow: 0 1px 0 rgb(0 0 0 / 0.3);
font-size: 22px;
cursor: pointer;
}
.key.fn { background: var(--key2); font-size: 15px; flex: 1.4; max-inline-size: 100px; }
.key.space { flex: 5; max-inline-size: none; font-size: 15px; }
.key.go { flex: 2; max-inline-size: 120px; background: var(--tint); color: #fff; font-size: 15px; }
.key:active { background: var(--key2); }
.kb-row.mid { padding-inline: 5%; }
@media (min-width: 760px) { .key { max-inline-size: 84px; block-size: 50px; } .kb-keys { gap: 8px; } }
/* ---------- constraints inspector (doubles as X-ray) ---------- */
.inspector {
block-size: 52%;
padding: 0;
border: 0;
border-radius: 13px 13px 0 0;
background: var(--bar);
backdrop-filter: saturate(180%) blur(24px);
color: var(--label);
box-shadow: 0 -8px 40px rgb(0 0 0 / 0.25);
overflow: auto;
overscroll-behavior: contain;
}
.inspector[data-⚓️-side] {
/* regular: a popover hanging off the bar button */
inline-size: 400px;
block-size: auto;
max-block-size: min(560px, 70dvh);
margin-block-start: 8px;
border-radius: 13px;
box-shadow: 0 0 0 0.5px var(--sep), 0 20px 60px rgb(0 0 0 / 0.3);
}
.insp-head {
position: sticky;
inset-block-start: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 8px 6px 16px;
background: color-mix(in srgb, var(--bg) 92%, transparent);
backdrop-filter: blur(20px);
}
.insp-head h2 { margin: 0; font-size: 17px; font-weight: 600; }
.insp-note { margin: 0 16px 8px; font-size: 13px; color: var(--label2); }
.insp-list { list-style: none; margin: 0; padding: 0 12px 16px; display: grid; gap: 8px; }
.insp-list li {
padding: 8px 12px;
border-radius: 10px;
background: var(--cell);
cursor: default;
}
.insp-list li:hover, .insp-list li:focus-visible { box-shadow: inset 0 0 0 2px var(--tint); outline: 0; }
.insp-list b { display: block; font-size: 13px; font-weight: 600; margin-block-end: 2px; }
.insp-list code {
display: block;
font: 12px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
letter-spacing: 0;
color: var(--label2);
white-space: pre-wrap;
}
.insp-list code.active { color: var(--tint); }
/* X-ray: named anchors get dashed outlines, the hovered one a solid highlight */
.screen[data-xray] [data-⚓️-name] { outline: 1px dashed color-mix(in srgb, var(--tint) 70%, transparent); outline-offset: -1px; }
.screen[data-xray] .guide { outline-width: 1px; outline-offset: 0; }
.screen[data-xray] .kbd-guide { background: color-mix(in srgb, var(--tint) 14%, transparent); z-index: 7; }
.screen[data-xray] .cta::after {
content: "side: " attr(data-⚓️-side);
position: absolute;
inset-block-start: -22px;
inset-inline-end: 0;
padding: 1px 6px;
border-radius: 5px;
font: 11px/16px ui-monospace, monospace;
letter-spacing: 0;
background: var(--tint);
color: #fff;
}
.screen [data-hl] {
outline: 2px solid var(--tint) !important;
outline-offset: 1px !important;
box-shadow: 0 0 0 6px color-mix(in srgb, var(--tint) 22%, transparent);
z-index: 8;
}
@media (prefers-reduced-motion: reduce) {
.kbd-guide, .keyboard, .switch, .switch::after, #fName, #hint { transition: none; }
}// State only: mode, size class, keyboard visibility, priority, inspector.
// Every geometry change is an attribute swap (activating a different
// constraint); the browser resolves where things go.
const A = (k) => `data-⚓️-${k}`;
const $ = (id) => document.getElementById(id);
const screen = $("scr");
const form = $("sv");
const inputs = [...form.querySelectorAll(".field input")];
const REGULAR = matchMedia("(min-width: 760px)");
const REDUCED = matchMedia("(prefers-reduced-motion: reduce)");
const state = {
mode: "signup",
regular: REGULAR.matches,
hardware: REGULAR.matches, // regular width assumes a hardware keyboard
kb: false,
valueWins: false,
inspector: false,
};
// replace one set of constraint attributes with another
function constrain(el, keys, values) {
for (const k of keys) el.removeAttribute(A(k));
for (const [k, v] of Object.entries(values)) el.setAttribute(A(k), v);
}
function applyConstraints() {
const { mode, regular, valueWins } = state;
const signup = mode === "signup";
screen.dataset.mode = mode;
// login hides the name field + hint, so their dependants re-anchor
$("fEmail").setAttribute(A("below"), signup ? "--name" : "--hdrp");
constrain($("hdra"), ["below", "align-leading", "align-top", "after"], regular
? { "align-top": "--hdrp", after: "--name" }
: { below: signup ? "--hint" : "--pw", "align-leading": "--margins" });
$("summary").setAttribute(A("sync-width"), regular ? "--name" : "--margins");
constrain($("cta"), ["pin-x", "align-leading", "sync-width"], regular
? { "align-leading": "--name", "sync-width": "--name" }
: { "pin-x": "" });
constrain($("inspector"), ["side", "align-leading", "align-bottom", "stretch-x", "to"], regular
? { to: "--cbtn", side: "below", "align-leading": "" }
: { to: "--scr", "align-bottom": "", "stretch-x": "" });
// compression resistance: which of label/detail sizes to fit, which truncates
for (const row of form.querySelectorAll(".row[data-⚓️-scope]")) {
const lbl = row.querySelector(".lbl");
const val = row.querySelector(".detail");
constrain(val, ["after"], valueWins ? {} : { after: "--lbl" });
constrain(lbl, ["pin-leading", "pin-y", "before"], valueWins
? { "pin-leading": "", "pin-y": "", before: "--val" } : {});
}
// the segmented control's selection pill fills the chosen segment
$("pill").setAttribute(A("fill"), signup ? "--seg-a" : "--seg-b");
$("segA").setAttribute("aria-checked", signup);
$("segB").setAttribute("aria-checked", !signup);
for (const el of [$("fName"), $("hint")]) el.inert = !signup;
const title = signup ? "Create account" : "Welcome back";
$("title").textContent = title;
$("navTitle").textContent = title;
$("sub").textContent = signup
? "One login for every dashboard. Tap a field to raise the keyboard."
: "Log in with your email. The name field is hidden, so email re-anchors to the header.";
$("cta").textContent = signup ? "Create account" : "Log in";
$("fPw").querySelector("input").autocomplete = signup ? "new-password" : "current-password";
const soft = !state.hardware;
for (const i of inputs) i.inputMode = soft ? "none" : "";
$("kbtn").setAttribute("aria-pressed", soft && regular);
renderInspector();
}
// ---------- simulated keyboard ----------
const LAYERS = {
abc: ["qwertyuiop", "asdfghjkl", "⇧zxcvbnm⌫", ["123", "space", "@", ".", "return"]],
num: ["1234567890", "-/:;()$&\"", "#_+=!?'⌫", ["ABC", "space", "@", ".", "return"]],
};
let layer = "abc";
let shift = false;
function buildKeys() {
const keys = $("kbKeys");
keys.replaceChildren();
LAYERS[layer].forEach((row, r) => {
const div = document.createElement("div");
div.className = `kb-row${r === 1 && layer === "abc" ? " mid" : ""}`;
for (const k of Array.isArray(row) ? row : [...row]) {
const b = document.createElement("button");
b.type = "button";
b.tabIndex = -1;
b.className = "key";
b.dataset.key = k;
if (["⇧", "⌫", "123", "ABC"].includes(k)) b.classList.add("fn");
if (k === "space") b.classList.add("space");
if (k === "return") {
b.classList.add("go");
b.textContent = document.activeElement === inputs.at(-1) ? "go" : "next";
} else {
b.textContent = k.length === 1 && shift ? k.toUpperCase() : k;
}
div.append(b);
}
keys.append(div);
});
}
function suggestions() {
const f = document.activeElement?.name;
const list = f === "email" ? ["you@example.com", "@icloud.com", "@gmail.com"]
: f === "name" ? ["Dork", "Adam", "Nerdy"]
: f === "password" ? ["Strong Password", "hunter2", "Hide"] : ["", "", "Hide"];
$("kbSuggest").replaceChildren(...list.map((s) => {
const b = document.createElement("button");
b.type = "button";
b.tabIndex = -1;
b.textContent = s;
b.dataset.suggest = s;
return b;
}));
}
function type(text) {
const el = document.activeElement;
if (!inputs.includes(el)) return;
const s = el.selectionStart ?? el.value.length;
const e = el.selectionEnd ?? el.value.length;
if (text === null) {
if (s === e && s > 0) el.setRangeText("", s - 1, s, "end");
else el.setRangeText("", s, e, "end");
} else {
el.setRangeText(text, s, e, "end");
}
el.dispatchEvent(new Event("input", { bubbles: true }));
}
const kb = $("kb");
// keep focus in the field while tapping keys
kb.addEventListener("pointerdown", (e) => e.preventDefault());
kb.addEventListener("click", (e) => {
const b = e.target.closest("button");
if (!b) return;
const el = document.activeElement;
if (b.dataset.suggest !== undefined) {
const s = b.dataset.suggest === "Strong Password" ? "xT9q-vB2m-Pk4R" : b.dataset.suggest;
if (!inputs.includes(el) || !s || s === "Hide") return el.blur();
el.value = s.startsWith("@") ? el.value.replace(/@.*$/, "") + s : s;
el.dispatchEvent(new Event("input", { bubbles: true }));
return;
}
const k = b.dataset.key;
if (k === "⇧") { shift = !shift; return buildKeys(); }
if (k === "123" || k === "ABC") { layer = layer === "abc" ? "num" : "abc"; return buildKeys(); }
if (k === "⌫") return type(null);
if (k === "space") return type(" ");
if (k === "return") {
const i = inputs.indexOf(el);
const next = inputs.slice(i + 1).find((n) => !n.closest("[inert]"));
return next ? next.focus() : form.requestSubmit();
}
type(shift ? k.toUpperCase() : k);
if (shift) { shift = false; buildKeys(); }
});
function setKeyboard(show) {
show = show && !state.hardware;
if (state.kb === show) return;
state.kb = show;
screen.dataset.kb = show ? "shown" : "hidden";
renderInspector();
if (REDUCED.matches) requestAnimationFrame(revealFocused);
}
// once the guide has settled, keep the focused field visible (UIKit adjusts
// the scroll view's content offset the same way)
function revealFocused() {
const el = document.activeElement;
if (inputs.includes(el)) el.closest(".field").scrollIntoView({ block: "nearest", behavior: REDUCED.matches ? "auto" : "smooth" });
}
$("kbd").addEventListener("transitionend", (e) => {
if (e.propertyName === "block-size" || e.propertyName === "height") revealFocused();
});
form.addEventListener("focusin", (e) => {
if (!inputs.includes(e.target)) return;
buildKeys();
suggestions();
if (state.kb) revealFocused();
setKeyboard(true);
});
// tapping an already-focused field brings a dismissed keyboard back
form.addEventListener("click", (e) => { if (inputs.includes(e.target)) setKeyboard(true); });
form.addEventListener("focusout", (e) => {
if (!inputs.includes(e.relatedTarget)) setKeyboard(false);
});
// ---------- state wiring ----------
for (const b of [$("segA"), $("segB")]) {
b.addEventListener("click", () => { state.mode = b.dataset.mode; applyConstraints(); });
}
$("seg").addEventListener("keydown", (e) => {
if (!["ArrowLeft", "ArrowRight"].includes(e.key)) return;
state.mode = state.mode === "signup" ? "login" : "signup";
applyConstraints();
$(state.mode === "signup" ? "segA" : "segB").focus();
});
$("prio").addEventListener("change", (e) => { state.valueWins = e.target.checked; applyConstraints(); });
$("kbtn").addEventListener("click", () => { state.hardware = !state.hardware; applyConstraints(); });
const mirror = { name: "dName", email: "dEmail" };
form.addEventListener("input", (e) => {
const id = mirror[e.target.name];
if (id) $(id).textContent = e.target.value || "Not set";
});
form.addEventListener("submit", (e) => {
e.preventDefault();
document.activeElement?.blur();
setKeyboard(false);
const cta = $("cta");
cta.dataset.done = "";
cta.textContent = state.mode === "signup" ? "✓ Account created" : "✓ Logged in";
setTimeout(() => { delete cta.dataset.done; applyConstraints(); }, 1600);
});
REGULAR.addEventListener("change", () => {
state.regular = REGULAR.matches;
state.hardware = REGULAR.matches;
setKeyboard(false);
applyConstraints();
});
// ---------- constraints inspector ----------
const insp = $("inspector");
function spec() {
const { regular: R, kb: K, valueWins: V } = state;
const S = state.mode === "signup";
const w = R ? "0.5 × margins.width − 18" : "1.0 × margins.width";
return [
["kbd", "keyboardLayoutGuide", K
? ["guide.top = keyboard.top (keyboard shown)", "guide.leading/trailing/bottom = superview"]
: ["guide.top = safeArea.bottom (keyboard hidden)", "guide.leading/trailing/bottom = superview"]],
["cta", "primaryButton", [
"button.bottom = keyboardLayoutGuide.top − 8",
...(R ? ["button.leading = name.leading", "button.width = name.width"]
: ["button.leading = superview.leading + 20", "button.trailing = superview.trailing − 20"]),
"button.height = 50"]],
["sv", "scrollView", [
"scrollView.top = safeArea.top",
"scrollView.bottom = keyboardLayoutGuide.top",
"scrollView.leading/trailing = superview",
"contentInset = (top: 44, bottom: 96)"]],
["nav", "navigationBar", ["navBar.top = safeArea.top", "navBar.leading/trailing = superview", "navBar.height = 44"]],
["margins", "layoutMarginsGuide", [`margins.leading = superview.leading + ${R ? 40 : 20}`, `margins.trailing = superview.trailing − ${R ? 40 : 20}`]],
["title", "largeTitle", ["title.top = contentLayoutGuide.top + 48", "title.leading = margins.leading"]],
["sub", "subtitle", ["subtitle.top = title.bottom + 6", "subtitle.leading = title.leading", `subtitle.width = ${w}`]],
["seg", "segmentedControl", [
"segmented.top = subtitle.bottom + 20", "segmented.leading = margins.leading", `segmented.width = ${w}`,
"segment[0].width = segment[1].width (fillEqually)", `selection.edges = segment[${S ? 0 : 1}].edges`]],
["fName", "nameField", S
? ["name.top = profileHeader.bottom + 8", "name.leading = margins.leading", `name.width = ${w}`]
: ["name.isHidden = true", "name.leading = margins.leading", `name.width = ${w}`]],
["fEmail", "emailField", [
S ? "email.top = name.bottom + 8" : "email.top = profileHeader.bottom + 8 (re-anchored)",
"email.leading = name.leading", "email.width = name.width"]],
["fPw", "passwordField", ["password.top = email.bottom + 8", "password.leading = email.leading", "password.width = email.width"]],
["hdra", "previewHeader", R
? ["header.top = profileHeader.top", "header.leading = name.trailing + 36 + 16"]
: [S ? "header.top = hint.bottom + 28" : "header.top = password.bottom + 28 (hint hidden)", "header.leading = margins.leading + 16"]],
["summary", "previewTable", ["table.top = header.bottom + 6", "table.leading = header.leading − 16", R ? "table.width = name.width" : "table.width = margins.width"]],
["rowTz", "row: label ↔ detail", V
? ["detail.trailing = row.trailingMargin", "detail.width ≤ row.width − 104", "label.leading = row.leadingMargin",
"label.trailing = detail.leading − 8",
"detail hugging 251 compressionResistance 751", "label hugging 250 compressionResistance 750 ← truncates"]
: ["label.leading = row.leadingMargin", "detail.leading = label.trailing + 8", "detail.trailing = row.trailingMargin",
"label hugging 251 compressionResistance 751", "detail hugging 250 compressionResistance 750 ← truncates"]],
];
}
function renderInspector() {
if (!state.inspector) return;
const list = $("inspList");
list.replaceChildren(...spec().map(([id, name, lines]) => {
const li = document.createElement("li");
li.tabIndex = 0;
li.dataset.for = id;
const b = document.createElement("b");
b.textContent = name;
li.append(b, ...lines.map((l) => {
const c = document.createElement("code");
c.textContent = l;
if (/keyboardLayoutGuide.top|re-anchored|keyboard shown|← truncates/.test(l)) c.className = "active";
return c;
}));
return li;
}));
}
function highlight(id) {
for (const el of screen.querySelectorAll("[data-hl]")) el.removeAttribute("data-hl");
if (id) $(id)?.setAttribute("data-hl", "");
}
const list = $("inspList");
list.addEventListener("pointerover", (e) => highlight(e.target.closest("li")?.dataset.for));
list.addEventListener("pointerleave", () => highlight());
list.addEventListener("focusin", (e) => highlight(e.target.closest("li")?.dataset.for));
list.addEventListener("focusout", () => highlight());
function setInspector(open) {
state.inspector = open;
$("cbtn").setAttribute("aria-pressed", open);
if (open) { screen.dataset.xray = ""; renderInspector(); insp.showPopover(); }
else { delete screen.dataset.xray; insp.hidePopover(); highlight(); }
}
$("cbtn").addEventListener("click", () => setInspector(!state.inspector));
$("inspClose").addEventListener("click", () => setInspector(false));
insp.addEventListener("keydown", (e) => { if (e.key === "Escape") { setInspector(false); $("cbtn").focus(); } });
buildKeys();
suggestions();
applyConstraints();data-⚓️-abovedata-⚓️-afterdata-⚓️-align-bottomdata-⚓️-align-leadingdata-⚓️-belowdata-⚓️-filldata-⚓️-namedata-⚓️-pin-bottomdata-⚓️-pin-leadingdata-⚓️-pin-topdata-⚓️-pin-trailingdata-⚓️-pin-xdata-⚓️-pin-ydata-⚓️-rootdata-⚓️-scopedata-⚓️-sidedata-⚓️-stretch-xdata-⚓️-sync-widthdata-⚓️-todata-⚓️-transition
iOS adaptive dashboard: size classes, readable guide & priorities →