Als ich Material Design zum ersten Mal entdeckte, war ich besonders inspiriert von seiner Button-Komponente. Sie nutzt einen Ripple-Effekt, um dem Benutzer auf einfache und elegante Weise Feedback zu geben.
Wie funktioniert dieser Effekt? Material Design-Buttons zeigen nicht nur eine nette Ripple-Animation, sondern die Animation ändert auch ihre Position je nachdem, wo auf den Button geklickt wird.

Wir können das gleiche Ergebnis erzielen. Wir beginnen mit einer prägnanten Lösung mit ES6+ JavaScript, bevor wir uns einige alternative Ansätze ansehen.
HTML
Unser Ziel ist es, unnötigen HTML-Markup zu vermeiden. Wir werden uns also mit dem absoluten Minimum begnügen
<button>Find out more</button>
Den Button stylen
Wir müssen einige Elemente unseres Ripples dynamisch per JavaScript stylen. Alles andere kann jedoch in CSS erfolgen. Für unsere Buttons sind nur zwei Eigenschaften erforderlich.
button {
position: relative;
overflow: hidden;
}
Die Verwendung von position: relative ermöglicht uns die Verwendung von position: absolute für unser Ripple-Element, was wir zur Steuerung seiner Position benötigen. Währenddessen verhindert overflow: hidden, dass der Ripple die Kanten des Buttons überschreitet. Alles andere ist optional. Aber im Moment sieht unser Button etwas altmodisch aus. Hier ist ein modernerer Ausgangspunkt
/* Roboto is Material's default font */
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
button {
position: relative;
overflow: hidden;
transition: background 400ms;
color: #fff;
background-color: #6200ee;
padding: 1rem 2rem;
font-family: 'Roboto', sans-serif;
font-size: 1.5rem;
outline: 0;
border: 0;
border-radius: 0.25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3);
cursor: pointer;
}
Styling der Ripples
Später werden wir JavaScript verwenden, um Ripples als Spans mit einer Klasse .ripple in unser HTML einzufügen. Aber bevor wir uns mit JavaScript befassen, definieren wir einen Stil für diese Ripples in CSS, damit wir sie bereithalten
span.ripple {
position: absolute; /* The absolute position we mentioned earlier */
border-radius: 50%;
transform: scale(0);
animation: ripple 600ms linear;
background-color: rgba(255, 255, 255, 0.7);
}
Um unsere Ripples kreisförmig zu machen, haben wir border-radius auf 50 % gesetzt. Und um sicherzustellen, dass jeder Ripple aus dem Nichts entsteht, haben wir die Standard-Skalierung auf 0 gesetzt. Im Moment können wir nichts sehen, da wir noch keinen Wert für die Eigenschaften top, left, width oder height haben; diese Eigenschaften werden wir bald mit JavaScript einfügen.
Was unseren CSS-Code betrifft, so müssen wir als letztes noch einen Endzustand für die Animation hinzufügen
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
Beachten Sie, dass wir keinen Startzustand mit dem Schlüsselwort from in den Keyframes definieren? Wir können from weglassen und CSS wird die fehlenden Werte basierend auf denen konstruieren, die für das animierte Element gelten. Dies geschieht, wenn die relevanten Werte explizit angegeben sind – wie bei transform: scale(0) – oder wenn sie Standardwerte sind, wie opacity: 1.
Nun zum JavaScript
Schließlich benötigen wir JavaScript, um die Position und Größe unserer Ripples dynamisch festzulegen. Die Größe sollte auf der Größe des Buttons basieren, während die Position sowohl auf der Position des Buttons als auch des Cursors basieren sollte.
Wir beginnen mit einer leeren Funktion, die ein Klickereignis als Argument nimmt
function createRipple(event) {
//
}
Wir greifen auf unseren Button zu, indem wir das currentTarget des Ereignisses finden.
const button = event.currentTarget;
Als nächstes instanziieren wir unser Span-Element und berechnen seinen Durchmesser und Radius basierend auf der Breite und Höhe des Buttons.
const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
Wir können nun die verbleibenden Eigenschaften definieren, die wir für unsere Ripples benötigen: left, top, width und height.
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;
circle.classList.add("ripple");
Bevor wir das Span-Element zum DOM hinzufügen, ist es gute Praxis zu prüfen, ob bereits vorhandene Ripples von früheren Klicks vorhanden sind und diese zu entfernen, bevor der nächste ausgeführt wird.
const ripple = button.getElementsByClassName("ripple")[0];
if (ripple) {
ripple.remove();
}
Als letzten Schritt fügen wir das Span als Kind-Element zum Button-Element hinzu, sodass es innerhalb des Buttons eingefügt wird.
button.appendChild(circle);
Nachdem unsere Funktion vollständig ist, müssen wir sie nur noch aufrufen. Dies könnte auf verschiedene Arten geschehen. Wenn wir den Ripple auf jeden Button auf unserer Seite anwenden möchten, können wir etwas wie folgt verwenden
const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
button.addEventListener("click", createRipple);
}
Jetzt haben wir einen funktionierenden Ripple-Effekt!
Weiterführend
Was ist, wenn wir weitergehen und diesen Effekt mit anderen Änderungen an der Position oder Größe unseres Buttons kombinieren wollen? Die Möglichkeit zur Anpassung ist schließlich einer der Hauptvorteile, wenn wir uns entscheiden, den Effekt selbst nachzubilden. Um zu testen, wie einfach es ist, unsere Funktion zu erweitern, habe ich beschlossen, einen "Magnet"-Effekt hinzuzufügen, der bewirkt, dass sich unser Button zum Cursor bewegt, wenn sich der Cursor innerhalb eines bestimmten Bereichs befindet.
Wir müssen uns auf einige der gleichen Variablen verlassen, die in der Ripple-Funktion definiert sind. Anstatt Code unnötigerweise zu wiederholen, sollten wir sie an einem Ort speichern, an dem sie für beide Methoden zugänglich sind. Wir sollten jedoch die gemeinsamen Variablen auf jeden einzelnen Button beschränkt halten. Eine Möglichkeit, dies zu erreichen, ist die Verwendung von Klassen, wie im folgenden Beispiel
Da der Magneteffekt den Cursor jedes Mal verfolgen muss, wenn er sich bewegt, müssen wir die Cursorposition nicht mehr berechnen, um einen Ripple zu erzeugen. Stattdessen können wir uns auf cursorX und cursorY verlassen.
Zwei wichtige neue Variablen sind magneticPullX und magneticPullY. Sie steuern, wie stark unsere Magnetmethode den Button dem Cursor hinterherzieht. Wenn wir also das Zentrum unseres Ripples definieren, müssen wir sowohl die Position des neuen Buttons (x und y) als auch den magnetischen Zug berücksichtigen.
const offsetLeft = this.left + this.x * this.magneticPullX;
const offsetTop = this.top + this.y * this.magneticPullY;
Um diese kombinierten Effekte auf alle unsere Buttons anzuwenden, müssen wir für jeden eine neue Instanz der Klasse erstellen
const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
new Button(button);
}
Andere Techniken
Natürlich ist dies nur eine Möglichkeit, einen Ripple-Effekt zu erzielen. Auf CodePen gibt es viele Beispiele, die verschiedene Implementierungen zeigen. Unten sind einige meiner Favoriten.
Nur CSS
Wenn ein Benutzer JavaScript deaktiviert hat, hat unser Ripple-Effekt keine Fallbacks. Aber es ist möglich, dem ursprünglichen Effekt mit nur CSS nahe zu kommen, indem die Pseudoklasse :active verwendet wird, um auf Klicks zu reagieren. Die Haupteinschränkung ist, dass der Ripple nur von einer Stelle ausgehen kann – normalerweise von der Mitte des Buttons –, anstatt auf die Position unserer Klicks zu reagieren. Dieses Beispiel von Ben Szabo ist besonders prägnant
JavaScript vor ES6
Leandro Parices Demo ist unserer Implementierung ähnlich, aber sie ist mit früheren JavaScript-Versionen kompatibel:
jQuery
Dieses Beispiel verwendet jQuery, um den Ripple-Effekt zu erzielen. Wenn Sie jQuery bereits als Abhängigkeit haben, kann dies helfen, einige Codezeilen zu sparen.
React
Zum Schluss noch ein letztes Beispiel von mir. Obwohl es möglich ist, React-Funktionen wie State und Refs zu verwenden, um den Ripple-Effekt zu erzeugen, sind diese nicht unbedingt erforderlich. Die Position und Größe des Ripples müssen für jeden Klick berechnet werden, daher gibt es keinen Vorteil, diese Informationen im State zu speichern. Außerdem können wir über das Klickereignis auf unser Button-Element zugreifen, sodass wir auch keine Refs benötigen.
Dieses React-Beispiel verwendet eine createRipple-Funktion, die identisch mit der ersten Implementierung dieses Artikels ist. Der Hauptunterschied besteht darin, dass unsere Funktion – als Methode der Button-Komponente – auf diese Komponente beschränkt ist. Außerdem ist der onClick-Event-Listener jetzt Teil unseres JSX
Das war erstaunlich…!!
Einer der Entwickler des tatsächlichen Material Design Ripple-Effekts schrieb vor einigen Jahren einen Artikel über die Kompromisse verschiedener Möglichkeiten, ihn zu implementieren und zu animieren. Dieser Link wäre eine schöne Ergänzung zu diesem Artikel.
Das ist so cool! Danke fürs Teilen!
Schön!! Ich würde gerne ein Tutorial für die Material Tooltips sehen! (Wie die, die in Google Docs oder YouTube verwendet werden), die sind ordentlich!
Ich habe den Pen geforked, um eine Houdini-Version zu erstellen. Was derzeit die Unterstützung auf Chromium-Browser beschränkt, aber es ermöglicht uns auch, den Effekt ohne zusätzliche Elemente oder Pseudoelemente zu erzielen, da der Ripple jetzt ein
radial-gradient()ist und vereinfacht auch das JavaScript, da wir die Größe des Ripples relativ zum Button nicht berechnen müssen.Wie das funktioniert
x, y(gesetzt als benutzerdefinierte Eigenschaften), der sich mit semitransparentem Weiß von variabler Alpha (gesetzt als benutzerdefinierte Eigenschaft--a) bis zu einem variablen Radius (gesetzt als benutzerdefinierte Eigenschaft--r) füllt.--aund--r, damit wir sie bei Bedarf animieren können.anihat (die er anfangs nicht hat), animieren wir diese benutzerdefinierten Eigenschaften von.7auf0und von0%auf100%.--xund--yauf die Koordinaten des geklickten Punktes relativ zur oberen linken Ecke des Buttons..anihinzu (dies startet die CSS-Animation)..anivom Button.Das ist ein wirklich interessanter Ansatz. Vielen Dank für das Teilen und die Mühe, die wichtigsten Änderungen zu erklären!
Schön, aber was ist mit einem Eingabefeld anstelle eines Buttons?
Schöne Technik, ich habe sie nach Svelte hier portiert. Ich habe ein paar Dinge geändert, um es dem offiziellen Material-Implementierung optisch näher zu bringen (Blur hinzufügen, geringere anfängliche Deckkraft und größere anfängliche Skalierung des Ripple-Spans).
Ich denke, das Einzige, was hier fehlt, ist, dass auf Mobilgeräten clientX und Y nicht existieren und Sie pageX und Y verwenden müssen, was bedeutet, dass Sie zuerst auf ein Touch-Ereignis prüfen müssen. Großartige Arbeit trotzdem.
Hallo JHeth etc. Ich habe Ihr Beispiel in eine Svelte-Aktion umgewandelt, die es ermöglicht, den Ripple-Effekt auf alles anzuwenden und ihn gleichzeitig durch CSS, CSS-Props und normale Props hochgradig konfigurierbar zu machen.
https://svelte.dev/repl/4430fde7d42a4b03845d81411e701702?version=3.44.3
Danke für diesen schönen Artikel!
Eine Sache, die ich anders gemacht habe, ist, anstatt bestehende Ripples beim nächsten Klick manuell zu entfernen, können Sie auch auf das Ende der Animation warten und den Ripple direkt danach entfernen
circle.addEventListener('animationend', () => button.removeChild(circle))Gute Arbeit, danke fürs Teilen
Toller Artikel! Dies ähnelt stark der Art und Weise, wie ich die v-wave-Direktive für Vue erstellt habe. Der Hauptunterschied besteht darin, dass der Ripple näher an der Implementierung auf Android liegt, da er bei
pointerdown(es sei denn,pointercancelwird ausgelöst) erscheint und bis zum Auslösen despointerup-Ereignisses sichtbar bleibt.Großartige Arbeit! Für alle, die dies auf absolut positionierte Elemente anwenden möchten, habe ich festgestellt, dass die Verwendung von
event.offsetXundevent.offsetYein besser vorhersagbares Codemuster ist.Im Wesentlichen ändern sich die
circle.style.[top|left]-Berechnungen zuDiese Änderung ist möglicherweise nur erforderlich, wenn das offsetParent Ihres Buttons nicht der Body ist, der bei top/left===0 verankert ist.
Fügen Sie
pointer-events: none;zum Stilspan.ripplehinzu, um Fehlberechnungen von offsetX und offsetY zu vermeiden, wenn Sie sehr schnell klicken (d. h. auf den Ripple).Vielen Dank fürs Teilen.
event.clientXist relativ zum Viewport, während button.offsetLeft relativ zum offsetParent des Buttons ist. Das Beispiel funktionierte nur wie im Artikel beschrieben, weil 'button' ein direktes Kind des Body war. Daher könnten wirbutton.getBoundingClientRect()verwenden, um die Eigenschaften left und top des Buttons abzurufen, die relativ zum Viewport sind. Dann könnte der Code wie folgt umgeschrieben werden:Ein weiteres Problem ist das Entfernen und Hinzufügen des Span-Elements jedes Mal, wenn
createRipple()aufgerufen wird. Sobald das Span-Element erstellt ist, sollte der Code die Animation per Klasse umschalten.Ersetzen Sie den Kreis durch eine SVG-Sechseckform, fügen Sie einen kleinen CSS-Spin hinzu und voilà
Die rotierende sechseckige Klick-Spot-Show
Jetzt rocken wir größere Räume sowie Buttons.
Hoppla, Link fehlgeschlagen
In VueJS pageX pageY anstelle von clientX clientY verwenden
Wenn ich das sagen darf, ich konnte das React-Sample nur dann richtig zum Laufen bringen, wenn ich
clientXundclientYinpageXundpageYgeändert habe.Kann mir bitte jemand die Berechnungen für links und oben erklären,
circle.style.left =
${event.clientX - (button.offsetLeft + radius)}px;circle.style.top =
${event.clientY - (button.offsetTop + radius)}px;Ich habe dies auf meiner Website ausprobiert, musste aber feststellen, dass es bei gescrollten Elementen nicht gut funktionierte. Das kann ich bestätigen, indem ich viele
<br>vor dem Button einfüge. Der Button bewegt sich nach unten, aber der Ripple ist immer noch an seiner ursprünglichen Position verankert. Daher hat der Button keinen Ripple.Hat jemand eine reine CSS- oder HTML-Lösung für dieses Problem? Ich habe ein JavaScript-Problem, das die Y-Achse des Ripples um den Wert von window.scrollY versetzt. Ich wäre sehr dankbar für Hilfe.
Sie müssen window.scrollX und window.scrollY berücksichtigen. funktionierende Demo https://codepen.io/giuliano-oliveira/pen/YzNYJZY
Wenn der übergeordnete Container des Buttons nicht der Body ist, benötigen wir für
element.offsetetwas wie:const getOffsetTop = element => {.let offsetTop = 0;
while (element) {
offsetTop += element.offsetTop;
element = element.offsetParent;
}
return offsetTop;
};
https://medium.com/@alexcambose/js-offsettop-property-is-not-great-and-here-is-why-b79842ef7582
Danke für dieses Tutorial! Ich verwende den Ripple auf einem großen weißen Hintergrund.
Der
linear-Übergang sieht etwas seltsam aus. Der Ripple ist im einen Moment da und im nächsten plötzlich verschwunden.Daher ziehe ich es vor,
ease-outanstelle vonlinearzu verwenden. (Hauptsächlich für diebackground-Eigenschaft, aber es sieht auch nicht schlecht aus, wenn estransformbeeinflusst, auch wenn reale Wellen sich nicht verlangsamen.)Nur eine freundliche Erinnerung, dass dieser Beispielcode WCAG nicht bestehen würde
https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html
Die Verwendung von
outline:0ohne eine alternative sichtbare Änderung bei Tastaturfokus führt dazu, dass dies die Erfolgskriterien nicht erfüllt.Hallo. Für diejenigen unter Ihnen, die die Code-Snippets im Artikel für Ihr eigenes Projekt verwenden möchten, integrieren Sie diese beiden oben geposteten Kommentare
– Verwenden Sie
offsetXundoffsetY(von Angelo Gonzales), und– Entfernen Sie den Ripple, sobald die Animation vorbei ist (von Jonas).
Ich habe anfangs einfach den Code im Artikel kopiert und eingefügt, nur um einen Fehler festzustellen. Machen Sie nicht dasselbe durch, was ich durchgemacht habe.
Wenn Sie
offsetvom Element verwenden, um seine Position zu erhalten, werden Sie Probleme haben, wenn Sie es innerhalb eines relativen übergeordneten Elements verwenden. Anstattoffsetzu verwenden, verwenden Sie stattdessengetBoundingClientRect(). Funktioniert einwandfrei! :)