Knop wordt bevestigingskaart Nieuw
De knop "Afspraak annuleren" groeit uit tot een bevestigingskaart (native dialog) en krimpt terug; met laden, fout (vink "storing" aan) en succes. View Transitions voor de vormverandering, zonder support of bij reduced motion verschijnt de kaart direct. Esc en Terug sluiten, de focus keert terug naar de knop. Voor een losse bevestigingspagina op een multi-page site: @view-transition { navigation: auto; } en dezelfde view-transition-name op knop en kaart.
Garage Bakker · ZwolleGepland
APK en kleine beurt
GX-418-K
Zo gebruik je het
- Plak de HTML op de plek waar het onderdeel moet komen.
- Zet de CSS in je stylesheet. Alle klassen hebben een eigen voorvoegsel, dus niets botst met je eigen opmaak.
- Voeg de JavaScript toe en roep init() aan met het element waar de HTML in staat.
<!-- Knop wordt bevestigingskaart · NedDev · eigen werk. -->
<div class="jouw-container">
<!-- Knop en dialog krijgen om beurten view-transition-name: vtd-shape; de browser morpht de vorm.
De dialog opent met show() (niet-modaal, blijft in het blok). Op een echte pagina mag showModal() en is Esc ingebouwd.
Bevestiging als aparte pagina (multi-page): @view-transition { navigation: auto; } en op beide pagina's view-transition-name: vtd-shape
op de knop en op de kaart. Reduced motion: @media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; } } -->
<div class="vtd">
<div class="vtd__card">
<div class="vtd__row"><span class="vtd__kick">Garage Bakker · Zwolle</span><span class="vtd__state">Gepland</span></div>
<p class="vtd__h">APK en kleine beurt</p>
<p class="vtd__meta">vr 16 okt · 09:30 tot ca. 11:00</p>
<div class="vtd__plate" aria-label="Kenteken GX-418-K"><span class="vtd__nl" aria-hidden="true">NL</span><span>GX-418-K</span></div>
<button class="vtd__open" type="button" aria-haspopup="dialog">Afspraak annuleren</button>
<button class="vtd__nieuw" type="button" hidden>Nieuwe afspraak plannen</button>
<dialog class="vtd__dlg" aria-labelledby="vtd-t" aria-describedby="vtd-d">
<p class="vtd__dt" id="vtd-t">Afspraak annuleren?</p>
<p class="vtd__dd" id="vtd-d">Je APK op vr 16 okt om 09:30 vervalt. Kosteloos tot 24 uur vooraf.</p>
<p class="vtd__err" role="alert" hidden></p>
<div class="vtd__acts"><button class="vtd__terug" type="button">Terug</button><button class="vtd__ja" type="button">Ja, annuleren</button></div>
</dialog>
</div>
<label class="vtd__sim"><input type="checkbox" class="vtd__storing"> storing</label>
<span class="vtd__sr" aria-live="polite"></span>
</div>
</div>/* Knop wordt bevestigingskaart · NedDev · eigen werk. */
.vtd{position:absolute;inset:0;background:#f4f3f1;font-family:'Switzer',system-ui,sans-serif;color:#0a0a0a;display:flex;align-items:center;justify-content:center;padding:12px}
.vtd__card{position:relative;width:100%;max-width:370px;height:100%;max-height:210px;background:#fff;border-radius:6px;box-shadow:0 0 0 1px rgba(10,10,10,.08),0 6px 18px rgba(10,10,10,.06);padding:13px 15px;box-sizing:border-box}
.vtd__row{display:flex;justify-content:space-between;align-items:center;gap:8px}
.vtd__kick{font:500 10px 'JetBrains Mono',monospace;letter-spacing:.1em;text-transform:uppercase;color:rgba(10,10,10,.6)}
.vtd__state{font:600 10px 'JetBrains Mono',monospace;padding:2px 7px;border-radius:999px;background:#e8f4ec;color:#1b6b3c}
.vtd__state.is-off{background:#fdece6;color:#a32d08}
.vtd__h{margin:8px 0 2px;font-size:17px;font-weight:700;letter-spacing:-.01em}
.vtd__meta{margin:0 0 9px;font-size:12px;color:rgba(10,10,10,.65)}
.vtd__plate{display:inline-flex;align-items:stretch;border:1.5px solid #0a0a0a;border-radius:3px;overflow:hidden;font:700 13px 'JetBrains Mono',monospace;letter-spacing:.04em;background:#f7c600}
.vtd__plate span:last-child{padding:3px 8px}
.vtd__nl{background:#1d3e9e;color:#fff;font-size:8px;padding:0 4px;display:flex;align-items:flex-end;padding-bottom:3px}
.vtd__open,.vtd__nieuw{all:unset;box-sizing:border-box;position:absolute;right:13px;bottom:13px;font-size:12.5px;font-weight:600;padding:8px 13px;border-radius:4px;cursor:pointer;transition:background-color .15s,color .15s,border-color .15s}
.vtd__open{border:1px solid rgba(10,10,10,.25);color:#a32d08;background:#fff}
.vtd__open:hover{border-color:#a32d08;background:#fdece6}
.vtd__nieuw{background:#0a0a0a;color:#fff}
.vtd__nieuw:hover{background:#F5480F}
.vtd__open[hidden],.vtd__nieuw[hidden]{display:none}
.vtd__open:focus-visible,.vtd__nieuw:focus-visible,.vtd__terug:focus-visible,.vtd__ja:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.vtd__open:active,.vtd__nieuw:active,.vtd__ja:active,.vtd__terug:active{transform:translateY(1px)}
.vtd__dlg{position:absolute;inset:auto 8px 8px auto;margin:0;width:min(268px,calc(100% - 16px));box-sizing:border-box;padding:12px 13px;border:0;border-radius:6px;background:#0a0a0a;color:#fff;box-shadow:0 10px 30px rgba(10,10,10,.28)}
.vtd__dt{margin:0 0 4px;font-size:14px;font-weight:700}
.vtd__dd{margin:0 0 10px;font-size:11.5px;line-height:1.45;color:rgba(255,255,255,.72)}
.vtd__err{margin:-4px 0 9px;font-size:11.5px;line-height:1.4;color:#ffb199}
.vtd__err[hidden]{display:none}
.vtd__acts{display:flex;justify-content:flex-end;gap:6px}
.vtd__terug,.vtd__ja{all:unset;box-sizing:border-box;font-size:12px;font-weight:600;padding:7px 11px;border-radius:4px;cursor:pointer;transition:background-color .15s,color .15s}
.vtd__terug{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}
.vtd__terug:hover{background:rgba(255,255,255,.1)}
.vtd__ja{background:#F5480F;color:#0a0a0a}
.vtd__ja:hover{background:#ff6a36}
.vtd__ja[aria-busy="true"]{cursor:progress;display:inline-flex;align-items:center;gap:6px;opacity:.85}
.vtd__ja[aria-busy="true"]::before{content:'';width:10px;height:10px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;animation:vtd-spin .7s linear infinite}
@keyframes vtd-spin{to{transform:rotate(360deg)}}
.vtd__sim{position:absolute;left:14px;bottom:6px;display:flex;align-items:center;gap:4px;font:500 9.5px 'JetBrains Mono',monospace;color:rgba(10,10,10,.5);cursor:pointer}
.vtd__sim input{margin:0;width:11px;height:11px;accent-color:#F5480F}
.vtd__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media (max-width:420px){.vtd{padding:8px 8px 18px}.vtd__card{padding:10px 12px}.vtd__h{font-size:15px;margin-top:6px}.vtd__meta{margin-bottom:7px}.vtd__open,.vtd__nieuw{right:10px;bottom:10px;padding:7px 11px;font-size:12px}.vtd__dd{font-size:11px;margin-bottom:8px}.vtd__dlg{padding:10px 11px}.vtd__sim{bottom:3px}}
@media (prefers-reduced-motion:reduce){.vtd__ja[aria-busy="true"]::before{animation-duration:2s}}
::view-transition-group(vtd-shape){animation-duration:.36s;animation-timing-function:cubic-bezier(.2,.8,.2,1);overflow:clip;border-radius:6px}
::view-transition-old(vtd-shape),::view-transition-new(vtd-shape){height:100%;width:100%;object-fit:none;object-position:right bottom}
::view-transition-old(vtd-shape){animation:vtd-uit .14s ease-out both}
::view-transition-new(vtd-shape){animation:vtd-in .22s ease-out .08s both}
@keyframes vtd-uit{to{opacity:0}}
@keyframes vtd-in{from{opacity:0}}
@media (prefers-reduced-motion:reduce){::view-transition-group(vtd-shape),::view-transition-old(vtd-shape),::view-transition-new(vtd-shape){animation:none!important}}// Knop wordt bevestigingskaart · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.vtd');if(!w)return;var openB=w.querySelector('.vtd__open'),nieuw=w.querySelector('.vtd__nieuw'),dlg=w.querySelector('.vtd__dlg'),terug=w.querySelector('.vtd__terug'),ja=w.querySelector('.vtd__ja'),err=w.querySelector('.vtd__err'),stor=w.querySelector('.vtd__storing'),state=w.querySelector('.vtd__state'),sr=w.querySelector('.vtd__sr');
var rm=matchMedia('(prefers-reduced-motion: reduce)');var busy=false;
var uid='vtd'+Math.random().toString(36).slice(2,7);var t1=w.querySelector('#vtd-t'),d1=w.querySelector('#vtd-d');t1.id=uid+'-t';d1.id=uid+'-d';dlg.setAttribute('aria-labelledby',t1.id);dlg.setAttribute('aria-describedby',d1.id);
function morph(from,to,upd,after){if(!document.startViewTransition||rm.matches){upd();after&&after();return;}busy=true;from.style.viewTransitionName='vtd-shape';
var t=document.startViewTransition(function(){from.style.viewTransitionName='';upd();to.style.viewTransitionName='vtd-shape';});
t.finished.finally(function(){to.style.viewTransitionName='';busy=false;after&&after();});}
function openD(){if(busy||dlg.open)return;err.hidden=true;ja.textContent='Ja, annuleren';
morph(openB,dlg,function(){openB.hidden=true;dlg.show();},function(){terug.focus({preventScroll:true});});}
function closeD(toBtn){if(busy||!dlg.open)return;morph(dlg,toBtn,function(){dlg.close();toBtn.hidden=false;},function(){toBtn.focus({preventScroll:true});});}
openB.addEventListener('click',openD);terug.addEventListener('click',function(){closeD(openB);});
dlg.addEventListener('keydown',function(e){if(e.key==='Escape'){e.preventDefault();if(ja.getAttribute('aria-busy')!=='true')closeD(openB);}
else if(e.key==='Tab'){var f=[terug,ja];var i=f.indexOf(document.activeElement);if(e.shiftKey&&i<=0){e.preventDefault();ja.focus();}else if(!e.shiftKey&&i===f.length-1){e.preventDefault();terug.focus();}}});
dlg.addEventListener('cancel',function(e){e.preventDefault();});
ja.addEventListener('click',function(){if(ja.getAttribute('aria-busy')==='true')return;ja.setAttribute('aria-busy','true');ja.textContent='Bezig';terug.disabled=true;err.hidden=true;
setTimeout(function(){ja.removeAttribute('aria-busy');terug.disabled=false;
if(stor.checked){ja.textContent='Opnieuw proberen';err.textContent='Dat lukte niet. Probeer het opnieuw of bel 038 123 4567.';err.hidden=false;ja.focus({preventScroll:true});return;}
state.textContent='Geannuleerd';state.classList.add('is-off');sr.textContent='Afspraak geannuleerd. Je krijgt een bevestiging per mail.';closeD(nieuw);},900);});
nieuw.addEventListener('click',function(){state.textContent='Gepland';state.classList.remove('is-off');nieuw.hidden=true;openB.hidden=false;openB.focus({preventScroll:true});sr.textContent='Nieuwe afspraak gepland voor vr 16 okt, 09:30';});
}
init(document.querySelector('.jouw-container'));
Meer uit componenten
Iets gezien dat je mooi vindt?
Zet het in je selectie en stuur hem op. Dan bouwen we je site eromheen, in jouw huisstijl.
Neem contact op →