Toasts: succes, fout, voortgang, ongedaan maken New
Four toast types on one quote screen: success (auto-hides after 5 s), error (stays, with "Retry" and loading state), progress with a bar ending in "Download", and delete with "Undo" and a countdown bar. They stack like a deck and fan out on hover or focus; timers pause then and when the tab is hidden. Announced via aria-live (errors assertive). Entrance with @starting-style.
Offerte 2026-041Concept
Keukenrenovatie fam. Bakker, Zeist
€ 18.450,00 incl. btw
Offerte 2026-041 is verwijderd.
How to use it
- Paste the HTML where the components should appear.
- Add the CSS to your stylesheet. All classes are prefixed, so nothing clashes with your own styles.
- Add the JavaScript and call init() with the element that contains the HTML.
<!-- Toasts: succes, fout, voortgang, ongedaan maken · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="tsv">
<div class="tsv__card">
<div class="tsv__row"><span class="tsv__eyebrow">Offerte 2026-041</span><span class="tsv__state">Concept</span></div>
<strong class="tsv__title">Keukenrenovatie fam. Bakker, Zeist</strong>
<span class="tsv__amt">€ 18.450,00 incl. btw</span>
<div class="tsv__btns"><button type="button" data-t="ok">Opslaan</button><button type="button" data-t="err">Versturen</button><button type="button" data-t="prog">PDF maken</button><button type="button" data-t="undo" class="tsv__del">Verwijderen</button></div>
</div>
<div class="tsv__gone"><p>Offerte 2026-041 is verwijderd.</p><button type="button" class="tsv__restore">Zet voorbeeld terug</button></div>
<ol class="tsv__region" aria-label="Meldingen"></ol>
<div class="tsv__sr" aria-live="polite"></div><div class="tsv__sr" aria-live="assertive"></div>
</div>
</div>/* Toasts: succes, fout, voortgang, ongedaan maken · NedDev · eigen werk. */
.tsv{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--line:rgba(10,10,10,.12);position:absolute;inset:0;overflow:hidden;font-family:'Switzer',system-ui,sans-serif;color:var(--ink);padding:14px 16px}
.tsv__card{background:#fff;border:1px solid var(--line);border-radius:6px;padding:12px 14px;display:flex;flex-direction:column;gap:4px;max-width:340px}
.tsv__row{display:flex;justify-content:space-between;align-items:center}
.tsv__eyebrow{font:600 9.5px/1 'JetBrains Mono',monospace;letter-spacing:.12em;text-transform:uppercase;color:#5b5b5b}
.tsv__state{font:600 9.5px/1 'JetBrains Mono',monospace;letter-spacing:.08em;text-transform:uppercase;padding:4px 6px;border-radius:3px;background:#f4f3f1}
.tsv__title{font-size:14px}
.tsv__amt{font-size:12px;color:#5b5b5b;font-variant-numeric:tabular-nums}
.tsv__btns{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.tsv__btns button,.tsv__restore{font:600 11.5px/1 'Switzer',system-ui,sans-serif;padding:8px 10px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);cursor:pointer;transition:background .15s,color .15s}
.tsv__btns button:hover,.tsv__restore:hover{background:var(--ink);color:#fff}
.tsv__btns button:active{transform:translateY(1px)}
.tsv__btns .tsv__del{border-color:#b3261e;color:#b3261e}
.tsv__btns .tsv__del:hover{background:#b3261e;color:#fff}
.tsv button:focus-visible{outline:2px solid var(--pd);outline-offset:2px}
.tsv__gone{display:none;max-width:340px;padding:16px;border:1px dashed rgba(10,10,10,.3);border-radius:6px;font-size:13px;color:#5b5b5b}
.tsv__gone p{margin:0 0 10px}
.tsv.is-deleted .tsv__card{display:none}.tsv.is-deleted .tsv__gone{display:block}
.tsv__region{position:absolute;right:12px;bottom:12px;width:min(290px,calc(100% - 24px));height:var(--fh,0);margin:0;padding:0;list-style:none;z-index:5}
.tsv__t{position:absolute;left:0;right:0;bottom:0;display:grid;grid-template-columns:18px 1fr auto auto;align-items:center;gap:8px;padding:9px 6px 9px 11px;border-radius:6px;background:var(--ink);color:#fff;box-shadow:0 10px 26px rgba(10,10,10,.22);transform-origin:50% 100%;overflow:hidden;opacity:1;translate:0 0;transition:transform .28s cubic-bezier(.2,.8,.2,1),opacity .25s ease,translate .3s cubic-bezier(.2,.8,.2,1)}
@starting-style{.tsv__t{translate:0 110%;opacity:0}}
.tsv__t.is-back{height:var(--fh)}
.tsv__t.is-back>*{opacity:0}
.tsv__t.is-out{opacity:0;translate:24px 0}
.tsv__ic{width:18px;height:18px;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.tsv__t--ok .tsv__ic{stroke:#6fd39a}.tsv__t--err .tsv__ic{stroke:#ff8a80}.tsv__t--prog .tsv__ic,.tsv__t--undo .tsv__ic{stroke:#ffb08f}
.tsv__msg{min-width:0;font-size:12.5px;line-height:1.3}
.tsv__msg b,.tsv__msg span{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tsv__msg b{font-weight:600}
.tsv__msg span{color:#c9c9c9;font-size:11.5px}
.tsv__a{font:700 11.5px/1 'Switzer',system-ui,sans-serif;padding:7px 8px;border:0;border-radius:4px;background:rgba(255,255,255,.12);color:#fff;cursor:pointer;white-space:nowrap;transition:background .15s}
.tsv__a:hover{background:rgba(255,255,255,.24)}
.tsv__a[aria-busy=true]::before{content:'';display:inline-block;vertical-align:-1px;width:9px;height:9px;margin-right:6px;border:2px solid #fff;border-right-color:transparent;border-radius:50%;animation:tsv-spin .7s linear infinite}
@keyframes tsv-spin{to{transform:rotate(360deg)}}
.tsv__x{width:26px;height:26px;border:0;border-radius:4px;background:none;color:#c9c9c9;cursor:pointer;display:grid;place-items:center;padding:0}
.tsv__x:hover{color:#fff;background:rgba(255,255,255,.12)}
.tsv__x svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round}
.tsv__t .tsv__a:focus-visible,.tsv__t .tsv__x:focus-visible{outline:2px solid var(--pop);outline-offset:1px}
.tsv__bar{position:absolute;left:0;bottom:0;height:3px;width:100%;background:var(--pop);transform-origin:left;transform:scaleX(0)}
.tsv__t--undo .tsv__bar{transform:scaleX(1);animation:tsv-cd var(--d,6s) linear forwards}
.tsv__region:hover .tsv__t--undo .tsv__bar,.tsv__region:focus-within .tsv__t--undo .tsv__bar,.tsv.is-hidden .tsv__t--undo .tsv__bar{animation-play-state:paused}
@keyframes tsv-cd{to{transform:scaleX(0)}}
.tsv__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@media (max-width:420px){.tsv{padding:10px 12px}.tsv__card{padding:10px 12px}.tsv__btns{margin-top:4px}.tsv__btns button{padding:7px 8px;font-size:11px}}
@media (prefers-reduced-motion:reduce){.tsv__t{transition:none}}// Toasts: succes, fout, voortgang, ongedaan maken · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.tsv');if(!w)return;
var reg=w.querySelector('.tsv__region'),srs=w.querySelectorAll('.tsv__sr'),list=[],MAX=4;
var IC={ok:'<circle cx="12" cy="12" r="9"/><path d="m8 12 3 3 5-6"/>',err:'<circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/>',prog:'<path d="M12 3v12M7 10l5 5 5-5M5 20h14"/>',undo:'<path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 0 12h-3"/>'};
var X='<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>';
function now(){var d=new Date();return d.getHours()+':'+(d.getMinutes()<10?'0':'')+d.getMinutes();}
function say(txt,assert){var el=srs[assert?1:0];el.textContent='';setTimeout(function(){el.textContent=txt;},40);}
function expanded(){return reg.matches(':hover')||reg.contains(document.activeElement);}
function layout(){var exp=expanded(),y=0,front=list[0];if(front)front.el.classList.remove('is-back');reg.style.setProperty('--fh',front?front.el.offsetHeight+'px':'0px');
list.forEach(function(t,i){t.el.classList.toggle('is-back',!exp&&i>0);t.el.style.zIndex=20-i;
if(exp){t.el.style.transform='translateY('+(-y)+'px)';y+=t.el.offsetHeight+8;t.el.style.opacity='';}
else{t.el.style.transform='translateY('+(-i*9)+'px) scale('+(1-i*.05)+')';t.el.style.opacity=i>2?'0':'';}});}
function paused(){return expanded()||document.hidden;}
function arm(t){clearTimeout(t.tm);if(!t.dur||paused())return;t.t0=Date.now();t.tm=setTimeout(function(){if(t.onEnd)t.onEnd();remove(t);},t.left);}
function hold(){list.forEach(function(t){if(t.tm){clearTimeout(t.tm);t.tm=0;t.left=Math.max(400,t.left-(Date.now()-t.t0));}});}
function resume(){list.forEach(function(t){if(!t.tm)arm(t);});}
function remove(t){clearTimeout(t.tm);var i=list.indexOf(t);if(i<0)return;list.splice(i,1);t.el.classList.add('is-out');var had=t.el.contains(document.activeElement);setTimeout(function(){t.el.remove();},260);layout();
if(had){var n=list[0],f=n?n.el.querySelector('.tsv__x'):(w.classList.contains('is-deleted')?w.querySelector('.tsv__restore'):w.querySelector('.tsv__btns button'));if(f)f.focus();}}
function set(t,kind,title,sub,action,dur){t.el.className='tsv__t tsv__t--'+kind;t.el.innerHTML='<svg class="tsv__ic" viewBox="0 0 24 24" aria-hidden="true">'+IC[kind]+'</svg><div class="tsv__msg"><b>'+title+'</b><span>'+sub+'</span></div>'+(action?'<button class="tsv__a" type="button">'+action+'</button>':'<span></span>')+'<button class="tsv__x" type="button" aria-label="Melding sluiten">'+X+'</button>'+(kind==='prog'||kind==='undo'?'<span class="tsv__bar" aria-hidden="true"></span>':'');
t.el.querySelector('.tsv__x').addEventListener('click',function(){remove(t);});t.dur=dur||0;t.left=dur||0;if(kind==='undo')t.el.style.setProperty('--d',dur+'ms');arm(t);layout();
say(title+'. '+sub,kind==='err');return t.el.querySelector('.tsv__a');}
function push(){var li=document.createElement('li');var t={el:li};reg.insertBefore(li,reg.firstChild);list.unshift(t);while(list.length>MAX)remove(list[list.length-1]);return t;}
var make={
ok:function(){var t=push();set(t,'ok','Opgeslagen','Concept bijgewerkt om '+now(),'',5000);},
err:function(){var t=push();var a=set(t,'err','Versturen mislukt','Geen e-mailadres bij de klant','Opnieuw',0);
a.addEventListener('click',function(){a.setAttribute('aria-busy','true');a.textContent='Bezig';setTimeout(function(){if(list.indexOf(t)<0)return;set(t,'ok','Offerte verstuurd','Naar bakker@voorbeeld.nl','',5000);var x=t.el.querySelector('.tsv__x');if(reg.contains(document.activeElement)||document.activeElement===document.body)x.focus();},1200);});},
prog:function(){var t=push();set(t,'prog','PDF wordt gemaakt','0%','',0);var bar=t.el.querySelector('.tsv__bar'),sub=t.el.querySelector('.tsv__msg span'),p=0;
var iv=setInterval(function(){if(list.indexOf(t)<0){clearInterval(iv);return;}p=Math.min(100,p+7+Math.random()*9);bar.style.transform='scaleX('+p/100+')';sub.textContent=Math.round(p)+'% · 4 blz.';
if(p>=100){clearInterval(iv);var a=set(t,'ok','PDF klaar','offerte-2026-041.pdf · 212 kB','Download',5000);a.addEventListener('click',function(){remove(t);});}},180);},
undo:function(){w.classList.add('is-deleted');var t=push();var a=set(t,'undo','Offerte verwijderd','2026-041 · Keukenrenovatie','Ongedaan maken',6000);
a.addEventListener('click',function(){t.onEnd=null;w.classList.remove('is-deleted');set(t,'ok','Offerte teruggezet','Alles staat weer zoals het was','',4000);w.querySelector('.tsv__del').focus();});
t.onEnd=function(){if(t.el.contains(document.activeElement))w.querySelector('.tsv__restore').focus();};
if(w.contains(document.activeElement)||document.activeElement===document.body)a.focus();}
};
w.querySelectorAll('.tsv__btns button').forEach(function(b){b.addEventListener('click',function(){make[b.dataset.t]();});});
w.querySelector('.tsv__restore').addEventListener('click',function(){w.classList.remove('is-deleted');w.querySelector('.tsv__btns button').focus();});
reg.addEventListener('mouseenter',function(){hold();layout();});reg.addEventListener('mouseleave',function(){setTimeout(function(){layout();if(!paused())resume();},0);});
reg.addEventListener('focusin',function(){hold();layout();});reg.addEventListener('focusout',function(){setTimeout(function(){layout();if(!paused())resume();},0);});
document.addEventListener('visibilitychange',function(){w.classList.toggle('is-hidden',document.hidden);if(document.hidden)hold();else resume();});
}
init(document.querySelector('.jouw-container'));
More components
Seen something you like?
Add it to your selection and send it over. We build your site around it, in your brand.
Get in touch →