UNPKG

16.1 kBMarkdownView Raw
1<!--docs:
2title: "Ripples"
3layout: detail
4section: components
5excerpt: "Ink ripple touch feedback effect."
6iconId: ripple
7path: /catalog/ripples/
8-->
9
10# Ripple
11
12MDC Ripple provides the JavaScript and CSS required to provide components (or any element at all) with a material "ink ripple" interaction effect. It is designed to be efficient, uninvasive, and usable without adding any extra DOM to your elements.
13
14MDC Ripple also works without JavaScript, where it gracefully degrades to a simpler CSS-Only implementation.
15
16## Design & API Documentation
17
18<ul class="icon-list">
19 <li class="icon-list-item icon-list-item--spec">
20 <a href="https://material.io/go/design-states">Material Design guidelines: States</a>
21 </li>
22 <li class="icon-list-item icon-list-item--link">
23 <a href="https://material-components.github.io/material-components-web-catalog/#/component/ripple">Demo</a>
24 </li>
25</ul>
26
27## Installation
28
29```
30npm install @material/ripple
31```
32
33## Usage
34
35A ripple can be applied to a variety of elements to represent interactive surfaces. Several MDC Web components, such as Button, FAB, Checkbox and Radio, also use ripples.
36
37A ripple can be added to an element through either a JavaScript or CSS-only implementation. When a ripple is initialized on an element using JS, it dynamically adds a `mdc-ripple-upgraded` class to that element. If ripple JS is not initialized but Sass mixins are included on the element, the ripple uses a simpler CSS-only implementation which relies on the `:hover`, `:focus`, and `:active` pseudo-classes.
38
39### CSS Classes
40
41CSS Class | Description
42--- | ---
43`mdc-ripple-surface` | Adds a ripple to the element
44`mdc-ripple-surface--primary` | Sets the ripple color to the theme primary color
45`mdc-ripple-surface--accent` | Sets the ripple color to the theme secondary color
46
47### Sass APIs
48
49In order to fully style the ripple effect for different states (hover/focus/pressed), the following mixins must be included:
50
51* `surface`, for base styles
52* Either `radius-bounded` or `radius-unbounded`, to appropriately size the ripple on the surface
53* Either the basic or advanced `states` mixins, as explained below
54
55##### Using basic states mixins
56```css
57@use "@material/ripple";
58
59.my-surface {
60 @include ripple.surface;
61 @include ripple.radius-bounded;
62 @include ripple.states;
63}
64```
65
66##### Using advanced states mixins
67```css
68.my-surface {
69 @include ripple.surface;
70 @include ripple.radius-bounded;
71 @include ripple.states-base-color(black);
72 @include ripple.states-opacities((hover: .1, focus: .3, press: .4));
73}
74```
75
76These APIs use pseudo-elements for the ripple effect: `::before` for the background, and `::after` for the foreground.
77
78#### Ripple Mixins
79
80Mixin | Description
81--- | ---
82`surface` | Mandatory. Adds base styles for a ripple surface
83`radius-bounded($radius)` | Adds styles for the radius of the ripple effect,<br>for bounded ripple surfaces
84`radius-unbounded($radius)` | Adds styles for the radius of the ripple effect,<br>for unbounded ripple surfaces
85
86> _NOTE_: It is mandatory to include _either_ `radius-bounded` or `radius-unbounded`. In both cases, `$radius` is optional and defaults to `100%`.
87
88#### Basic States Mixins
89
90Mixin | Description
91--- | ---
92`states($color, $has-nested-focusable-element)` | Mandatory. Adds state and ripple styles in the given color
93`states-activated($color, $has-nested-focusable-element)` | Optional. Adds state and ripple styles for activated states in the given color
94`states-selected($color, $has-nested-focusable-element)` | Optional. Adds state and ripple styles for selected states in the given color
95
96> _NOTE_: Each of the mixins above adds ripple styles using the indicated color, deciding opacity values based on whether the passed color is light or dark.
97
98> _NOTE_: The `states-activated` and `states-selected` mixins add the appropriate state styles to the root element containing `&--activated` or `&--selected` modifier classes respectively.
99
100> _NOTE_: `$has-nested-focusable-element` defaults to `false` but should be set to `true` if the component contains a focusable element (e.g. an input) inside the root element.
101
102#### Advanced States Mixins
103
104When using the advanced states mixins instead of the basic states mixins, every one of the mixins below should be included at least once.
105
106These mixins can also be used to emit activated or selected styles, by applying them within a selector for
107`&--activated` or `&--selected` modifier classes.
108
109Mixin | Description
110--- | ---
111`states-base-color($color)` | Mandatory. Sets up base state styles using the provided color
112`states-opacities($opacity-map, $has-nested-focusable-element)` | Sets the opacity of the ripple in any of the `hover`, `focus`, or `press` states. The `opacity-map` can specify one or more of these states as keys. States not specified in the map resort to default opacity values.
113
114> _NOTE_: `$has-nested-focusable-element` defaults to `false` but should be set to `true` if the component contains a focusable element (e.g. an input) inside the root element.
115
116> _DEPRECATED_: The individual mixins `states-hover-opacity($opacity)`, `states-focus-opacity($opacity, $has-nested-focusable-element)`, and `states-press-opacity($opacity)` are deprecated in favor of the unified `states-opacities($opacity-map, $has-nested-focusable-element)` mixin above.
117
118#### Sass Functions
119
120Function | Description
121--- | ---
122`states-opacity($color, $state)` | Returns the appropriate default opacity to apply to the given color in the given state (hover, focus, press, selected, or activated)
123
124### `MDCRipple`
125
126The `MDCRipple` JavaScript component allows for programmatic activation / deactivation of the ripple, for interdependent interaction between
127components. For example, this is used for making form field labels trigger the ripples in their corresponding input elements.
128
129To use the `MDCRipple` component, first [import the `MDCRipple` JS](../../docs/importing-js.md). Then, initialize the ripple with the correct DOM element.
130
131```javascript
132const surface = document.querySelector('.my-surface');
133const ripple = new MDCRipple(surface);
134```
135
136You can also use `attachTo()` as an alias if you don't care about retaining a reference to the
137ripple.
138
139```javascript
140MDCRipple.attachTo(document.querySelector('.my-surface'));
141```
142
143Property | Value Type | Description
144--- | --- | ---
145`unbounded` | Boolean | Whether or not the ripple is unbounded
146> _NOTE_: Surfaces for bounded ripples should have the `overflow` property set to `hidden`, while surfaces for unbounded ripples should have it set to `visible`.
147
148Method Signature | Description
149--- | ---
150`activate() => void` | Proxies to the foundation's `activate` method
151`deactivate() => void` | Proxies to the foundation's `deactivate` method
152`layout() => void` | Proxies to the foundation's `layout` method
153`handleFocus() => void` | Handles focus event on the ripple surface
154`handleBlur() => void` | Handles blur event on the ripple surface
155
156### `MDCRippleAdapter`
157
158| Method Signature | Description |
159| --- | --- |
160| `browserSupportsCssVars() => boolean` | Whether or not the given browser supports CSS Variables. |
161| `isUnbounded() => boolean` | Whether or not the ripple should be considered unbounded. |
162| `isSurfaceActive() => boolean` | Whether or not the surface the ripple is acting upon is [active](https://www.w3.org/TR/css3-selectors/#useraction-pseudos) |
163| `isSurfaceDisabled() => boolean` | Whether or not the ripple is attached to a disabled component |
164| `addClass(className: string) => void` | Adds a class to the ripple surface |
165| `removeClass(className: string) => void` | Removes a class from the ripple surface |
166| `containsEventTarget(target: EventTarget) => boolean` | Whether or not the ripple surface contains the given event target |
167| `registerInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler on the ripple surface |
168| `deregisterInteractionHandler(evtType: string, handler: EventListener) => void` | Unregisters an event handler on the ripple surface |
169| `registerDocumentInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler on the documentElement |
170| `deregisterDocumentInteractionHandler(evtType: string, handler: EventListener) => void` | Unregisters an event handler on the documentElement |
171| `registerResizeHandler(handler: Function) => void` | Registers a handler to be called when the ripple surface (or its viewport) resizes |
172| `deregisterResizeHandler(handler: Function) => void` | Unregisters a handler to be called when the ripple surface (or its viewport) resizes |
173| `updateCssVariable(varName: string, value: (string or null)) => void` | Sets the CSS property `varName` on the ripple surface to the value specified |
174| `computeBoundingRect() => ClientRect` | Returns the ClientRect for the surface |
175| `getWindowPageOffset() => {x: number, y: number}` | Returns the `page{X,Y}Offset` values for the window object |
176
177> _NOTE_: When implementing `browserSupportsCssVars`, please take the [Safari 9](#caveat-safari) considerations into account. We provide a `supportsCssVariables` function within the `util.js` which we recommend using, as it handles this for you.
178
179### `MDCRippleFoundation`
180
181Method Signature | Description
182--- | ---
183`activate() => void` | Triggers an activation of the ripple (the first stage, which happens when the ripple surface is engaged via interaction, such as a `mousedown` or a `pointerdown` event). It expands from the center.
184`deactivate() => void` | Triggers a deactivation of the ripple (the second stage, which happens when the ripple surface is engaged via interaction, such as a `mouseup` or a `pointerup` event). It expands from the center.
185`layout() => void` | Recomputes all dimensions and positions for the ripple element. Useful if a ripple surface's position or dimension is changed programmatically.
186`setUnbounded(unbounded: boolean) => void` | Sets the ripple to be unbounded or not, based on the given boolean.
187
188## Tips/Tricks
189
190### Using a sentinel element for a ripple
191
192Usually, you'll want to leverage `::before` and `::after` pseudo-elements when integrating the ripple into MDC Web components. If you can't use pseudo-elements, create a sentinel element inside your root element. The sentinel element covers the root element's surface.
193
194```html
195<div class="my-component">
196 <div class="mdc-ripple-surface"></div>
197 <!-- your component DOM -->
198</div>
199```
200
201### Unbounded ripple
202
203You can set a ripple to be _unbounded_, such as those used for MDC Checkboxes and MDC Radio Buttons, either imperatively in JS _or_ declaratively using the DOM.
204
205#### Using JS
206
207Set the `unbounded` property on the `MDCRipple` component.
208
209```javascript
210const ripple = new MDCRipple(root);
211ripple.unbounded = true;
212```
213
214#### Using DOM
215
216Add a `data-mdc-ripple-is-unbounded` attribute to your root element.
217
218```html
219<div class="my-surface" data-mdc-ripple-is-unbounded>
220 <p>A surface</p>
221</div>
222```
223
224### MDCRipple with custom functionality
225
226Usually, you'll want to use `MDCRipple` _along_ with the component for the actual UI element you're trying to add a
227ripple to. `MDCRipple` has a static `createAdapter(instance)` method that can be used to instantiate a ripple within
228any `MDCComponent` that requires custom adapter functionality.
229
230```ts
231class MyMDCComponent extends MDCComponent {
232 constructor() {
233 super(...arguments);
234 const foundation = new MDCRippleFoundation({
235 ...MDCRipple.createAdapter(this),
236 isSurfaceActive: () => this.isActive, /* Custom functionality */
237 });
238 this.ripple = new MDCRipple(this.root, foundation);
239 }
240}
241```
242
243### Handling keyboard events for custom UI components
244
245Different keyboard events activate different elements. For example, the space key activates buttons, while the enter key activates links.
246
247`MDCRipple` uses the `adapter.isSurfaceActive()` method to detect whether or not a keyboard event has activated the surface the ripple is on. Our vanilla implementation of the adapter does this by checking whether the `:active` pseudo-class has been applied to the ripple surface. However, this approach will _not_ work for custom components that the browser does not apply this pseudo-class to.
248
249To make your component work properly with keyboard events, you'll have to listen for both `keydown` and `keyup` events to set some state that determines whether or not the surface is "active".
250
251```ts
252class MyComponent {
253 constructor(element) {
254 this.root = element;
255 this.active = false;
256 this.root.addEventListener('keydown', evt => {
257 if (isSpace(evt)) {
258 this.active = true;
259 }
260 });
261 this.root.addEventListener('keyup', evt => {
262 if (isSpace(evt)) {
263 this.active = false;
264 }
265 });
266 const foundation = new MDCRippleFoundation(
267 ...MDCRipple.createAdapter(this),
268 // ...
269 isSurfaceActive: () => this.active
270 });
271 this.ripple = new MDCRipple(this.root, foundation);
272 }
273}
274```
275
276### Specifying known element dimensions for asynchronous style loading
277
278If you asynchronously load style resources, such as loading stylesheets dynamically or loading fonts, then `adapter.getClientRect()` may return _incorrect_ dimensions if the ripple is initialized before the stylesheet/font has loaded. In this case, you can override the default behavior of `getClientRect()` to return the correct results.
279
280For example, if you know an icon font sizes its elements to `24px` width and height:
281```js
282const foundation = new MDCRippleFoundation({
283 // ...
284 computeBoundingRect: () => {
285 const {left, top} = element.getBoundingClientRect();
286 const dim = 24;
287 return {
288 left,
289 top,
290 width: dim,
291 height: dim,
292 right: left + dim,
293 bottom: top + dim
294 };
295 }
296});
297this.ripple = new MDCRipple(this.root, foundation);
298```
299
300### The util API
301
302External frameworks and libraries can use the following utility methods when integrating a component.
303
304Method Signature | Description
305--- | ---
306`util.supportsCssVariables(windowObj, forceRefresh = false) => Boolean` | Determine whether the current browser supports CSS variables (custom properties)
307`util.getNormalizedEventCoords(ev, pageOffset, clientRect) => object` | Determines X/Y coordinates of an event normalized for touch events and ripples
308
309> _NOTE_: The function `util.supportsCssVariables` cache its results; `forceRefresh` will force recomputation, but is used mainly for testing and should not be necessary in normal use.
310
311## Caveats
312
313### Caveat: Safari 9
314
315> TL;DR ripples are disabled in Safari 9 because of a bug with CSS variables.
316
317The ripple works by updating CSS variables used by pseudo-elements. Unfortunately, in Safari 9.1, there is a bug where updating a CSS variable on an element will _not_ trigger a style recalculation on that element's pseudo-elements (try out [this codepen](http://codepen.io/traviskaufman/pen/jARYOR) in Chrome, and then in Safari 9.1 to see the issue). Webkit builds which have this bug fixed (e.g. the builds used in Safari 10+)
318support [CSS 4 Hex Notation](https://drafts.csswg.org/css-color/#hex-notation) while those without the fix don't. We feature-detect whether we are working with a WebKit build that can handle our usage of CSS variables.
319
320### Caveat: Mobile Safari
321
322> TL;DR for CSS-only ripple styles to work as intended, register a `touchstart` event handler on the affected element or its ancestor.
323
324Mobile Safari does not trigger `:active` styles noticeably by default, as
325[documented](https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html#//apple_ref/doc/uid/TP40006510-SW5)
326in the Safari Web Content Guide. This effectively suppresses the intended pressed state styles for CSS-only ripple surfaces. This behavior can be remedied by registering a `touchstart` event handler on the element, or on any common ancestor of the desired elements.
327
328See [this StackOverflow answer](https://stackoverflow.com/a/33681490) for additional information on mobile Safari's behavior.