Die Überschrift wird per JS in einzelne Buchstaben zerlegt, die beim Erscheinen gestaffelt von unten hochgleiten und einblenden (Stagger über --i und transition-delay); der Replay-Knopf löst den Einflug erneut aus. Das aria-label hält die Headline für Screenreader lesbar, bei reduzierter Bewegung steht sie sofort im Endzustand. Vorbild: reactbits SplitText.
Handwerk, das man sieht.
<div class="te1-stage">
<h2 class="te1-headline" data-splittext aria-label="Handwerk, das man sieht.">Handwerk, das man sieht.</h2>
<button type="button" class="te1-replay" data-splittext-replay>
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v4h4"/>
</svg>
Erneut abspielen
</button>
</div>
<style>
.te1-stage{width:100%;text-align:center}
.te1-headline{margin:0;font-size:clamp(1.6rem,5vw,2.6rem);font-weight:800;line-height:1.12;
letter-spacing:-.01em;color:#f4f4f5}
.te1-word{display:inline-block;white-space:nowrap} /* Wort bleibt zusammen, Umbruch nur zwischen Wörtern */
.te1-char{display:inline-block;will-change:transform,opacity}
/* Startzustand: jeder Buchstabe unten + unsichtbar */
.te1-stage .te1-char{opacity:0;transform:translateY(.9em)}
/* Endzustand nach Trigger: hochgleiten + einblenden, gestaffelt über --i */
.te1-stage.is-in .te1-char{opacity:1;transform:translateY(0);
transition:transform .5s cubic-bezier(.22,1,.36,1),opacity .5s cubic-bezier(.22,1,.36,1);
transition-delay:calc(var(--i) * 35ms)}
.te1-replay{display:inline-flex;align-items:center;gap:.45rem;margin-top:1.4rem;
padding:.5rem .95rem;font:inherit;font-size:.85rem;color:#e9e9ee;background:#1c1c22;
border:1px solid #26262c;border-radius:99px;cursor:pointer;
transition:border-color .2s ease,background .2s ease,color .2s ease}
.te1-replay svg{color:var(--akzent,#7c87ff);transition:transform .4s ease}
.te1-replay:hover,.te1-replay:focus-visible{border-color:var(--akzent,#7c87ff);background:#202028;color:#f4f4f5}
.te1-replay:hover svg{transform:rotate(-90deg)}
.te1-replay:focus-visible{outline:2px solid var(--akzent,#7c87ff);outline-offset:2px}
@media (prefers-reduced-motion:reduce){
.te1-stage .te1-char{opacity:1;transform:none} /* Endzustand direkt zeigen */
.te1-stage.is-in .te1-char{transition:none;transition-delay:0s}
.te1-replay,.te1-replay svg{transition:none}
.te1-replay:hover svg{transform:none}
}
</style>
<script>
(function(){
var stages = document.querySelectorAll('.te1-stage');
if(!stages.length) return;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches;
stages.forEach(function(stage){
var h = stage.querySelector('[data-splittext]');
var btn = stage.querySelector('[data-splittext-replay]');
if(!h || h.dataset.split === '1') return; /* mehrfach-sicher */
/* Text in Wörter, Wörter in Buchstaben zerlegen; Leerzeichen als echte
Textknoten erhalten, damit der Zeilenumbruch weiter funktioniert */
var text = h.textContent, idx = 0;
h.textContent = '';
text.split(/(\s+)/).forEach(function(part){
if(/^\s+$/.test(part)){ h.appendChild(document.createTextNode(part)); return; }
var word = document.createElement('span');
word.className = 'te1-word';
part.split('').forEach(function(ch){
var s = document.createElement('span');
s.className = 'te1-char';
s.style.setProperty('--i', idx++);
s.textContent = ch;
word.appendChild(s);
});
h.appendChild(word);
});
h.dataset.split = '1';
function play(){
if(reduce){ stage.classList.add('is-in'); return; }
stage.classList.remove('is-in');
void h.offsetWidth; /* Reflow erzwingen -> Stagger neu starten */
stage.classList.add('is-in');
}
/* Erstes Abspielen, sobald die Headline sichtbar wird */
if('IntersectionObserver' in window && !reduce){
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){ if(e.isIntersecting){ play(); io.disconnect(); } });
}, { threshold: 0.5 });
io.observe(stage);
} else {
play();
}
if(btn) btn.addEventListener('click', play);
});
})();
</script>