1 |
|
2 | title: "Ripples"
|
3 | layout: detail
|
4 | section: components
|
5 | excerpt: "Ink ripple touch feedback effect."
|
6 | iconId: ripple
|
7 | path: /catalog/ripples/
|
8 | -->
|
9 |
|
10 | # Ripple
|
11 |
|
12 | MDC 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 |
|
14 | MDC 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 | ```
|
30 | npm install @material/ripple
|
31 | ```
|
32 |
|
33 | ## Usage
|
34 |
|
35 | A 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 |
|
37 | A 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 |
|
41 | CSS 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 |
|
49 | In 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 |
|
76 | These APIs use pseudo-elements for the ripple effect: `::before` for the background, and `::after` for the foreground.
|
77 |
|
78 | #### Ripple Mixins
|
79 |
|
80 | Mixin | 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 |
|
90 | Mixin | 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 |
|
104 | When using the advanced states mixins instead of the basic states mixins, every one of the mixins below should be included at least once.
|
105 |
|
106 | These 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 |
|
109 | Mixin | 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 |
|
120 | Function | 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 |
|
126 | The `MDCRipple` JavaScript component allows for programmatic activation / deactivation of the ripple, for interdependent interaction between
|
127 | components. For example, this is used for making form field labels trigger the ripples in their corresponding input elements.
|
128 |
|
129 | To 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
|
132 | const surface = document.querySelector('.my-surface');
|
133 | const ripple = new MDCRipple(surface);
|
134 | ```
|
135 |
|
136 | You can also use `attachTo()` as an alias if you don't care about retaining a reference to the
|
137 | ripple.
|
138 |
|
139 | ```javascript
|
140 | MDCRipple.attachTo(document.querySelector('.my-surface'));
|
141 | ```
|
142 |
|
143 | Property | 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 |
|
148 | Method 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 |
|
181 | Method 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 |
|
192 | Usually, 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 |
|
203 | You 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 |
|
207 | Set the `unbounded` property on the `MDCRipple` component.
|
208 |
|
209 | ```javascript
|
210 | const ripple = new MDCRipple(root);
|
211 | ripple.unbounded = true;
|
212 | ```
|
213 |
|
214 | #### Using DOM
|
215 |
|
216 | Add 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 |
|
226 | Usually, you'll want to use `MDCRipple` _along_ with the component for the actual UI element you're trying to add a
|
227 | ripple to. `MDCRipple` has a static `createAdapter(instance)` method that can be used to instantiate a ripple within
|
228 | any `MDCComponent` that requires custom adapter functionality.
|
229 |
|
230 | ```ts
|
231 | class 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 |
|
245 | Different 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 |
|
249 | To 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
|
252 | class 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 |
|
278 | If 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 |
|
280 | For example, if you know an icon font sizes its elements to `24px` width and height:
|
281 | ```js
|
282 | const 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 | });
|
297 | this.ripple = new MDCRipple(this.root, foundation);
|
298 | ```
|
299 |
|
300 | ### The util API
|
301 |
|
302 | External frameworks and libraries can use the following utility methods when integrating a component.
|
303 |
|
304 | Method 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 |
|
317 | The 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+)
|
318 | support [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 |
|
324 | Mobile 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)
|
326 | in 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 |
|
328 | See [this StackOverflow answer](https://stackoverflow.com/a/33681490) for additional information on mobile Safari's behavior.
|