10 / 18
All 18 in 18 CSS Forms
- 01Contact Form with Character Counter
- 02Registration Form with Password Strength
- 03Checkout Payment Form with Order Summary
- 04Shipping Address Form with Autocomplete
- 05Newsletter Signup Form, Inline to Stacked
- 06Support Ticket Form with File Attachments
- 07NPS Survey Form with Follow-Up Question
- 08Job Application Form with Resume Upload
- 09Profile Settings Form with Avatar Upload
- 10Restaurant Table Reservation Form
- 11Donation Form with Preset Amounts
- 12Insurance Quote Request Form
- 13Event RSVP Form with Guest Count
- 14Product Return and Refund Request Form
- 15Patient Intake Form with Consent Checkbox
- 16Mortgage Pre-Qualification Form
- 17Law Firm Consultation Request Form
- 18SaaS Demo Request Form
Restaurant Table Reservation Form
A fine-dining booking widget in candle-lit black and gold: a party-size stepper, a horizontally scrolling date strip built from radios with scroll-snap, a time-slot grid where fully-booked slots are genuinely disabled (and say so), seating-area cards with photography, an occasion selector, and a live sentence summary — “Table for 4 · Fri 9 Oct · 8:00 pm · Terrace” — that updates as you choose.
Build interactive UI like this from scratch: 20 HTML, CSS & JavaScript projects
ZTM · JavaScript Web Projects · 30‑day money-back guarantee · Affiliate link
The code
<div class="frm-10">
<button type="button" class="frm-10__theme" data-theme-toggle aria-label="Dark mode" aria-pressed="false" title="Switch to dark mode"><svg class="frm-10__moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></svg><svg class="frm-10__sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
<div class="frm-10__stage">
<div class="frm-10__card">
<figure class="frm-10__hero">
<img src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1000&h=500&fit=crop&auto=format&q=70" width="1000" height="500" alt="Warmly lit restaurant dining room with set tables" loading="lazy" decoding="async">
<figcaption><span>Maison Ardent</span><small>Modern French · Mayfair, London</small></figcaption>
</figure>
<div class="frm-10__body" role="form" aria-labelledby="frm-10-title">
<h2 id="frm-10-title">Reserve a table</h2>
<div class="frm-10__party">
<span id="frm-10-party-l">Guests</span>
<div class="frm-10__stepper" role="group" aria-labelledby="frm-10-party-l">
<button type="button" data-d="-1" aria-label="Fewer guests">−</button>
<output aria-live="polite" class="frm-10__n">2</output>
<button type="button" data-d="1" aria-label="More guests">+</button>
</div>
</div>
<fieldset class="frm-10__fs"><legend>Date</legend><div class="frm-10__dates"></div></fieldset>
<fieldset class="frm-10__fs"><legend>Time <span class="frm-10__avail"></span></legend><div class="frm-10__times"></div><p class="frm-10__large" hidden>For parties of 9 or more, please call <a href="tel:+442070000000">020 7000 0000</a>.</p></fieldset>
<fieldset class="frm-10__fs">
<legend>Seating</legend>
<div class="frm-10__areas">
<label><input type="radio" name="frm-10-area" value="Dining room" checked><img src="https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Dining room</span></label>
<label><input type="radio" name="frm-10-area" value="Terrace"><img src="https://images.unsplash.com/photo-1559339352-11d035aa65de?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Terrace</span></label>
<label><input type="radio" name="frm-10-area" value="Chef's counter"><img src="https://images.unsplash.com/photo-1552566626-52f8b828add9?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Chef's counter</span></label>
</div>
</fieldset>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-name">Name</label><input id="frm-10-name" autocomplete="name" required></div>
<div class="frm-10__field"><label for="frm-10-tel">Phone</label><input id="frm-10-tel" type="tel" autocomplete="tel" required></div>
</div>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-occ">Occasion</label><select id="frm-10-occ"><option>None</option><option>Birthday</option><option>Anniversary</option><option>Business</option><option>Date night</option></select></div>
<div class="frm-10__field"><label for="frm-10-req">Requests <span>Optional</span></label><input id="frm-10-req" placeholder="Allergies, high chair, quiet table…"></div>
</div>
<div class="frm-10__summary"><p role="status" class="frm-10__sum"></p><button type="button" class="frm-10__book">Confirm reservation</button></div>
</div>
</div>
</div>
</div><div class="frm-10">
<button type="button" class="frm-10__theme" data-theme-toggle aria-label="Dark mode" aria-pressed="false" title="Switch to dark mode"><svg class="frm-10__moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></svg><svg class="frm-10__sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
<div class="frm-10__stage">
<div class="frm-10__card">
<figure class="frm-10__hero">
<img src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1000&h=500&fit=crop&auto=format&q=70" width="1000" height="500" alt="Warmly lit restaurant dining room with set tables" loading="lazy" decoding="async">
<figcaption><span>Maison Ardent</span><small>Modern French · Mayfair, London</small></figcaption>
</figure>
<div class="frm-10__body" role="form" aria-labelledby="frm-10-title">
<h2 id="frm-10-title">Reserve a table</h2>
<div class="frm-10__party">
<span id="frm-10-party-l">Guests</span>
<div class="frm-10__stepper" role="group" aria-labelledby="frm-10-party-l">
<button type="button" data-d="-1" aria-label="Fewer guests">−</button>
<output aria-live="polite" class="frm-10__n">2</output>
<button type="button" data-d="1" aria-label="More guests">+</button>
</div>
</div>
<fieldset class="frm-10__fs"><legend>Date</legend><div class="frm-10__dates"></div></fieldset>
<fieldset class="frm-10__fs"><legend>Time <span class="frm-10__avail"></span></legend><div class="frm-10__times"></div><p class="frm-10__large" hidden>For parties of 9 or more, please call <a href="tel:+442070000000">020 7000 0000</a>.</p></fieldset>
<fieldset class="frm-10__fs">
<legend>Seating</legend>
<div class="frm-10__areas">
<label><input type="radio" name="frm-10-area" value="Dining room" checked><img src="https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Dining room</span></label>
<label><input type="radio" name="frm-10-area" value="Terrace"><img src="https://images.unsplash.com/photo-1559339352-11d035aa65de?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Terrace</span></label>
<label><input type="radio" name="frm-10-area" value="Chef's counter"><img src="https://images.unsplash.com/photo-1552566626-52f8b828add9?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Chef's counter</span></label>
</div>
</fieldset>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-name">Name</label><input id="frm-10-name" autocomplete="name" required></div>
<div class="frm-10__field"><label for="frm-10-tel">Phone</label><input id="frm-10-tel" type="tel" autocomplete="tel" required></div>
</div>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-occ">Occasion</label><select id="frm-10-occ"><option>None</option><option>Birthday</option><option>Anniversary</option><option>Business</option><option>Date night</option></select></div>
<div class="frm-10__field"><label for="frm-10-req">Requests <span>Optional</span></label><input id="frm-10-req" placeholder="Allergies, high chair, quiet table…"></div>
</div>
<div class="frm-10__summary"><p role="status" class="frm-10__sum"></p><button type="button" class="frm-10__book">Confirm reservation</button></div>
</div>
</div>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Jost:wght@400;500;600&display=swap');
.frm-10 {
--bg: #0d0b09;
--card: #15120f;
--raised: #1d1915;
--ink: #f3ebdf;
--muted: #a69a89;
--line: #2e2821;
--gold: #d4a95a;
--gold-2: #f0d39a;
--on-gold: #1a1206;
--serif: "Cormorant Garamond",ui-serif,Georgia,serif;
--sans: "Jost",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
--ease: cubic-bezier(.16,1,.3,1);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: radial-gradient(50rem 30rem at 50% 0%, #2a1e10, transparent 70%), var(--bg);
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.frm-10[data-theme="light"] {
--bg: #f4eee4;
--card: #fffaf2;
--raised: #f6efe3;
--ink: #2a2017;
--muted: #7b6c5b;
--line: #e5d9c6;
--gold: #9a7232;
--gold-2: #b88a3e;
--on-gold: #fff;
background: var(--bg);
}
.frm-10,
.frm-10 *,
.frm-10 *::before,
.frm-10 *::after {
box-sizing: border-box;
}
.frm-10__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,5vw,4rem);
}
.frm-10__card {
width: 100%;
max-width: 40rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: 1.5rem;
overflow: hidden;
box-shadow: 0 40px 90px -40px rgba(0,0,0,.9), 0 0 0 1px color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__hero {
position: relative;
margin: 0;
height: 12rem;
background: #2a1e10;
}
.frm-10__hero img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
filter: saturate(.9) contrast(1.05);
}
.frm-10__hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(transparent 20%, var(--card));
}
.frm-10__hero figcaption {
position: absolute;
z-index: 1;
left: clamp(1.25rem,5vw,2.25rem);
bottom: .5rem;
display: grid;
}
.frm-10__hero figcaption span {
font: 500 clamp(2rem,6vw,2.75rem)/1 var(--serif);
letter-spacing: .01em;
color: var(--gold-2);
}
.frm-10__hero small {
font-size: .75rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--muted);
margin-top: .3rem;
}
.frm-10__body {
display: grid;
gap: 1.4rem;
padding: 1.25rem clamp(1.25rem,5vw,2.25rem) clamp(1.25rem,5vw,2.25rem);
}
.frm-10 h2 {
margin: 0;
font: italic 500 1.5rem var(--serif);
color: var(--muted);
}
.frm-10__party {
display: flex;
align-items: center;
justify-content: space-between;
padding: .75rem 1rem;
border: 1px solid var(--line);
border-radius: 1rem;
background: var(--raised);
}
.frm-10__party > span,
.frm-10 legend,
.frm-10 label {
font-size: .75rem;
font-weight: 500;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__stepper {
display: flex;
align-items: center;
gap: .4rem;
}
.frm-10__stepper button {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
border: 1px solid var(--line);
background: transparent;
color: var(--gold-2);
font-size: 1.25rem;
cursor: pointer;
transition: background .2s, border-color .2s;
}
.frm-10__stepper button:hover:not(:disabled) {
border-color: var(--gold);
background: color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__stepper button:disabled {
opacity: .3;
cursor: not-allowed;
}
.frm-10__stepper button:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.frm-10__n {
min-width: 2.5rem;
text-align: center;
font: 600 1.75rem var(--serif);
font-variant-numeric: lining-nums;
}
.frm-10__fs {
border: 0;
margin: 0;
padding: 0;
min-width: 0;
display: grid;
gap: .7rem;
}
.frm-10__fs legend {
padding: 0;
margin-bottom: .7rem;
display: flex;
gap: .6rem;
align-items: baseline;
}
.frm-10__avail {
letter-spacing: .04em;
text-transform: none;
color: var(--gold);
font-size: .8rem;
}
.frm-10__dates {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 4.25rem;
gap: .5rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
padding: .2rem;
margin: -.2rem;
-webkit-mask: linear-gradient(90deg,#000 85%,transparent);
mask: linear-gradient(90deg,#000 85%,transparent);
}
.frm-10__dates::-webkit-scrollbar {
display: none;
}
.frm-10__dates label,
.frm-10__times label,
.frm-10__areas label {
position: relative;
letter-spacing: normal;
text-transform: none;
}
.frm-10__dates label {
scroll-snap-align: start;
}
.frm-10 :is(.frm-10__dates,.frm-10__times,.frm-10__areas) input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
z-index: 1;
}
.frm-10__dates span {
display: grid;
justify-items: center;
gap: .1rem;
padding: .7rem 0;
border-radius: 1rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
transition: all .25s var(--ease);
}
.frm-10__dates small {
font-size: .66rem;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__dates b {
font: 600 1.6rem/1 var(--serif);
}
.frm-10__dates input:checked + span {
background: linear-gradient(160deg,var(--gold-2),var(--gold));
color: var(--on-gold);
border-color: transparent;
box-shadow: 0 10px 24px -12px var(--gold);
}
.frm-10__dates input:checked + span small {
color: inherit;
opacity: .8;
}
.frm-10 :is(.frm-10__dates,.frm-10__times) input:focus-visible + span {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
.frm-10__times {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(5.25rem,1fr));
gap: .45rem;
}
.frm-10__times span {
display: grid;
place-items: center;
min-height: 2.75rem;
border-radius: .7rem;
border: 1px solid var(--line);
color: var(--ink);
font-size: .92rem;
font-variant-numeric: tabular-nums;
transition: all .2s;
}
.frm-10__times label:hover input:not(:disabled) + span {
border-color: var(--gold);
}
.frm-10__times input:checked + span {
background: color-mix(in oklab,var(--gold) 20%,transparent);
border-color: var(--gold);
color: var(--gold-2);
font-weight: 600;
}
.frm-10__times input:disabled {
cursor: not-allowed;
}
.frm-10__times input:disabled + span {
text-decoration: line-through;
opacity: .35;
}
.frm-10__times span {
animation: frm-10-in .4s var(--ease) both;
animation-delay: calc(var(--k,0) * 25ms);
}
@keyframes frm-10-in {
from {
opacity: 0;
translate: 0 6px;
}
}
.frm-10__large {
margin: 0;
color: var(--muted);
font-size: .9rem;
}
.frm-10__large a {
color: var(--gold-2);
}
.frm-10 a {
color: var(--gold-2);
}
.frm-10 a:hover {
color: var(--gold);
}
.frm-10__areas {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: .6rem;
}
.frm-10__areas label {
display: grid;
border-radius: 1rem;
overflow: hidden;
border: 1px solid var(--line);
background: var(--raised);
transition: border-color .25s, box-shadow .25s;
}
.frm-10__areas img {
width: 100%;
aspect-ratio: 3/2;
height: auto;
object-fit: cover;
display: block;
background: #2a1e10;
filter: grayscale(.6) brightness(.7);
transition: filter .4s var(--ease), scale .6s var(--ease);
}
.frm-10__areas span {
padding: .55rem .7rem;
font-size: .85rem;
color: var(--ink);
}
.frm-10__areas label:hover img {
scale: 1.05;
}
.frm-10__areas label:has(:checked) {
border-color: var(--gold);
box-shadow: 0 0 0 1px var(--gold);
}
.frm-10__areas label:has(:checked) img {
filter: none;
}
.frm-10__areas label:has(:focus-visible) {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
@media (max-width: 30rem) {
.frm-10__areas {
grid-template-columns: 1fr;
}
.frm-10__areas label {
grid-template-columns: 5rem 1fr;
align-items: center;
}
}
.frm-10__grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit,minmax(min(100%,13rem),1fr));
}
.frm-10__field {
display: grid;
gap: .45rem;
}
.frm-10__field label span {
text-transform: none;
letter-spacing: 0;
opacity: .7;
}
.frm-10__field input,
.frm-10__field select {
width: 100%;
min-height: 2.9rem;
padding: .6rem .9rem;
border-radius: .7rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
font: inherit;
font-size: .95rem;
}
.frm-10__field :is(input,select):focus-visible {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px color-mix(in oklab,var(--gold) 22%,transparent);
}
.frm-10__field input:user-invalid {
border-color: #e57373;
}
.frm-10__summary {
display: grid;
gap: .8rem;
padding-top: 1.2rem;
border-top: 1px solid var(--line);
}
.frm-10__sum {
margin: 0;
font: 500 1.25rem/1.3 var(--serif);
color: var(--gold-2);
text-align: center;
text-wrap: balance;
}
.frm-10__book {
min-height: 3.3rem;
border: 0;
border-radius: 999px;
cursor: pointer;
font: inherit;
font-size: .82rem;
font-weight: 600;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--on-gold);
background: linear-gradient(110deg,var(--gold),var(--gold-2),var(--gold));
background-size: 200% 100%;
transition: background-position .8s var(--ease), translate .2s var(--ease);
}
.frm-10__book:hover {
background-position: 100% 0;
translate: 0 -2px;
}
.frm-10__book:focus-visible {
outline: 2px solid var(--gold-2);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.frm-10 * {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.frm-10 :is(.frm-10__dates,.frm-10__times) input:checked + span {
background: Highlight;
color: HighlightText;
}
.frm-10__areas label:has(:checked) {
outline: 2px solid Highlight;
}
.frm-10__book {
border: 1px solid ButtonText;
}
}
/* Theme toggle — top-right of the demo. Ink-relative colours, so it suits light and dark. */
.frm-10 {
position: relative;
padding-top: 72px;
box-sizing: border-box;
}
.frm-10__theme {
position: absolute;
top: 16px;
right: 16px;
z-index: 20;
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background-color .2s, transform .2s;
}
.frm-10__theme:hover {
background: color-mix(in srgb, currentColor 14%, transparent);
}
.frm-10__theme:active {
transform: scale(.94);
}
.frm-10__theme:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.frm-10__theme svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.frm-10__theme .frm-10__sun,
.frm-10__theme[aria-pressed="true"] .frm-10__moon {
display: none;
}
.frm-10__theme[aria-pressed="true"] .frm-10__sun {
display: block;
}
@media (prefers-reduced-motion: reduce) {
.frm-10__theme {
transition: none;
}
}
@media (forced-colors: active) {
.frm-10__theme {
border-color: ButtonText;
background: ButtonFace;
color: ButtonText;
}
}@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Jost:wght@400;500;600&display=swap');
.frm-10 {
--bg: #0d0b09;
--card: #15120f;
--raised: #1d1915;
--ink: #f3ebdf;
--muted: #a69a89;
--line: #2e2821;
--gold: #d4a95a;
--gold-2: #f0d39a;
--on-gold: #1a1206;
--serif: "Cormorant Garamond",ui-serif,Georgia,serif;
--sans: "Jost",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
--ease: cubic-bezier(.16,1,.3,1);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: radial-gradient(50rem 30rem at 50% 0%, #2a1e10, transparent 70%), var(--bg);
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.frm-10[data-theme="light"] {
--bg: #f4eee4;
--card: #fffaf2;
--raised: #f6efe3;
--ink: #2a2017;
--muted: #7b6c5b;
--line: #e5d9c6;
--gold: #9a7232;
--gold-2: #b88a3e;
--on-gold: #fff;
background: var(--bg);
}
.frm-10,
.frm-10 *,
.frm-10 *::before,
.frm-10 *::after {
box-sizing: border-box;
}
.frm-10__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,5vw,4rem);
}
.frm-10__card {
width: 100%;
max-width: 40rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: 1.5rem;
overflow: hidden;
box-shadow: 0 40px 90px -40px rgba(0,0,0,.9), 0 0 0 1px color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__hero {
position: relative;
margin: 0;
height: 12rem;
background: #2a1e10;
}
.frm-10__hero img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
filter: saturate(.9) contrast(1.05);
}
.frm-10__hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(transparent 20%, var(--card));
}
.frm-10__hero figcaption {
position: absolute;
z-index: 1;
left: clamp(1.25rem,5vw,2.25rem);
bottom: .5rem;
display: grid;
}
.frm-10__hero figcaption span {
font: 500 clamp(2rem,6vw,2.75rem)/1 var(--serif);
letter-spacing: .01em;
color: var(--gold-2);
}
.frm-10__hero small {
font-size: .75rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--muted);
margin-top: .3rem;
}
.frm-10__body {
display: grid;
gap: 1.4rem;
padding: 1.25rem clamp(1.25rem,5vw,2.25rem) clamp(1.25rem,5vw,2.25rem);
}
.frm-10 h2 {
margin: 0;
font: italic 500 1.5rem var(--serif);
color: var(--muted);
}
.frm-10__party {
display: flex;
align-items: center;
justify-content: space-between;
padding: .75rem 1rem;
border: 1px solid var(--line);
border-radius: 1rem;
background: var(--raised);
}
.frm-10__party > span,
.frm-10 legend,
.frm-10 label {
font-size: .75rem;
font-weight: 500;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__stepper {
display: flex;
align-items: center;
gap: .4rem;
}
.frm-10__stepper button {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
border: 1px solid var(--line);
background: transparent;
color: var(--gold-2);
font-size: 1.25rem;
cursor: pointer;
transition: background .2s, border-color .2s;
}
.frm-10__stepper button:hover:not(:disabled) {
border-color: var(--gold);
background: color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__stepper button:disabled {
opacity: .3;
cursor: not-allowed;
}
.frm-10__stepper button:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.frm-10__n {
min-width: 2.5rem;
text-align: center;
font: 600 1.75rem var(--serif);
font-variant-numeric: lining-nums;
}
.frm-10__fs {
border: 0;
margin: 0;
padding: 0;
min-width: 0;
display: grid;
gap: .7rem;
}
.frm-10__fs legend {
padding: 0;
margin-bottom: .7rem;
display: flex;
gap: .6rem;
align-items: baseline;
}
.frm-10__avail {
letter-spacing: .04em;
text-transform: none;
color: var(--gold);
font-size: .8rem;
}
.frm-10__dates {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 4.25rem;
gap: .5rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
padding: .2rem;
margin: -.2rem;
-webkit-mask: linear-gradient(90deg,#000 85%,transparent);
mask: linear-gradient(90deg,#000 85%,transparent);
}
.frm-10__dates::-webkit-scrollbar {
display: none;
}
.frm-10__dates label,
.frm-10__times label,
.frm-10__areas label {
position: relative;
letter-spacing: normal;
text-transform: none;
}
.frm-10__dates label {
scroll-snap-align: start;
}
.frm-10 :is(.frm-10__dates,.frm-10__times,.frm-10__areas) input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
z-index: 1;
}
.frm-10__dates span {
display: grid;
justify-items: center;
gap: .1rem;
padding: .7rem 0;
border-radius: 1rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
transition: all .25s var(--ease);
}
.frm-10__dates small {
font-size: .66rem;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__dates b {
font: 600 1.6rem/1 var(--serif);
}
.frm-10__dates input:checked + span {
background: linear-gradient(160deg,var(--gold-2),var(--gold));
color: var(--on-gold);
border-color: transparent;
box-shadow: 0 10px 24px -12px var(--gold);
}
.frm-10__dates input:checked + span small {
color: inherit;
opacity: .8;
}
.frm-10 :is(.frm-10__dates,.frm-10__times) input:focus-visible + span {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
.frm-10__times {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(5.25rem,1fr));
gap: .45rem;
}
.frm-10__times span {
display: grid;
place-items: center;
min-height: 2.75rem;
border-radius: .7rem;
border: 1px solid var(--line);
color: var(--ink);
font-size: .92rem;
font-variant-numeric: tabular-nums;
transition: all .2s;
}
.frm-10__times label:hover input:not(:disabled) + span {
border-color: var(--gold);
}
.frm-10__times input:checked + span {
background: color-mix(in oklab,var(--gold) 20%,transparent);
border-color: var(--gold);
color: var(--gold-2);
font-weight: 600;
}
.frm-10__times input:disabled {
cursor: not-allowed;
}
.frm-10__times input:disabled + span {
text-decoration: line-through;
opacity: .35;
}
.frm-10__times span {
animation: frm-10-in .4s var(--ease) both;
animation-delay: calc(var(--k,0) * 25ms);
}
@keyframes frm-10-in {
from {
opacity: 0;
translate: 0 6px;
}
}
.frm-10__large {
margin: 0;
color: var(--muted);
font-size: .9rem;
}
.frm-10__large a {
color: var(--gold-2);
}
.frm-10 a {
color: var(--gold-2);
}
.frm-10 a:hover {
color: var(--gold);
}
.frm-10__areas {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: .6rem;
}
.frm-10__areas label {
display: grid;
border-radius: 1rem;
overflow: hidden;
border: 1px solid var(--line);
background: var(--raised);
transition: border-color .25s, box-shadow .25s;
}
.frm-10__areas img {
width: 100%;
aspect-ratio: 3/2;
height: auto;
object-fit: cover;
display: block;
background: #2a1e10;
filter: grayscale(.6) brightness(.7);
transition: filter .4s var(--ease), scale .6s var(--ease);
}
.frm-10__areas span {
padding: .55rem .7rem;
font-size: .85rem;
color: var(--ink);
}
.frm-10__areas label:hover img {
scale: 1.05;
}
.frm-10__areas label:has(:checked) {
border-color: var(--gold);
box-shadow: 0 0 0 1px var(--gold);
}
.frm-10__areas label:has(:checked) img {
filter: none;
}
.frm-10__areas label:has(:focus-visible) {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
@media (max-width: 30rem) {
.frm-10__areas {
grid-template-columns: 1fr;
}
.frm-10__areas label {
grid-template-columns: 5rem 1fr;
align-items: center;
}
}
.frm-10__grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit,minmax(min(100%,13rem),1fr));
}
.frm-10__field {
display: grid;
gap: .45rem;
}
.frm-10__field label span {
text-transform: none;
letter-spacing: 0;
opacity: .7;
}
.frm-10__field input,
.frm-10__field select {
width: 100%;
min-height: 2.9rem;
padding: .6rem .9rem;
border-radius: .7rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
font: inherit;
font-size: .95rem;
}
.frm-10__field :is(input,select):focus-visible {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px color-mix(in oklab,var(--gold) 22%,transparent);
}
.frm-10__field input:user-invalid {
border-color: #e57373;
}
.frm-10__summary {
display: grid;
gap: .8rem;
padding-top: 1.2rem;
border-top: 1px solid var(--line);
}
.frm-10__sum {
margin: 0;
font: 500 1.25rem/1.3 var(--serif);
color: var(--gold-2);
text-align: center;
text-wrap: balance;
}
.frm-10__book {
min-height: 3.3rem;
border: 0;
border-radius: 999px;
cursor: pointer;
font: inherit;
font-size: .82rem;
font-weight: 600;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--on-gold);
background: linear-gradient(110deg,var(--gold),var(--gold-2),var(--gold));
background-size: 200% 100%;
transition: background-position .8s var(--ease), translate .2s var(--ease);
}
.frm-10__book:hover {
background-position: 100% 0;
translate: 0 -2px;
}
.frm-10__book:focus-visible {
outline: 2px solid var(--gold-2);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.frm-10 * {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.frm-10 :is(.frm-10__dates,.frm-10__times) input:checked + span {
background: Highlight;
color: HighlightText;
}
.frm-10__areas label:has(:checked) {
outline: 2px solid Highlight;
}
.frm-10__book {
border: 1px solid ButtonText;
}
}
/* Theme toggle — top-right of the demo. Ink-relative colours, so it suits light and dark. */
.frm-10 {
position: relative;
padding-top: 72px;
box-sizing: border-box;
}
.frm-10__theme {
position: absolute;
top: 16px;
right: 16px;
z-index: 20;
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background-color .2s, transform .2s;
}
.frm-10__theme:hover {
background: color-mix(in srgb, currentColor 14%, transparent);
}
.frm-10__theme:active {
transform: scale(.94);
}
.frm-10__theme:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.frm-10__theme svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.frm-10__theme .frm-10__sun,
.frm-10__theme[aria-pressed="true"] .frm-10__moon {
display: none;
}
.frm-10__theme[aria-pressed="true"] .frm-10__sun {
display: block;
}
@media (prefers-reduced-motion: reduce) {
.frm-10__theme {
transition: none;
}
}
@media (forced-colors: active) {
.frm-10__theme {
border-color: ButtonText;
background: ButtonFace;
color: ButtonText;
}
}// Theme toggle (top-right): flips data-theme on the root and mirrors outside changes.
(() => {
document.querySelectorAll('.frm-10').forEach((root) => {
const btn = root.querySelector(':scope > [data-theme-toggle]');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const mq = matchMedia('(prefers-color-scheme: dark)');
const isDark = () => (root.dataset.theme ? root.dataset.theme === 'dark' : true);
const sync = () => {
const dark = isDark();
btn.setAttribute('aria-pressed', String(dark));
btn.title = dark ? 'Switch to light mode' : 'Switch to dark mode';
};
btn.addEventListener('click', () => { root.dataset.theme = isDark() ? 'light' : 'dark'; sync(); });
mq.addEventListener('change', sync);
new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ['data-theme'] });
sync();
});
})();
(() => {
const root = document.querySelector('.frm-10');
if (!root) return;
const $ = (s) => root.querySelector(s);
const DAYS = 10, MAX = 12, SLOTS = ['17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00'];
let party = 2;
const wd = new Intl.DateTimeFormat('en-GB', { weekday: 'short' }), full = new Intl.DateTimeFormat('en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
const dates = $('.frm-10__dates'), times = $('.frm-10__times');
const days = [...Array(DAYS)].map((_, i) => { const d = new Date(); d.setDate(d.getDate() + i); return d; });
dates.innerHTML = days.map((d, i) => '<label><input type="radio" name="frm-10-date" value="' + i + '"' + (i === 0 ? ' checked' : '') + ' aria-label="' + full.format(d) + '"><span aria-hidden="true"><small>' + (i === 0 ? 'Today' : wd.format(d)) + '</small><b>' + d.getDate() + '</b></span></label>').join('');
const fmt12 = (t) => { let [h, m] = t.split(':').map(Number); const ap = h >= 12 ? 'pm' : 'am'; h = h % 12 || 12; return h + ':' + String(m).padStart(2, '0') + ' ' + ap; };
const isBooked = (dayIdx, slotIdx) => ((dayIdx * 7 + slotIdx * 3 + party * 5) % 10) < (party > 4 ? 6 : 3);
const renderTimes = () => {
const day = Number(root.querySelector('[name="frm-10-date"]:checked').value);
const prev = (root.querySelector('[name="frm-10-time"]:checked') || {}).value;
const large = party > 8;
$('.frm-10__large').hidden = !large;
times.hidden = large;
times.innerHTML = SLOTS.map((t, i) => { const b = isBooked(day, i); return '<label><input type="radio" name="frm-10-time" value="' + t + '"' + (b ? ' disabled' : '') + (t === prev && !b ? ' checked' : '') + ' aria-label="' + fmt12(t) + (b ? ', fully booked' : '') + '"><span style="--k:' + i + '" aria-hidden="true">' + fmt12(t) + '</span></label>'; }).join('');
const open = SLOTS.filter((_, i) => !isBooked(day, i)).length;
$('.frm-10__avail').textContent = large ? '' : open + ' of ' + SLOTS.length + ' available';
summary();
};
const summary = () => {
const d = days[Number(root.querySelector('[name="frm-10-date"]:checked').value)];
const t = root.querySelector('[name="frm-10-time"]:checked'), a = root.querySelector('[name="frm-10-area"]:checked').value;
$('.frm-10__sum').textContent = 'Table for ' + party + ' · ' + full.format(d) + ' · ' + (t ? fmt12(t.value) : 'choose a time') + ' · ' + a;
};
root.querySelectorAll('.frm-10__stepper button').forEach((b) => b.addEventListener('click', () => {
party = Math.max(1, Math.min(MAX, party + Number(b.dataset.d)));
$('.frm-10__n').textContent = party;
root.querySelector('[data-d="-1"]').disabled = party === 1; root.querySelector('[data-d="1"]').disabled = party === MAX;
renderTimes();
}));
dates.addEventListener('change', renderTimes);
root.addEventListener('change', (e) => { if (e.target.name === 'frm-10-time' || e.target.name === 'frm-10-area') summary(); });
$('.frm-10__book').addEventListener('click', () => {
if (!root.querySelector('[name="frm-10-time"]:checked')) { (times.querySelector('input:not(:disabled)') || $('.frm-10__book')).focus(); $('.frm-10__sum').textContent = 'Please choose a time.'; return; }
const bad = [...root.querySelectorAll('[required]')].find((f) => !f.checkValidity());
if (bad) { bad.reportValidity(); return; }
$('.frm-10__sum').textContent = 'Confirmed — ' + $('.frm-10__sum').textContent + '. A confirmation text is on its way.';
$('.frm-10__book').textContent = 'Reservation confirmed';
});
renderTimes();
})();// Theme toggle (top-right): flips data-theme on the root and mirrors outside changes.
(() => {
document.querySelectorAll('.frm-10').forEach((root) => {
const btn = root.querySelector(':scope > [data-theme-toggle]');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const mq = matchMedia('(prefers-color-scheme: dark)');
const isDark = () => (root.dataset.theme ? root.dataset.theme === 'dark' : true);
const sync = () => {
const dark = isDark();
btn.setAttribute('aria-pressed', String(dark));
btn.title = dark ? 'Switch to light mode' : 'Switch to dark mode';
};
btn.addEventListener('click', () => { root.dataset.theme = isDark() ? 'light' : 'dark'; sync(); });
mq.addEventListener('change', sync);
new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ['data-theme'] });
sync();
});
})();
(() => {
const root = document.querySelector('.frm-10');
if (!root) return;
const $ = (s) => root.querySelector(s);
const DAYS = 10, MAX = 12, SLOTS = ['17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00'];
let party = 2;
const wd = new Intl.DateTimeFormat('en-GB', { weekday: 'short' }), full = new Intl.DateTimeFormat('en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
const dates = $('.frm-10__dates'), times = $('.frm-10__times');
const days = [...Array(DAYS)].map((_, i) => { const d = new Date(); d.setDate(d.getDate() + i); return d; });
dates.innerHTML = days.map((d, i) => '<label><input type="radio" name="frm-10-date" value="' + i + '"' + (i === 0 ? ' checked' : '') + ' aria-label="' + full.format(d) + '"><span aria-hidden="true"><small>' + (i === 0 ? 'Today' : wd.format(d)) + '</small><b>' + d.getDate() + '</b></span></label>').join('');
const fmt12 = (t) => { let [h, m] = t.split(':').map(Number); const ap = h >= 12 ? 'pm' : 'am'; h = h % 12 || 12; return h + ':' + String(m).padStart(2, '0') + ' ' + ap; };
const isBooked = (dayIdx, slotIdx) => ((dayIdx * 7 + slotIdx * 3 + party * 5) % 10) < (party > 4 ? 6 : 3);
const renderTimes = () => {
const day = Number(root.querySelector('[name="frm-10-date"]:checked').value);
const prev = (root.querySelector('[name="frm-10-time"]:checked') || {}).value;
const large = party > 8;
$('.frm-10__large').hidden = !large;
times.hidden = large;
times.innerHTML = SLOTS.map((t, i) => { const b = isBooked(day, i); return '<label><input type="radio" name="frm-10-time" value="' + t + '"' + (b ? ' disabled' : '') + (t === prev && !b ? ' checked' : '') + ' aria-label="' + fmt12(t) + (b ? ', fully booked' : '') + '"><span style="--k:' + i + '" aria-hidden="true">' + fmt12(t) + '</span></label>'; }).join('');
const open = SLOTS.filter((_, i) => !isBooked(day, i)).length;
$('.frm-10__avail').textContent = large ? '' : open + ' of ' + SLOTS.length + ' available';
summary();
};
const summary = () => {
const d = days[Number(root.querySelector('[name="frm-10-date"]:checked').value)];
const t = root.querySelector('[name="frm-10-time"]:checked'), a = root.querySelector('[name="frm-10-area"]:checked').value;
$('.frm-10__sum').textContent = 'Table for ' + party + ' · ' + full.format(d) + ' · ' + (t ? fmt12(t.value) : 'choose a time') + ' · ' + a;
};
root.querySelectorAll('.frm-10__stepper button').forEach((b) => b.addEventListener('click', () => {
party = Math.max(1, Math.min(MAX, party + Number(b.dataset.d)));
$('.frm-10__n').textContent = party;
root.querySelector('[data-d="-1"]').disabled = party === 1; root.querySelector('[data-d="1"]').disabled = party === MAX;
renderTimes();
}));
dates.addEventListener('change', renderTimes);
root.addEventListener('change', (e) => { if (e.target.name === 'frm-10-time' || e.target.name === 'frm-10-area') summary(); });
$('.frm-10__book').addEventListener('click', () => {
if (!root.querySelector('[name="frm-10-time"]:checked')) { (times.querySelector('input:not(:disabled)') || $('.frm-10__book')).focus(); $('.frm-10__sum').textContent = 'Please choose a time.'; return; }
const bad = [...root.querySelectorAll('[required]')].find((f) => !f.checkValidity());
if (bad) { bad.reportValidity(); return; }
$('.frm-10__sum').textContent = 'Confirmed — ' + $('.frm-10__sum').textContent + '. A confirmation text is on its way.';
$('.frm-10__book').textContent = 'Reservation confirmed';
});
renderTimes();
})();Here's a working CSS Form from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Restaurant Table Reservation Form
Source: https://codefronts.com/components/css-forms/restaurant-table-reservation-form/
A fine-dining booking widget in candle-lit black and gold: a party-size stepper, a horizontally scrolling date strip built from radios with scroll-snap, a time-slot grid where fully-booked slots are genuinely disabled (and say so), seating-area cards with photography, an occasion selector, and a live sentence summary — “Table for 4 · Fri 9 Oct · 8:00 pm · Terrace” — that updates as you choose.
## HTML
```html
<div class="frm-10">
<button type="button" class="frm-10__theme" data-theme-toggle aria-label="Dark mode" aria-pressed="false" title="Switch to dark mode"><svg class="frm-10__moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></svg><svg class="frm-10__sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
<div class="frm-10__stage">
<div class="frm-10__card">
<figure class="frm-10__hero">
<img src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1000&h=500&fit=crop&auto=format&q=70" width="1000" height="500" alt="Warmly lit restaurant dining room with set tables" loading="lazy" decoding="async">
<figcaption><span>Maison Ardent</span><small>Modern French · Mayfair, London</small></figcaption>
</figure>
<div class="frm-10__body" role="form" aria-labelledby="frm-10-title">
<h2 id="frm-10-title">Reserve a table</h2>
<div class="frm-10__party">
<span id="frm-10-party-l">Guests</span>
<div class="frm-10__stepper" role="group" aria-labelledby="frm-10-party-l">
<button type="button" data-d="-1" aria-label="Fewer guests">−</button>
<output aria-live="polite" class="frm-10__n">2</output>
<button type="button" data-d="1" aria-label="More guests">+</button>
</div>
</div>
<fieldset class="frm-10__fs"><legend>Date</legend><div class="frm-10__dates"></div></fieldset>
<fieldset class="frm-10__fs"><legend>Time <span class="frm-10__avail"></span></legend><div class="frm-10__times"></div><p class="frm-10__large" hidden>For parties of 9 or more, please call <a href="tel:+442070000000">020 7000 0000</a>.</p></fieldset>
<fieldset class="frm-10__fs">
<legend>Seating</legend>
<div class="frm-10__areas">
<label><input type="radio" name="frm-10-area" value="Dining room" checked><img src="https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Dining room</span></label>
<label><input type="radio" name="frm-10-area" value="Terrace"><img src="https://images.unsplash.com/photo-1559339352-11d035aa65de?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Terrace</span></label>
<label><input type="radio" name="frm-10-area" value="Chef's counter"><img src="https://images.unsplash.com/photo-1552566626-52f8b828add9?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Chef's counter</span></label>
</div>
</fieldset>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-name">Name</label><input id="frm-10-name" autocomplete="name" required></div>
<div class="frm-10__field"><label for="frm-10-tel">Phone</label><input id="frm-10-tel" type="tel" autocomplete="tel" required></div>
</div>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-occ">Occasion</label><select id="frm-10-occ"><option>None</option><option>Birthday</option><option>Anniversary</option><option>Business</option><option>Date night</option></select></div>
<div class="frm-10__field"><label for="frm-10-req">Requests <span>Optional</span></label><input id="frm-10-req" placeholder="Allergies, high chair, quiet table…"></div>
</div>
<div class="frm-10__summary"><p role="status" class="frm-10__sum"></p><button type="button" class="frm-10__book">Confirm reservation</button></div>
</div>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Jost:wght@400;500;600&display=swap');
.frm-10 {
--bg: #0d0b09;
--card: #15120f;
--raised: #1d1915;
--ink: #f3ebdf;
--muted: #a69a89;
--line: #2e2821;
--gold: #d4a95a;
--gold-2: #f0d39a;
--on-gold: #1a1206;
--serif: "Cormorant Garamond",ui-serif,Georgia,serif;
--sans: "Jost",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
--ease: cubic-bezier(.16,1,.3,1);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: radial-gradient(50rem 30rem at 50% 0%, #2a1e10, transparent 70%), var(--bg);
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.frm-10[data-theme="light"] {
--bg: #f4eee4;
--card: #fffaf2;
--raised: #f6efe3;
--ink: #2a2017;
--muted: #7b6c5b;
--line: #e5d9c6;
--gold: #9a7232;
--gold-2: #b88a3e;
--on-gold: #fff;
background: var(--bg);
}
.frm-10,
.frm-10 *,
.frm-10 *::before,
.frm-10 *::after {
box-sizing: border-box;
}
.frm-10__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,5vw,4rem);
}
.frm-10__card {
width: 100%;
max-width: 40rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: 1.5rem;
overflow: hidden;
box-shadow: 0 40px 90px -40px rgba(0,0,0,.9), 0 0 0 1px color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__hero {
position: relative;
margin: 0;
height: 12rem;
background: #2a1e10;
}
.frm-10__hero img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
filter: saturate(.9) contrast(1.05);
}
.frm-10__hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(transparent 20%, var(--card));
}
.frm-10__hero figcaption {
position: absolute;
z-index: 1;
left: clamp(1.25rem,5vw,2.25rem);
bottom: .5rem;
display: grid;
}
.frm-10__hero figcaption span {
font: 500 clamp(2rem,6vw,2.75rem)/1 var(--serif);
letter-spacing: .01em;
color: var(--gold-2);
}
.frm-10__hero small {
font-size: .75rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--muted);
margin-top: .3rem;
}
.frm-10__body {
display: grid;
gap: 1.4rem;
padding: 1.25rem clamp(1.25rem,5vw,2.25rem) clamp(1.25rem,5vw,2.25rem);
}
.frm-10 h2 {
margin: 0;
font: italic 500 1.5rem var(--serif);
color: var(--muted);
}
.frm-10__party {
display: flex;
align-items: center;
justify-content: space-between;
padding: .75rem 1rem;
border: 1px solid var(--line);
border-radius: 1rem;
background: var(--raised);
}
.frm-10__party > span,
.frm-10 legend,
.frm-10 label {
font-size: .75rem;
font-weight: 500;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__stepper {
display: flex;
align-items: center;
gap: .4rem;
}
.frm-10__stepper button {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
border: 1px solid var(--line);
background: transparent;
color: var(--gold-2);
font-size: 1.25rem;
cursor: pointer;
transition: background .2s, border-color .2s;
}
.frm-10__stepper button:hover:not(:disabled) {
border-color: var(--gold);
background: color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__stepper button:disabled {
opacity: .3;
cursor: not-allowed;
}
.frm-10__stepper button:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.frm-10__n {
min-width: 2.5rem;
text-align: center;
font: 600 1.75rem var(--serif);
font-variant-numeric: lining-nums;
}
.frm-10__fs {
border: 0;
margin: 0;
padding: 0;
min-width: 0;
display: grid;
gap: .7rem;
}
.frm-10__fs legend {
padding: 0;
margin-bottom: .7rem;
display: flex;
gap: .6rem;
align-items: baseline;
}
.frm-10__avail {
letter-spacing: .04em;
text-transform: none;
color: var(--gold);
font-size: .8rem;
}
.frm-10__dates {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 4.25rem;
gap: .5rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
padding: .2rem;
margin: -.2rem;
-webkit-mask: linear-gradient(90deg,#000 85%,transparent);
mask: linear-gradient(90deg,#000 85%,transparent);
}
.frm-10__dates::-webkit-scrollbar {
display: none;
}
.frm-10__dates label,
.frm-10__times label,
.frm-10__areas label {
position: relative;
letter-spacing: normal;
text-transform: none;
}
.frm-10__dates label {
scroll-snap-align: start;
}
.frm-10 :is(.frm-10__dates,.frm-10__times,.frm-10__areas) input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
z-index: 1;
}
.frm-10__dates span {
display: grid;
justify-items: center;
gap: .1rem;
padding: .7rem 0;
border-radius: 1rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
transition: all .25s var(--ease);
}
.frm-10__dates small {
font-size: .66rem;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__dates b {
font: 600 1.6rem/1 var(--serif);
}
.frm-10__dates input:checked + span {
background: linear-gradient(160deg,var(--gold-2),var(--gold));
color: var(--on-gold);
border-color: transparent;
box-shadow: 0 10px 24px -12px var(--gold);
}
.frm-10__dates input:checked + span small {
color: inherit;
opacity: .8;
}
.frm-10 :is(.frm-10__dates,.frm-10__times) input:focus-visible + span {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
.frm-10__times {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(5.25rem,1fr));
gap: .45rem;
}
.frm-10__times span {
display: grid;
place-items: center;
min-height: 2.75rem;
border-radius: .7rem;
border: 1px solid var(--line);
color: var(--ink);
font-size: .92rem;
font-variant-numeric: tabular-nums;
transition: all .2s;
}
.frm-10__times label:hover input:not(:disabled) + span {
border-color: var(--gold);
}
.frm-10__times input:checked + span {
background: color-mix(in oklab,var(--gold) 20%,transparent);
border-color: var(--gold);
color: var(--gold-2);
font-weight: 600;
}
.frm-10__times input:disabled {
cursor: not-allowed;
}
.frm-10__times input:disabled + span {
text-decoration: line-through;
opacity: .35;
}
.frm-10__times span {
animation: frm-10-in .4s var(--ease) both;
animation-delay: calc(var(--k,0) * 25ms);
}
@keyframes frm-10-in {
from {
opacity: 0;
translate: 0 6px;
}
}
.frm-10__large {
margin: 0;
color: var(--muted);
font-size: .9rem;
}
.frm-10__large a {
color: var(--gold-2);
}
.frm-10 a {
color: var(--gold-2);
}
.frm-10 a:hover {
color: var(--gold);
}
.frm-10__areas {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: .6rem;
}
.frm-10__areas label {
display: grid;
border-radius: 1rem;
overflow: hidden;
border: 1px solid var(--line);
background: var(--raised);
transition: border-color .25s, box-shadow .25s;
}
.frm-10__areas img {
width: 100%;
aspect-ratio: 3/2;
height: auto;
object-fit: cover;
display: block;
background: #2a1e10;
filter: grayscale(.6) brightness(.7);
transition: filter .4s var(--ease), scale .6s var(--ease);
}
.frm-10__areas span {
padding: .55rem .7rem;
font-size: .85rem;
color: var(--ink);
}
.frm-10__areas label:hover img {
scale: 1.05;
}
.frm-10__areas label:has(:checked) {
border-color: var(--gold);
box-shadow: 0 0 0 1px var(--gold);
}
.frm-10__areas label:has(:checked) img {
filter: none;
}
.frm-10__areas label:has(:focus-visible) {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
@media (max-width: 30rem) {
.frm-10__areas {
grid-template-columns: 1fr;
}
.frm-10__areas label {
grid-template-columns: 5rem 1fr;
align-items: center;
}
}
.frm-10__grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit,minmax(min(100%,13rem),1fr));
}
.frm-10__field {
display: grid;
gap: .45rem;
}
.frm-10__field label span {
text-transform: none;
letter-spacing: 0;
opacity: .7;
}
.frm-10__field input,
.frm-10__field select {
width: 100%;
min-height: 2.9rem;
padding: .6rem .9rem;
border-radius: .7rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
font: inherit;
font-size: .95rem;
}
.frm-10__field :is(input,select):focus-visible {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px color-mix(in oklab,var(--gold) 22%,transparent);
}
.frm-10__field input:user-invalid {
border-color: #e57373;
}
.frm-10__summary {
display: grid;
gap: .8rem;
padding-top: 1.2rem;
border-top: 1px solid var(--line);
}
.frm-10__sum {
margin: 0;
font: 500 1.25rem/1.3 var(--serif);
color: var(--gold-2);
text-align: center;
text-wrap: balance;
}
.frm-10__book {
min-height: 3.3rem;
border: 0;
border-radius: 999px;
cursor: pointer;
font: inherit;
font-size: .82rem;
font-weight: 600;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--on-gold);
background: linear-gradient(110deg,var(--gold),var(--gold-2),var(--gold));
background-size: 200% 100%;
transition: background-position .8s var(--ease), translate .2s var(--ease);
}
.frm-10__book:hover {
background-position: 100% 0;
translate: 0 -2px;
}
.frm-10__book:focus-visible {
outline: 2px solid var(--gold-2);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.frm-10 * {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.frm-10 :is(.frm-10__dates,.frm-10__times) input:checked + span {
background: Highlight;
color: HighlightText;
}
.frm-10__areas label:has(:checked) {
outline: 2px solid Highlight;
}
.frm-10__book {
border: 1px solid ButtonText;
}
}
/* Theme toggle — top-right of the demo. Ink-relative colours, so it suits light and dark. */
.frm-10 {
position: relative;
padding-top: 72px;
box-sizing: border-box;
}
.frm-10__theme {
position: absolute;
top: 16px;
right: 16px;
z-index: 20;
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background-color .2s, transform .2s;
}
.frm-10__theme:hover {
background: color-mix(in srgb, currentColor 14%, transparent);
}
.frm-10__theme:active {
transform: scale(.94);
}
.frm-10__theme:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.frm-10__theme svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.frm-10__theme .frm-10__sun,
.frm-10__theme[aria-pressed="true"] .frm-10__moon {
display: none;
}
.frm-10__theme[aria-pressed="true"] .frm-10__sun {
display: block;
}
@media (prefers-reduced-motion: reduce) {
.frm-10__theme {
transition: none;
}
}
@media (forced-colors: active) {
.frm-10__theme {
border-color: ButtonText;
background: ButtonFace;
color: ButtonText;
}
}
```
## JavaScript
```js
// Theme toggle (top-right): flips data-theme on the root and mirrors outside changes.
(() => {
document.querySelectorAll('.frm-10').forEach((root) => {
const btn = root.querySelector(':scope > [data-theme-toggle]');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const mq = matchMedia('(prefers-color-scheme: dark)');
const isDark = () => (root.dataset.theme ? root.dataset.theme === 'dark' : true);
const sync = () => {
const dark = isDark();
btn.setAttribute('aria-pressed', String(dark));
btn.title = dark ? 'Switch to light mode' : 'Switch to dark mode';
};
btn.addEventListener('click', () => { root.dataset.theme = isDark() ? 'light' : 'dark'; sync(); });
mq.addEventListener('change', sync);
new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ['data-theme'] });
sync();
});
})();
(() => {
const root = document.querySelector('.frm-10');
if (!root) return;
const $ = (s) => root.querySelector(s);
const DAYS = 10, MAX = 12, SLOTS = ['17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00'];
let party = 2;
const wd = new Intl.DateTimeFormat('en-GB', { weekday: 'short' }), full = new Intl.DateTimeFormat('en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
const dates = $('.frm-10__dates'), times = $('.frm-10__times');
const days = [...Array(DAYS)].map((_, i) => { const d = new Date(); d.setDate(d.getDate() + i); return d; });
dates.innerHTML = days.map((d, i) => '<label><input type="radio" name="frm-10-date" value="' + i + '"' + (i === 0 ? ' checked' : '') + ' aria-label="' + full.format(d) + '"><span aria-hidden="true"><small>' + (i === 0 ? 'Today' : wd.format(d)) + '</small><b>' + d.getDate() + '</b></span></label>').join('');
const fmt12 = (t) => { let [h, m] = t.split(':').map(Number); const ap = h >= 12 ? 'pm' : 'am'; h = h % 12 || 12; return h + ':' + String(m).padStart(2, '0') + ' ' + ap; };
const isBooked = (dayIdx, slotIdx) => ((dayIdx * 7 + slotIdx * 3 + party * 5) % 10) < (party > 4 ? 6 : 3);
const renderTimes = () => {
const day = Number(root.querySelector('[name="frm-10-date"]:checked').value);
const prev = (root.querySelector('[name="frm-10-time"]:checked') || {}).value;
const large = party > 8;
$('.frm-10__large').hidden = !large;
times.hidden = large;
times.innerHTML = SLOTS.map((t, i) => { const b = isBooked(day, i); return '<label><input type="radio" name="frm-10-time" value="' + t + '"' + (b ? ' disabled' : '') + (t === prev && !b ? ' checked' : '') + ' aria-label="' + fmt12(t) + (b ? ', fully booked' : '') + '"><span style="--k:' + i + '" aria-hidden="true">' + fmt12(t) + '</span></label>'; }).join('');
const open = SLOTS.filter((_, i) => !isBooked(day, i)).length;
$('.frm-10__avail').textContent = large ? '' : open + ' of ' + SLOTS.length + ' available';
summary();
};
const summary = () => {
const d = days[Number(root.querySelector('[name="frm-10-date"]:checked').value)];
const t = root.querySelector('[name="frm-10-time"]:checked'), a = root.querySelector('[name="frm-10-area"]:checked').value;
$('.frm-10__sum').textContent = 'Table for ' + party + ' · ' + full.format(d) + ' · ' + (t ? fmt12(t.value) : 'choose a time') + ' · ' + a;
};
root.querySelectorAll('.frm-10__stepper button').forEach((b) => b.addEventListener('click', () => {
party = Math.max(1, Math.min(MAX, party + Number(b.dataset.d)));
$('.frm-10__n').textContent = party;
root.querySelector('[data-d="-1"]').disabled = party === 1; root.querySelector('[data-d="1"]').disabled = party === MAX;
renderTimes();
}));
dates.addEventListener('change', renderTimes);
root.addEventListener('change', (e) => { if (e.target.name === 'frm-10-time' || e.target.name === 'frm-10-area') summary(); });
$('.frm-10__book').addEventListener('click', () => {
if (!root.querySelector('[name="frm-10-time"]:checked')) { (times.querySelector('input:not(:disabled)') || $('.frm-10__book')).focus(); $('.frm-10__sum').textContent = 'Please choose a time.'; return; }
const bad = [...root.querySelectorAll('[required]')].find((f) => !f.checkValidity());
if (bad) { bad.reportValidity(); return; }
$('.frm-10__sum').textContent = 'Confirmed — ' + $('.frm-10__sum').textContent + '. A confirmation text is on its way.';
$('.frm-10__book').textContent = 'Reservation confirmed';
});
renderTimes();
})();
```Here's a working CSS Form from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Restaurant Table Reservation Form
Source: https://codefronts.com/components/css-forms/restaurant-table-reservation-form/
A fine-dining booking widget in candle-lit black and gold: a party-size stepper, a horizontally scrolling date strip built from radios with scroll-snap, a time-slot grid where fully-booked slots are genuinely disabled (and say so), seating-area cards with photography, an occasion selector, and a live sentence summary — “Table for 4 · Fri 9 Oct · 8:00 pm · Terrace” — that updates as you choose.
## HTML
```html
<div class="frm-10">
<button type="button" class="frm-10__theme" data-theme-toggle aria-label="Dark mode" aria-pressed="false" title="Switch to dark mode"><svg class="frm-10__moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></svg><svg class="frm-10__sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
<div class="frm-10__stage">
<div class="frm-10__card">
<figure class="frm-10__hero">
<img src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1000&h=500&fit=crop&auto=format&q=70" width="1000" height="500" alt="Warmly lit restaurant dining room with set tables" loading="lazy" decoding="async">
<figcaption><span>Maison Ardent</span><small>Modern French · Mayfair, London</small></figcaption>
</figure>
<div class="frm-10__body" role="form" aria-labelledby="frm-10-title">
<h2 id="frm-10-title">Reserve a table</h2>
<div class="frm-10__party">
<span id="frm-10-party-l">Guests</span>
<div class="frm-10__stepper" role="group" aria-labelledby="frm-10-party-l">
<button type="button" data-d="-1" aria-label="Fewer guests">−</button>
<output aria-live="polite" class="frm-10__n">2</output>
<button type="button" data-d="1" aria-label="More guests">+</button>
</div>
</div>
<fieldset class="frm-10__fs"><legend>Date</legend><div class="frm-10__dates"></div></fieldset>
<fieldset class="frm-10__fs"><legend>Time <span class="frm-10__avail"></span></legend><div class="frm-10__times"></div><p class="frm-10__large" hidden>For parties of 9 or more, please call <a href="tel:+442070000000">020 7000 0000</a>.</p></fieldset>
<fieldset class="frm-10__fs">
<legend>Seating</legend>
<div class="frm-10__areas">
<label><input type="radio" name="frm-10-area" value="Dining room" checked><img src="https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Dining room</span></label>
<label><input type="radio" name="frm-10-area" value="Terrace"><img src="https://images.unsplash.com/photo-1559339352-11d035aa65de?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Terrace</span></label>
<label><input type="radio" name="frm-10-area" value="Chef's counter"><img src="https://images.unsplash.com/photo-1552566626-52f8b828add9?w=400&h=260&fit=crop&auto=format&q=60" width="400" height="260" alt="" loading="lazy" decoding="async"><span>Chef's counter</span></label>
</div>
</fieldset>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-name">Name</label><input id="frm-10-name" autocomplete="name" required></div>
<div class="frm-10__field"><label for="frm-10-tel">Phone</label><input id="frm-10-tel" type="tel" autocomplete="tel" required></div>
</div>
<div class="frm-10__grid">
<div class="frm-10__field"><label for="frm-10-occ">Occasion</label><select id="frm-10-occ"><option>None</option><option>Birthday</option><option>Anniversary</option><option>Business</option><option>Date night</option></select></div>
<div class="frm-10__field"><label for="frm-10-req">Requests <span>Optional</span></label><input id="frm-10-req" placeholder="Allergies, high chair, quiet table…"></div>
</div>
<div class="frm-10__summary"><p role="status" class="frm-10__sum"></p><button type="button" class="frm-10__book">Confirm reservation</button></div>
</div>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Jost:wght@400;500;600&display=swap');
.frm-10 {
--bg: #0d0b09;
--card: #15120f;
--raised: #1d1915;
--ink: #f3ebdf;
--muted: #a69a89;
--line: #2e2821;
--gold: #d4a95a;
--gold-2: #f0d39a;
--on-gold: #1a1206;
--serif: "Cormorant Garamond",ui-serif,Georgia,serif;
--sans: "Jost",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
--ease: cubic-bezier(.16,1,.3,1);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: radial-gradient(50rem 30rem at 50% 0%, #2a1e10, transparent 70%), var(--bg);
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
.frm-10[data-theme="light"] {
--bg: #f4eee4;
--card: #fffaf2;
--raised: #f6efe3;
--ink: #2a2017;
--muted: #7b6c5b;
--line: #e5d9c6;
--gold: #9a7232;
--gold-2: #b88a3e;
--on-gold: #fff;
background: var(--bg);
}
.frm-10,
.frm-10 *,
.frm-10 *::before,
.frm-10 *::after {
box-sizing: border-box;
}
.frm-10__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1rem,5vw,4rem);
}
.frm-10__card {
width: 100%;
max-width: 40rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: 1.5rem;
overflow: hidden;
box-shadow: 0 40px 90px -40px rgba(0,0,0,.9), 0 0 0 1px color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__hero {
position: relative;
margin: 0;
height: 12rem;
background: #2a1e10;
}
.frm-10__hero img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
filter: saturate(.9) contrast(1.05);
}
.frm-10__hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(transparent 20%, var(--card));
}
.frm-10__hero figcaption {
position: absolute;
z-index: 1;
left: clamp(1.25rem,5vw,2.25rem);
bottom: .5rem;
display: grid;
}
.frm-10__hero figcaption span {
font: 500 clamp(2rem,6vw,2.75rem)/1 var(--serif);
letter-spacing: .01em;
color: var(--gold-2);
}
.frm-10__hero small {
font-size: .75rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--muted);
margin-top: .3rem;
}
.frm-10__body {
display: grid;
gap: 1.4rem;
padding: 1.25rem clamp(1.25rem,5vw,2.25rem) clamp(1.25rem,5vw,2.25rem);
}
.frm-10 h2 {
margin: 0;
font: italic 500 1.5rem var(--serif);
color: var(--muted);
}
.frm-10__party {
display: flex;
align-items: center;
justify-content: space-between;
padding: .75rem 1rem;
border: 1px solid var(--line);
border-radius: 1rem;
background: var(--raised);
}
.frm-10__party > span,
.frm-10 legend,
.frm-10 label {
font-size: .75rem;
font-weight: 500;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__stepper {
display: flex;
align-items: center;
gap: .4rem;
}
.frm-10__stepper button {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
border: 1px solid var(--line);
background: transparent;
color: var(--gold-2);
font-size: 1.25rem;
cursor: pointer;
transition: background .2s, border-color .2s;
}
.frm-10__stepper button:hover:not(:disabled) {
border-color: var(--gold);
background: color-mix(in oklab,var(--gold) 12%,transparent);
}
.frm-10__stepper button:disabled {
opacity: .3;
cursor: not-allowed;
}
.frm-10__stepper button:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.frm-10__n {
min-width: 2.5rem;
text-align: center;
font: 600 1.75rem var(--serif);
font-variant-numeric: lining-nums;
}
.frm-10__fs {
border: 0;
margin: 0;
padding: 0;
min-width: 0;
display: grid;
gap: .7rem;
}
.frm-10__fs legend {
padding: 0;
margin-bottom: .7rem;
display: flex;
gap: .6rem;
align-items: baseline;
}
.frm-10__avail {
letter-spacing: .04em;
text-transform: none;
color: var(--gold);
font-size: .8rem;
}
.frm-10__dates {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 4.25rem;
gap: .5rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
padding: .2rem;
margin: -.2rem;
-webkit-mask: linear-gradient(90deg,#000 85%,transparent);
mask: linear-gradient(90deg,#000 85%,transparent);
}
.frm-10__dates::-webkit-scrollbar {
display: none;
}
.frm-10__dates label,
.frm-10__times label,
.frm-10__areas label {
position: relative;
letter-spacing: normal;
text-transform: none;
}
.frm-10__dates label {
scroll-snap-align: start;
}
.frm-10 :is(.frm-10__dates,.frm-10__times,.frm-10__areas) input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
z-index: 1;
}
.frm-10__dates span {
display: grid;
justify-items: center;
gap: .1rem;
padding: .7rem 0;
border-radius: 1rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
transition: all .25s var(--ease);
}
.frm-10__dates small {
font-size: .66rem;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
.frm-10__dates b {
font: 600 1.6rem/1 var(--serif);
}
.frm-10__dates input:checked + span {
background: linear-gradient(160deg,var(--gold-2),var(--gold));
color: var(--on-gold);
border-color: transparent;
box-shadow: 0 10px 24px -12px var(--gold);
}
.frm-10__dates input:checked + span small {
color: inherit;
opacity: .8;
}
.frm-10 :is(.frm-10__dates,.frm-10__times) input:focus-visible + span {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
.frm-10__times {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(5.25rem,1fr));
gap: .45rem;
}
.frm-10__times span {
display: grid;
place-items: center;
min-height: 2.75rem;
border-radius: .7rem;
border: 1px solid var(--line);
color: var(--ink);
font-size: .92rem;
font-variant-numeric: tabular-nums;
transition: all .2s;
}
.frm-10__times label:hover input:not(:disabled) + span {
border-color: var(--gold);
}
.frm-10__times input:checked + span {
background: color-mix(in oklab,var(--gold) 20%,transparent);
border-color: var(--gold);
color: var(--gold-2);
font-weight: 600;
}
.frm-10__times input:disabled {
cursor: not-allowed;
}
.frm-10__times input:disabled + span {
text-decoration: line-through;
opacity: .35;
}
.frm-10__times span {
animation: frm-10-in .4s var(--ease) both;
animation-delay: calc(var(--k,0) * 25ms);
}
@keyframes frm-10-in {
from {
opacity: 0;
translate: 0 6px;
}
}
.frm-10__large {
margin: 0;
color: var(--muted);
font-size: .9rem;
}
.frm-10__large a {
color: var(--gold-2);
}
.frm-10 a {
color: var(--gold-2);
}
.frm-10 a:hover {
color: var(--gold);
}
.frm-10__areas {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: .6rem;
}
.frm-10__areas label {
display: grid;
border-radius: 1rem;
overflow: hidden;
border: 1px solid var(--line);
background: var(--raised);
transition: border-color .25s, box-shadow .25s;
}
.frm-10__areas img {
width: 100%;
aspect-ratio: 3/2;
height: auto;
object-fit: cover;
display: block;
background: #2a1e10;
filter: grayscale(.6) brightness(.7);
transition: filter .4s var(--ease), scale .6s var(--ease);
}
.frm-10__areas span {
padding: .55rem .7rem;
font-size: .85rem;
color: var(--ink);
}
.frm-10__areas label:hover img {
scale: 1.05;
}
.frm-10__areas label:has(:checked) {
border-color: var(--gold);
box-shadow: 0 0 0 1px var(--gold);
}
.frm-10__areas label:has(:checked) img {
filter: none;
}
.frm-10__areas label:has(:focus-visible) {
outline: 2px solid var(--gold-2);
outline-offset: 2px;
}
@media (max-width: 30rem) {
.frm-10__areas {
grid-template-columns: 1fr;
}
.frm-10__areas label {
grid-template-columns: 5rem 1fr;
align-items: center;
}
}
.frm-10__grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit,minmax(min(100%,13rem),1fr));
}
.frm-10__field {
display: grid;
gap: .45rem;
}
.frm-10__field label span {
text-transform: none;
letter-spacing: 0;
opacity: .7;
}
.frm-10__field input,
.frm-10__field select {
width: 100%;
min-height: 2.9rem;
padding: .6rem .9rem;
border-radius: .7rem;
border: 1px solid var(--line);
background: var(--raised);
color: var(--ink);
font: inherit;
font-size: .95rem;
}
.frm-10__field :is(input,select):focus-visible {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px color-mix(in oklab,var(--gold) 22%,transparent);
}
.frm-10__field input:user-invalid {
border-color: #e57373;
}
.frm-10__summary {
display: grid;
gap: .8rem;
padding-top: 1.2rem;
border-top: 1px solid var(--line);
}
.frm-10__sum {
margin: 0;
font: 500 1.25rem/1.3 var(--serif);
color: var(--gold-2);
text-align: center;
text-wrap: balance;
}
.frm-10__book {
min-height: 3.3rem;
border: 0;
border-radius: 999px;
cursor: pointer;
font: inherit;
font-size: .82rem;
font-weight: 600;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--on-gold);
background: linear-gradient(110deg,var(--gold),var(--gold-2),var(--gold));
background-size: 200% 100%;
transition: background-position .8s var(--ease), translate .2s var(--ease);
}
.frm-10__book:hover {
background-position: 100% 0;
translate: 0 -2px;
}
.frm-10__book:focus-visible {
outline: 2px solid var(--gold-2);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.frm-10 * {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.frm-10 :is(.frm-10__dates,.frm-10__times) input:checked + span {
background: Highlight;
color: HighlightText;
}
.frm-10__areas label:has(:checked) {
outline: 2px solid Highlight;
}
.frm-10__book {
border: 1px solid ButtonText;
}
}
/* Theme toggle — top-right of the demo. Ink-relative colours, so it suits light and dark. */
.frm-10 {
position: relative;
padding-top: 72px;
box-sizing: border-box;
}
.frm-10__theme {
position: absolute;
top: 16px;
right: 16px;
z-index: 20;
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
transition: background-color .2s, transform .2s;
}
.frm-10__theme:hover {
background: color-mix(in srgb, currentColor 14%, transparent);
}
.frm-10__theme:active {
transform: scale(.94);
}
.frm-10__theme:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.frm-10__theme svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.frm-10__theme .frm-10__sun,
.frm-10__theme[aria-pressed="true"] .frm-10__moon {
display: none;
}
.frm-10__theme[aria-pressed="true"] .frm-10__sun {
display: block;
}
@media (prefers-reduced-motion: reduce) {
.frm-10__theme {
transition: none;
}
}
@media (forced-colors: active) {
.frm-10__theme {
border-color: ButtonText;
background: ButtonFace;
color: ButtonText;
}
}
```
## JavaScript
```js
// Theme toggle (top-right): flips data-theme on the root and mirrors outside changes.
(() => {
document.querySelectorAll('.frm-10').forEach((root) => {
const btn = root.querySelector(':scope > [data-theme-toggle]');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const mq = matchMedia('(prefers-color-scheme: dark)');
const isDark = () => (root.dataset.theme ? root.dataset.theme === 'dark' : true);
const sync = () => {
const dark = isDark();
btn.setAttribute('aria-pressed', String(dark));
btn.title = dark ? 'Switch to light mode' : 'Switch to dark mode';
};
btn.addEventListener('click', () => { root.dataset.theme = isDark() ? 'light' : 'dark'; sync(); });
mq.addEventListener('change', sync);
new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ['data-theme'] });
sync();
});
})();
(() => {
const root = document.querySelector('.frm-10');
if (!root) return;
const $ = (s) => root.querySelector(s);
const DAYS = 10, MAX = 12, SLOTS = ['17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00'];
let party = 2;
const wd = new Intl.DateTimeFormat('en-GB', { weekday: 'short' }), full = new Intl.DateTimeFormat('en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
const dates = $('.frm-10__dates'), times = $('.frm-10__times');
const days = [...Array(DAYS)].map((_, i) => { const d = new Date(); d.setDate(d.getDate() + i); return d; });
dates.innerHTML = days.map((d, i) => '<label><input type="radio" name="frm-10-date" value="' + i + '"' + (i === 0 ? ' checked' : '') + ' aria-label="' + full.format(d) + '"><span aria-hidden="true"><small>' + (i === 0 ? 'Today' : wd.format(d)) + '</small><b>' + d.getDate() + '</b></span></label>').join('');
const fmt12 = (t) => { let [h, m] = t.split(':').map(Number); const ap = h >= 12 ? 'pm' : 'am'; h = h % 12 || 12; return h + ':' + String(m).padStart(2, '0') + ' ' + ap; };
const isBooked = (dayIdx, slotIdx) => ((dayIdx * 7 + slotIdx * 3 + party * 5) % 10) < (party > 4 ? 6 : 3);
const renderTimes = () => {
const day = Number(root.querySelector('[name="frm-10-date"]:checked').value);
const prev = (root.querySelector('[name="frm-10-time"]:checked') || {}).value;
const large = party > 8;
$('.frm-10__large').hidden = !large;
times.hidden = large;
times.innerHTML = SLOTS.map((t, i) => { const b = isBooked(day, i); return '<label><input type="radio" name="frm-10-time" value="' + t + '"' + (b ? ' disabled' : '') + (t === prev && !b ? ' checked' : '') + ' aria-label="' + fmt12(t) + (b ? ', fully booked' : '') + '"><span style="--k:' + i + '" aria-hidden="true">' + fmt12(t) + '</span></label>'; }).join('');
const open = SLOTS.filter((_, i) => !isBooked(day, i)).length;
$('.frm-10__avail').textContent = large ? '' : open + ' of ' + SLOTS.length + ' available';
summary();
};
const summary = () => {
const d = days[Number(root.querySelector('[name="frm-10-date"]:checked').value)];
const t = root.querySelector('[name="frm-10-time"]:checked'), a = root.querySelector('[name="frm-10-area"]:checked').value;
$('.frm-10__sum').textContent = 'Table for ' + party + ' · ' + full.format(d) + ' · ' + (t ? fmt12(t.value) : 'choose a time') + ' · ' + a;
};
root.querySelectorAll('.frm-10__stepper button').forEach((b) => b.addEventListener('click', () => {
party = Math.max(1, Math.min(MAX, party + Number(b.dataset.d)));
$('.frm-10__n').textContent = party;
root.querySelector('[data-d="-1"]').disabled = party === 1; root.querySelector('[data-d="1"]').disabled = party === MAX;
renderTimes();
}));
dates.addEventListener('change', renderTimes);
root.addEventListener('change', (e) => { if (e.target.name === 'frm-10-time' || e.target.name === 'frm-10-area') summary(); });
$('.frm-10__book').addEventListener('click', () => {
if (!root.querySelector('[name="frm-10-time"]:checked')) { (times.querySelector('input:not(:disabled)') || $('.frm-10__book')).focus(); $('.frm-10__sum').textContent = 'Please choose a time.'; return; }
const bad = [...root.querySelectorAll('[required]')].find((f) => !f.checkValidity());
if (bad) { bad.reportValidity(); return; }
$('.frm-10__sum').textContent = 'Confirmed — ' + $('.frm-10__sum').textContent + '. A confirmation text is on its way.';
$('.frm-10__book').textContent = 'Reservation confirmed';
});
renderTimes();
})();
```How this works
The date strip is a row of radio labels inside a scroll container with snap points, so it swipes like a native picker on touch and arrow-keys like a radio group on desktop:
.frm-10__dates{
display:grid; grid-auto-flow:column; grid-auto-columns:4.25rem;
overflow-x:auto; scroll-snap-type:x mandatory; scrollbar-width:none;
}
.frm-10__dates label{ scroll-snap-align:start; }Unavailable times are real disabled radios — skipped by arrow keys and announced as “dimmed” — styled with a strike-through so the meaning isn't colour-only:
.frm-10__times input:disabled + span{
text-decoration:line-through; opacity:.35; cursor:not-allowed;
}Availability is recomputed when the date or party size changes (larger parties get fewer slots), and the selected time is cleared if it becomes unavailable. The summary sentence is a role="status" live region.
Make it yours
- Set service hours in the
SLOTSarray; seatings every 30 minutes by default. - Replace
isBooked()with a fetch to your reservation system (OpenTable, Resy, SevenRooms, or your own). - Change
DAYSto show two weeks instead of one. - Add or remove seating areas — each is an image radio card.
- Swap the gold accent (
--gold) for brass, copper or a brand colour.
Gotchas — read before shipping
- Disabled radios aren't focusable; if none are available on a day, say so in text — an empty grid is confusing.
- Use the restaurant's time zone, not the visitor's, when rendering slot times.
- Large parties (8+) usually need a call or deposit — show that message instead of slots.
- Hide the native scrollbar only if there's another affordance (fade edges, arrows); here the cut-off chip signals more content.
- Phone numbers: use
type="tel"andautocomplete="tel"; never validate format strictly across countries.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by :has(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 105+.
scroll-snap is universal. :has() highlights the selected seating card (Firefox 121+). Everything else is baseline.