Warm Editorial — forms
Asymmetric two-column layout: fields on the left, a sticky summary on the right that updates as the form is filled. Inputs are soft-cornered rather than pills, because a pill text field is unreadable at length.
Paste this into Claude, Cursor or Copilot — or point the agent at
https://ui-deploy.com/r/warm-editorial/form-panel/prompt.md
# Booking Form Panel — Warm Editorial
Paste this whole file into Claude, Cursor or Copilot.
---
## What to build
An asymmetric two-column booking form: fields on the left in a
`minmax(0,1fr) 370px` grid, and on the right a **sticky summary panel** that
updates live as the form is filled.
Fields are grouped in `<fieldset>` blocks with a quiet `<legend>`. Below 900px the
summary moves above the form (`order:-1`) and stops being sticky.
---
## Tokens
```css
:root{
--bg2:#F6F3ED; --bg3:#E4DED2; --border:#DAD3C6; --border2:#C6BDAC;
--text:#22201D; --text2:#5C574F; --text3:#8C857A;
--accent:#A38A6A; --accent-text:#7D6644; --accent-s:rgba(163,138,106,.12);
--radius:28px; --radius-sm:12px; --radius-pill:999px;
--font-display:'Inter Tight',system-ui,sans-serif;
--font-body:'Inter',system-ui,sans-serif;
--ease:cubic-bezier(.22,.61,.36,1);
}
[data-theme="dark"]{
--bg2:#1E1A17; --bg3:#262119; --border:#2E2822; --border2:#40382F;
--text:#EDE7DD; --text2:#A79E92; --text3:#8A8177;
--accent:#B39B7C; --accent-text:#C0A888; --accent-s:rgba(179,155,124,.13);
}
```
---
## Rules — these are not preferences
1. **Inputs are soft-cornered (12px), not pills.** Everything else in this language
is a 999px pill; text fields are the deliberate exception. A pill text input is
unreadable the moment the value is longer than a word.
2. **Every field has a real `<label for>`.** A placeholder is not a label — it
disappears exactly when the user needs it.
3. **State "required" in words, not colour.** A red asterisk alone fails anyone who
cannot see the red.
4. **The focus ring is `border-color:var(--accent)` plus
`box-shadow:0 0 0 3px var(--accent-s)`** — and set `outline:none` only because a
visible ring is already being drawn. Never remove the ring without replacing it.
5. **Set `min-width:0` on grid children.** Without it a long value or a `<select>`
forces the column wider than the grid and the layout scrolls sideways. This is
the most common bug in this component.
6. **Summary values need `overflow-wrap:anywhere`.** A long email address will
otherwise push the panel out of the card.
7. **`position:sticky` comes off below 900px.** A sticky panel on a phone eats the
viewport.
8. Buttons stay pills, 52px. Icons inline SVG only, no emoji.
---
## Adapting it
- **Multi-step flow** — keep the summary panel and swap the fieldsets. The panel is
what makes a stepped form tolerable; do not drop it to save space.
- **Server validation** — render the error under the field in `--text2` with a
bronze left border, and set `aria-describedby` and `aria-invalid` on the input.
- **Do not format currency in the input handler.** Format server-side and write the
finished string in.
---
## Reference implementation
See `code.html` — 18 lines of dependency-free JS.
Paste this whole file into Claude, Cursor or Copilot.
An asymmetric two-column booking form: fields on the left in a minmax(0,1fr) 370px grid, and on the right a sticky summary panel that updates live as the form is filled.
Fields are grouped in <fieldset> blocks with a quiet <legend>. Below 900px the summary moves above the form (order:-1) and stops being sticky.
:root{
--bg2:#F6F3ED; --bg3:#E4DED2; --border:#DAD3C6; --border2:#C6BDAC;
--text:#22201D; --text2:#5C574F; --text3:#8C857A;
--accent:#A38A6A; --accent-text:#7D6644; --accent-s:rgba(163,138,106,.12);
--radius:28px; --radius-sm:12px; --radius-pill:999px;
--font-display:'Inter Tight',system-ui,sans-serif;
--font-body:'Inter',system-ui,sans-serif;
--ease:cubic-bezier(.22,.61,.36,1);
}
[data-theme="dark"]{
--bg2:#1E1A17; --bg3:#262119; --border:#2E2822; --border2:#40382F;
--text:#EDE7DD; --text2:#A79E92; --text3:#8A8177;
--accent:#B39B7C; --accent-text:#C0A888; --accent-s:rgba(179,155,124,.13);
}
<label for>. A placeholder is not a label — it disappears exactly when the user needs it.border-color:var(--accent) plus box-shadow:0 0 0 3px var(--accent-s)** — and set outline:none only because a visible ring is already being drawn. Never remove the ring without replacing it.min-width:0 on grid children. Without it a long value or a <select> forces the column wider than the grid and the layout scrolls sideways. This is the most common bug in this component.overflow-wrap:anywhere. A long email address will otherwise push the panel out of the card.position:sticky comes off below 900px. A sticky panel on a phone eats the viewport.what makes a stepped form tolerable; do not drop it to save space.
--text2 with a bronze left border, and set aria-describedby and aria-invalid on the input.
finished string in.
See code.html — 18 lines of dependency-free JS.
<!-- Warm Editorial — Booking form with a live summary panel
Requires tokens.css. -->
<section class="we-form-wrap">
<form class="we-form" novalidate>
<p class="we-form-eyebrow"><span aria-hidden="true"></span>Book a visit</p>
<h2 class="we-form-title">Tell us what you need.</h2>
<fieldset class="we-fs">
<legend class="we-legend">About you</legend>
<div class="we-grid-2">
<div class="we-field">
<label class="we-label" for="we-name">Full name <span class="we-req">required</span></label>
<input class="we-input" id="we-name" name="name" type="text" autocomplete="name" data-sum="name">
</div>
<div class="we-field">
<label class="we-label" for="we-email">Email <span class="we-req">required</span></label>
<input class="we-input" id="we-email" name="email" type="email" autocomplete="email" data-sum="email">
</div>
</div>
</fieldset>
<fieldset class="we-fs">
<legend class="we-legend">Your visit</legend>
<div class="we-grid-2">
<div class="we-field">
<label class="we-label" for="we-service">Service</label>
<select class="we-input" id="we-service" name="service" data-sum="service">
<option>Assessment</option>
<option selected>Recovery</option>
<option>Sustained</option>
</select>
</div>
<div class="we-field">
<label class="we-label" for="we-date">Preferred date</label>
<input class="we-input" id="we-date" name="date" type="date" data-sum="date">
</div>
</div>
<div class="we-field">
<label class="we-label" for="we-notes">Anything we should know</label>
<textarea class="we-input we-textarea" id="we-notes" name="notes" rows="4"
placeholder="Injuries, training load, what you are hoping to change."></textarea>
</div>
</fieldset>
<button class="we-submit" type="submit">
Request the booking
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14"/><path d="M13 6l6 6-6 6"/>
</svg>
</button>
<p class="we-form-foot">We reply within one working day. No card details at this stage.</p>
</form>
<aside class="we-summary" aria-label="Booking summary">
<div class="we-summary-in">
<p class="we-summary-label">Summary</p>
<dl class="we-summary-list">
<div><dt>Name</dt><dd data-out="name">—</dd></div>
<div><dt>Email</dt><dd data-out="email">—</dd></div>
<div><dt>Service</dt><dd data-out="service">Recovery</dd></div>
<div><dt>Date</dt><dd data-out="date">—</dd></div>
</dl>
<p class="we-summary-note">
Nothing is confirmed until we write back. Changing your mind afterwards costs
you nothing.
</p>
</div>
</aside>
</section>
<style>
.we-form-wrap{
display:grid;gap:28px;
grid-template-columns:minmax(0,1fr) 370px;
max-width:76rem;margin:0 auto;padding:0 20px;align-items:start;
}
.we-form-eyebrow{
display:flex;align-items:center;gap:7px;margin:0 0 16px;
font-family:var(--font-display);font-size:11.5px;font-weight:600;
letter-spacing:.16em;text-transform:uppercase;color:var(--accent-text);
}
.we-form-eyebrow span{width:5px;height:5px;border-radius:50%;background:var(--accent);flex:0 0 5px}
.we-form-title{
font-family:var(--font-display);font-weight:600;
font-size:clamp(28px,4vw,48px);line-height:1.03;letter-spacing:-.03em;
color:var(--text);margin:0 0 36px;
}
.we-fs{border:0;padding:0;margin:0 0 28px;min-width:0}
.we-legend{
padding:0;margin-bottom:16px;
font-family:var(--font-display);font-size:12.5px;font-weight:600;
letter-spacing:.01em;color:var(--text3);
}
.we-grid-2{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr))}
.we-field{display:flex;flex-direction:column;margin-bottom:16px;min-width:0}
.we-grid-2 .we-field{margin-bottom:0}
.we-label{
margin-bottom:8px;font-family:var(--font-body);font-size:13px;font-weight:500;
color:var(--text2);display:flex;align-items:center;gap:8px;
}
/* Required is stated in words. Colour alone is not a label. */
.we-req{
font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
color:var(--accent-text);
}
/* Inputs are soft-cornered, not pills. A pill text field is unreadable once the
value is longer than a word. */
.we-input{
width:100%;min-width:0;height:48px;padding:0 15px;
background:var(--bg3);border:1px solid var(--border2);
border-radius:var(--radius-sm);
font-family:var(--font-body);font-size:15px;color:var(--text);
transition:border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.we-input::placeholder{color:var(--text3)}
.we-input:focus{
outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-s);
}
.we-input:focus-visible{outline:none}
.we-textarea{height:auto;padding:13px 15px;line-height:1.65;resize:vertical}
.we-submit{
display:inline-flex;align-items:center;justify-content:center;gap:8px;
height:52px;padding:0 26px;border:0;border-radius:var(--radius-pill);
background:var(--text);color:var(--bg);
font-family:var(--font-body);font-size:14.5px;font-weight:500;cursor:pointer;
transition:opacity .2s var(--ease);
}
.we-submit svg{width:16px;height:16px}
.we-submit:hover{opacity:.85}
.we-submit:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.we-form-foot{margin:16px 0 0;font-size:12.5px;color:var(--text3)}
.we-summary{position:sticky;top:88px}
.we-summary-in{
background:var(--bg2);border:1px solid var(--border);
border-radius:var(--radius);padding:28px;
}
.we-summary-label{
margin:0 0 18px;font-family:var(--font-display);font-size:11.5px;font-weight:600;
letter-spacing:.16em;text-transform:uppercase;color:var(--accent-text);
}
.we-summary-list{margin:0 0 20px;display:grid;gap:14px}
.we-summary-list > div{
display:flex;justify-content:space-between;gap:16px;
padding-bottom:14px;border-bottom:1px solid var(--border);
}
.we-summary-list > div:last-child{border-bottom:0;padding-bottom:0}
.we-summary-list dt{font-size:13px;color:var(--text3);flex:0 0 auto}
.we-summary-list dd{
margin:0;font-size:14px;font-weight:500;color:var(--text);
text-align:right;min-width:0;overflow-wrap:anywhere;
}
.we-summary-note{margin:0;font-size:12.5px;line-height:1.6;color:var(--text3)}
@media (max-width:900px){
.we-form-wrap{grid-template-columns:minmax(0,1fr)}
.we-summary{position:static;order:-1}
}
@media (prefers-reduced-motion:reduce){.we-input,.we-submit{transition:none}}
</style>
<script>
(function () {
var form = document.querySelector('.we-form');
var panel = document.querySelector('.we-summary');
if (!form || !panel) return;
function sync(el) {
var out = panel.querySelector('[data-out="' + el.dataset.sum + '"]');
if (out) out.textContent = el.value.trim() || '—';
}
form.querySelectorAll('[data-sum]').forEach(function (el) {
el.addEventListener('input', function () { sync(el); });
el.addEventListener('change', function () { sync(el); });
});
})();
</script>
Needs tokens.css on the page.