touch – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Sun, 13 Dec 2020 23:49:16 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 touch – CSS-Tricks https://css-tricks.com 32 32 45537868 Interaction Media Features and Their Potential (for Incorrect Assumptions) https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/ https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/#comments Mon, 14 Sep 2020 14:43:56 +0000 https://css-tricks.com/?p=320427 The Media Queries Level 4 Interaction Media Features — pointer, hover, any-pointer and any-hover — are meant to allow sites to implement different styles and functionality (either CSS-specific interactivity like :hover, or JavaScript behaviors, when queried using window.matchMedia), depending on the particular characteristics of a user’s input device.


Interaction Media Features and Their Potential (for Incorrect Assumptions) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This is an updated and greatly expanded version of the article originally published on dev.opera back in 2015. That article referenced the Editor’s Draft, 24 March 2015 of the specification Media Queries Level 4, and contained a fairly big misunderstanding about how any-hover:none would end up being evaluated by browsers in practice.

The spec has since been updated (including clarifications and examples that I submitted following the publication of the original article), so this updated version removes the incorrect information of the original and brings the explanations in line with the most recent working draft. It also covers additional aspects relating to JavaScript touch/input detection.

The Media Queries Level 4 Interaction Media Featurespointer, hover, any-pointer and any-hover — are meant to allow sites to implement different styles and functionality (either CSS-specific interactivity like :hover, or JavaScript behaviors, when queried using window.matchMedia), depending on the particular characteristics of a user’s input devices.

Although the specification is still in working draft, interaction media features are generally well supported, though, to date, there are still some issues and inconsistencies in the various browser implementations — see the recent pointer/hover/any-pointer/any-hover test results, with references to relevant browser bugs.

Common use cases cited for interaction media features are often “make controls bigger/smaller depending on whether the users has a touchscreen device or is using a mouse/stylus” and “only use a CSS dropdown menu if the user has an input that allows hover-based interactions.”

@media (pointer: fine) {
  /* using a mouse or stylus - ok to use small buttons/controls */
}
@media (pointer: coarse) {
  /* using touch - make buttons and other "touch targets" bigger */
}
@media (hover: hover) {
  /* ok to use :hover-based menus */
}
@media (hover: none) {
  /* don't use :hover-based menus */
}

There are also examples of developers using these new interaction media features as a way of achieving standards-based “touch detection,” often just for listening to touch events when the device is identified as having a coarse pointer.

if (window.matchMedia && window.matchMedia("(pointer:coarse)").matches) {
  /* if the pointer is coarse, listen to touch events */
  target.addEventListener("touchstart", ...);
  // ...
} else {
  /* otherwise, listen to mouse and keyboard events */
  // ...
}

However, these approaches are slightly naive, and stem from a misunderstanding of what these interaction media queries are designed to tell us.

What’s the primary input?

One of the limitations of pointer and hover is that, by design, they only expose the characteristics of what a browser deems to be the primary pointer input. What the browser thinks, and what a user is actually using as their primary input, may differ — particularly now that the lines between devices, and the types of inputs they support, is becoming more and more blurry.

Microsoft Surface with a keyboard, trackpad, external bluetooth mouse, touchscreen.
Which one’s the “primary” input? the answer may depend on the activity.

Right out of the gate, it’s worth noting that interaction media features only cover pointer inputs (mouse, stylus, touchscreen). They don’t provide any way of detecting if a user’s primary input is a keyboard or keyboard-like interface, such as a switch control. In theory, for a keyboard user, a browser could report pointer: none, signaling that the user’s primary input is not a pointer at all. However, in practice, no browser offers a way for users to specify that they are in fact keyboard users. So keep in mind that, regardless of what the interaction media feature queries may return, it’s worth making sure that your site or app also works for keyboard users.

Traditionally, we could say that a phone or tablet’s primary input is the touchscreen. However, even on these devices, a user may have an additional input, like a paired bluetooth mouse (a feature that has been available for years on Android, is now supported in iPadOS, and is sure to land in iOS), that they are using as their primary input.

An Android phone with a paired bluetooth keyboard and mouse, with the screen showing an actual mouse pointer and right-click context menu in Chrome
An iPad with a paired bluetooth keyboard, mouse, and Apple Pencil, with the screen showing the mouse “dot” and right-click context menu in Safari

In this case, while the device nominally has pointer: coarse and hover: none, users may actually be using a fine pointer device that is capable of hovers. Similarly, if a user has a stylus (like the Apple Pencil), their primary input may still be reported as the touchscreen, but rather than pointer: coarse, they now have an input that can provide fine pointer accuracy.

In these particular scenarios, if all the site is doing is making buttons and controls bigger and avoiding hover-based interactions, that would not be a major problem for the user: despite using a fine and hover-capable mouse, or a fine but still not hover-capable stylus, they will get styling and functionality aimed at the coarse, non-hover-capable touchscreen.

If the site is using the cues from pointer: coarse for more drastic changes, such as then only listening to touch events, then that will be problematic for users — see the section about incorrect assumptions that can completely break the experience.

However, consider the opposite: a “regular” desktop or laptop with a touchscreen, like Microsoft’s Surface. In most cases, the primary input will be the trackpad/mouse — with pointer:fine and hover:hover — but the user may well be using the touchscreen, which has coarse pointer accuracy and does not have hover capability. If styling and functionality are then tailored specifically to rely on the characteristics of the trackpad/mouse, the user may find it problematic or impossible to use the coarse, non-hover-capable touchscreen.

FeatureTouchscreenTouchscreen + MouseDesktop/LaptopDesktop/Laptop + Touchscreen
pointer:coarsetruetruefalsefalse
pointer:finefalsefalsetruetrue
hover:nonetruetruefalsefalse
hover:hoverfalsefalsetruetrue

For a similar take on this problem, see ”The Good & Bad of Level 4 Media Queries” by Stu Cox. While it refers to an even earlier iteration of the spec that only contained pointer and hover and a requirement for these features to report the least capable, rather than the primary, input device.

The problem with the original pointer and hover on their own is that they don’t account for multi-input scenarios, and they rely on the browser to be able to correctly pick a single primary input. That’s where any-pointer and any-hover come into play.

Testing the capabilities of all inputs

Instead of focusing purely on the primary pointer input, any-pointer and any-hover report the combined capabilities of all available pointer inputs.

In order to support multi-input scenarios, where different (pointer-based) inputs may have different characteristics, more than one of the values for any-pointer (and, theoretically, any-hover, but this aspect is useless as we’ll see later) can match, if different input devices have different characteristics< (compared to pointer and hover, which only ever refer to the capabilities of the primary pointer input). In current implementations, these media features generally evaluate as follows:

FeatureTouchscreenTouchscreen + MouseDesktop/LaptopDesktop/Laptop + Touchscreen
any-pointer:coarsetruetruefalsetrue
any-pointer:finefalsetruetruetrue
any-hover:nonetruefalsefalsefalse
any-hover:hoverfalsetruetruetrue
Comparison of Firefox on Android’s media query results with just the touchscreen, and when adding a bluetooth mouse. Note how pointer and hover remain the same, but any-pointer and any-hover change to cover the new hover-capable fine input.

Going back to the original use cases for the interaction media features, instead of basing our decision to provide larger or smaller inputs or to enable hover-based functionality only on the characteristics of the primary pointer input, we can make that decision based on the characteristics of any available pointer inputs. Roughly translated, instead of saying “make all controls bigger if the primary input has pointer: coarse” or “only offer a CSS menu if the primary input has hover: hover,” we can build media queries that equate to saying, “if any of the pointer inputs is coarse, make the controls bigger” and “only offer a hover-based menu if at least one of the pointer inputs available to the user is hover-capable.”

@media (any-pointer: coarse) {
  /* at least one of the pointer inputs
    is coarse, best to make buttons and 
    other "touch targets" bigger (using 
    the query "defensively" to target 
    the least capable input) */
}
@media (any-hover: hover) {
  /* at least one of the inputs is 
     hover-capable, so it's at least 
     possible for users to trigger
     hover-based menus */
}

Due to the way that any-pointer and any-hover are currently defined (as “the union of capabilities of all pointing devices available to the user”), any-pointer: none will only ever evaluate to true if there are no pointer inputs available, and, more crucially, any-hover: none will only ever be true if none of the pointer inputs present are hover-capable. Particularly for the latter, it’s therefore not possible to use the any-hover: none query to determine if only one or more of the pointer inputs present is not hover-capable — we can only use this media feature query to determine whether or not all inputs are not hover-capable, which is something that can just as well be achieved by checking if any-hover: hover evaluates to false. This makes the any-hover: none query essentially redundant.

We could work around this by inferring that if any-pointer: coarse is true, it’s likely a touchscreen, and generally those inputs are not hover-capable, but conceptually, we’re making assumptions here, and the moment there’s a coarse pointer that is also hover-capable, that logic falls apart. (And for those doubting that we may ever see a touchscreen with hover, remember that some devices, like the Samsung Galaxy Note and Microsoft’s Surface, have a hover-capable stylus that is detected even when it’s not touching the digitizer/screen, so some form of “hovering touch” detection may not be out of the question in the future.)

Combining queries for more educated guesses

The information provided by any-pointer and any-hover can of course be combined with pointer and hover, as well as the browser’s determination of what the primary input is capable of, for some slightly more nuanced assessments.

@media (pointer: coarse) and (any-pointer: fine) {
  /* the primary input is a touchscreen, but
     there is also a fine input (a mouse or 
     perhaps stylus) present. Make the design
     touch-first, mouse/stylus users can
     still use this just fine (though it may 
     feel a big clunky for them?) */
}
@media (pointer: fine) and (any-pointer: coarse) {
  /* the primary input is a mouse/stylus,
     but there is also a touchscreen 
     present. May be safest to make 
     controls big, just in case users do 
     actually use the touchscreen? */
}
@media (hover: none) and (any-hover: hover) {
  /* the primary input can't hover, but
     the user has at least one other
     input available that would let them
     hover. Do you trust that the primary
     input is in fact what the user is 
     more likely to use, and omit hover-
     based interactions? Or treat hover 
     as something optional — can be 
     used (e.g. to provide shortcuts) to 
     users that do use the mouse, but 
     don't rely on it? */
}

Dynamic changes

Per the specification, browsers should re-evaluate media queries in response to changes in the user environment. This means that pointer, hover, any-pointer, and any-hover interaction media features can change dynamically at any point. For instance, adding/removing a bluetooth mouse on a mobile/tablet device will trigger a change in any-pointer / any-hover. A more drastic example would be a Surface tablet, where adding/removing the device’s “type cover” (which includes a keyboard and trackpad) will result in changes to the primary input itself (going from pointer: fine / hover: hover when the cover is present, to pointer: coarse / hover: none when the Surface is in “tablet mode”).

Screenshots of Firefox on a Surface tablet. With the cover attached, pointer:finehover:hoverany-pointer:coarseany-pointer:fine, and any-hover:hover are true; once the cover is removed (and Windows asks if the user wants to switch to “tablet mode”), touch becomes the primary input with pointer:coarse and hover:none, and only any-pointer:coarse and any-hover:none are true.

If you’re modifying your site’s layout/functionality based on these media features, be aware that the site may suddenly change “under the user’s feet” whenever the inputs change — not just when the page/site is first loaded.

Media queries may not be enough — roll on scripting

The fundamental shortcoming of the interaction media features is that they won’t necessarily tell us anything about the input devices that are in use right now. For that, we may need to dig deeper into solutions, like What Input?, that keep track of the specific JavaScript events fired. But of course, those solutions can only give us information about the user’s input after they have already started interacting with the site — at which point it may be too late to make drastic changes to your layout or functionality.

Keep in mind that even these JavaScript-based approaches can just as easily lead to incorrect results. That’s especially true on mobile/tablet platforms, or in situations where assistive technologies are involved, where it is common to see “faked” events being generated. For instance, if we look over the series of events fired when activating a control on desktop using a keyboard and screen reader, we can see that fake mouse events are triggered. Assistive technologies do this because, historically, a lot of web content has been coded to work for mouse users, but not necessarily for keyboard users, making a simulation of those interactions necessary for some functionalities.

Similarly, when activating “Full Keyboard Support” in iOS’s Settings → Accessibility → Keyboard, it’s possible for users to navigate web content using an external bluetooth keyboard, just as they would on desktop. But if we look at the event sequence for mobile/tablet devices and paired keyboard/mouse, that situation produces pointer events, touch events, and fallback mouse events — the same sequence we’d get for a touchscreen interaction.

Showing iOS settings with Full Keyboard Access enabled on the left and an iPhone browser window open to the right with the What Input tool.
When enabled, iOS’s “Full Keyboard Access” setting results in pointer, touch, and mouse events. What Input? identifies this as a touch input

In all these situations, scripts like What Input? will — understandably, through no fault of its own — misidentify the current input type.

Incorrect assumptions that can completely break the experience

Having outlined the complexity of multi-input devices, it should be clear by now that approaches that only listen to specific types of events, like the form of “touch detection” we see commonly in use, quickly fall apart.

if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) {
  /* if the pointer is coarse, listen to touch events */
  target.addEventListener("touchstart", ...);
  // ...
} else {
  /* otherwise, listen to mouse and keyboard events */
  target.addEventListener("click", ...);
  // ...
}

In the case of a “touch” device with additional inputs — such as a mobile or tablet with an external mouse — this code will essentially prevent the user from being able to use anything other than their touchscreen. And on devices that are primarily mouse-driven but do have a secondary touchscreen interface — like a Microsoft Surface — the user will be unable to use their touchscreen.

Instead of thinking about this as “touch or mouse/keyboard,” realize that it’s often a case of “touch and mouse/keyboard.” If we only want to register touch events when there’s an actual touchscreen device for performance reasons, we can try detecting any-pointer: coarse. But we should also keep other regular event listeners for mouse and keyboard.

/* always, as a matter of course, listen to mouse and keyboard events */
target.addEventListener("click", ...);
 // ...

if (window.matchMedia && window.matchMedia("(any-pointer: coarse)").matches) {
  /* if there's a coarse pointer, *also* listen to touch events */
  target.addEventListener("touchstart", ...);
  // ...
}

Alternatively, we could avoid this entire conundrum about different types of events by using pointer events, which cover all types of pointer inputs in a single, unified event model, and are fairly well supported.

Give users an explicit choice

One potential solution for neatly circumventing our inability to make absolute determinations about which type of input the users are using may be to use the information provided by media queries and tools like What Input?, not to immediately switch between different layouts/functionalities — or worse, to only listen to particular types of events, and potentially locking out any additional input types — but to use them only as signals for when to provide users with an explicit way to switch modes.

For instance, see the way Microsoft Office lets you change between “Touch” and “Mouse” mode. On touch devices, this option is shown by default in the application’s toolbar, while on non-touch devices, it’s initially hidden (though it can be enabled, regardless of whether or not a touchscreen is present).

Screenshot of Microsoft Office's 'Touch/Mouse mode' dropdown, and a comparison of (part of) the toolbar as it's presented in each mode

A site or web application could take the same approach, and even set the default based on what the primary input is — but still allow users to explicitly change modes. And, using an approach similar to What Input?, the site could detect the first appearance of a touch-based input, and alert/prompt the user if they want to switch to a touch-friendly mode.

Potential for incorrect assumptions — query responsibly

Using Media Queries Level 4 Interaction Media Features and adapting our sites based on the characteristics of the available primary or additional pointer input is a great idea — but beware false assumptions about what these media features actually say. As with similar feature detection methods, developers need to be aware of what exactly they’re trying to detect, the limitations of that particular detection, and most importantly, consider why they are doing it — in a similar way to the problem I outlined in my article on detecting touch.

pointer and hover tell us about the capabilities of whatever the browser determines to be the primary device input. any-pointer and any-hover tell you about the capabilities of all connected inputs, and combined with information about the primary pointer input, they allow us to make educated guesses about a user’s particular device/scenario. We can use these features to inform our layout, or the type of interaction/functionality we want to offer; but don’t discount the possibility that those assumptions may be incorrect. The media queries themselves are not necessarily flawed (though the fact that most browsers seem to still have quirks and bugs adds to the potential problems). It just depends on how they’re used.

With that, I want to conclude by offering suggestions to “defend” yourself from the pitfalls of input detections.

? Don’t

Assume a single input type. It’s not “touch or mouse/keyboard” these days, but “touch and mouse/keyboard” — and the available input types may change at any moment, even after the initial page load.

Just go by pointer and hover. the “primary” pointer input is not necessarily the one that your users are using.

Rely on hover in general. Regardless of what hover or any-hover suggest, your users may have a pointer input that they’re currently using that is not hover-capable, and you can’t currently detect this unless it’s the primary input (since hover: none  is true if that particular input lacks hover, but any-hover: none will only ever be true if none of the inputs are hover-capable). And remember that hover-based interfaces generally don’t work for keyboard users.

? Do

Make your interfaces “touch-friendly.” If you detect that there’s an any-pointer:coarse input (most likely a touchscreen), consider providing large touch targets and sufficient spacing between them. Even if the user is using another input, like a mouse, at that moment, no harm done.

Give users a choice. If all else fails, consider giving the user an option/toggle to switch between touch or mouse layouts. Feel free to use any information you can glean from the media queries (such as any-pointer: coarse being true) to make an educated guess about the toggle’s initial setting.

Remember about keyboard users. Regardless of any pointer inputs that the user may or may not be using, don’t forget about keyboard accessibility — it can’t be conclusively detected, so just make sure your stuff works for keyboard users as a matter of course.


Interaction Media Features and Their Potential (for Incorrect Assumptions) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/feed/ 3 320427
touch-action https://css-tricks.com/almanac/properties/t/touch-action/ Wed, 02 Jan 2019 15:18:55 +0000 http://css-tricks.com/?page_id=280309 The touch-action property in CSS gives you control over the effect of touchscreen interactions with an element, similar to the more widely-used pointer-events property used to control mouse interactions.

#element {
  touch-action: pan-right pinch-zoom;
}

The touch-action property is useful …


touch-action originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The touch-action property in CSS gives you control over the effect of touchscreen interactions with an element, similar to the more widely-used pointer-events property used to control mouse interactions.

#element {
  touch-action: pan-right pinch-zoom;
}

The touch-action property is useful primarily for interactive UI elements that need slightly different behavior depending on the type of device being used. When your users might expect an element to behave a particular way with a mouse, and slightly different behavior on a touch screen, touch-action will come in handy.

The most obvious example of this is an interactive map element. If you’ve ever viewed a map not designed to work with touch devices, you’ve probably tried to pinch and zoom only to find the browser magnifying the element, but not actually zooming the actual map.

By default, a browser will handle touch interactions automatically: Pinch to zoom, swipe to scroll, etc. Setting touch-action to none will disable all browser handling of these events, leaving them up to you to implement (via JavaScript). If you only want to take over one interaction, tell the browser to handle the rest. For example, if you wrote some JavaScript that only handles zooming, you can tell the browser to handle everything else with this property: touch-action: pan-x pan-y;.

See the Pen examples of touch-action by CSS-Tricks (@css-tricks) on CodePen.

Syntax

touch-action: auto | none | [ [ pan-x | pan-left | pan-right ] || [ pan-y | pan-up | pan-down ] || pinch-zoom ] | manipulation

Values

The touch-action property accepts the following values:

  • auto: Allows the browser to handle all pan and zoom interactions.
  • none: Disables browsers from handling all pan and zoom interactions. This opens up the ability to custom define those interactions in JavaScript.
  • pan-x: Enables horizontal panning with a single finger interaction. It is shorthand for the pan-left and pan-right values, but can be used in combination with pan-y, pan-up, pan-down and pinch-zoom.
  • pan-y Enables vertical panning with a single finger interaction. It is shorthand for the pan-up and pan-down values, but can be used in combination with pan-x, pan-left, pan-right and pinch-zoom.
  • manipulation: Enables pinch and zoom interactions, but disables others you might find on some devices, like double-tap to zoom. It is shorthand for the combination of pan-x pan-y pinch-zoom.
  • pan-left (Experimental): Enables a single finger interaction when a user’s finger moves to the right, which pans toward the left.
  • pan-right (Experimental): Enables a single finger interaction when a user’s finger moves to the left, which pans toward the right.
  • pan-down (Experimental): Enables a single finger interaction when a user’s finger moves up, which pans downward.
  • pan-up (Experimental): Enables a single finger interaction when a user’s finger moves down, which pans upward.
  • pinch-zoom: Enables multi-finger interactions and can be combined with any other pan- value.

Related

Browser Support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
365210*12No

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
10810710813.0-13.1

Safari is the glaring omission to touch-action support. iOs Safari has limited support, only for the auto and manipulation values.

Additional Information


touch-action originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
280309
Creating an Animated Login Form for TouchID https://css-tricks.com/creating-an-animated-login-form-for-touchid/ https://css-tricks.com/creating-an-animated-login-form-for-touchid/#comments Tue, 11 Dec 2018 15:07:20 +0000 http://css-tricks.com/?p=279803 I came across this amazing Dribbble shot by Jakub Reis a while back. It caught my eye and I knew that I just had to try recreating it in code. At that moment, I didn’t know how. I tried out …


Creating an Animated Login Form for TouchID originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I came across this amazing Dribbble shot by Jakub Reis a while back. It caught my eye and I knew that I just had to try recreating it in code. At that moment, I didn’t know how. I tried out a bunch of different things, and about a year later, I finally managed to make this demo.

I learned a couple of things along the way, so let me take you on a little journey of what I did to make this because you may learn a thing or two as well.

See the Pen Opening screen for a banking app by Kirill Kiyutin (@kiyutink) on CodePen.

Step 1: Split the work into parts

I watched the original GIF many times. My goal was to split the animation into small, digestible chunks and I was able to break it down like this:

I know, it looks a lot — but we can do this!

Step 2: Take the original demo apart frame-by-frame

I needed to extract as much info as I could out of the original GIF to have a good understanding of the animation, so I split it up into single frames. There actually are a lot of services that can do this for us. I used one at ezgif.com but it could have just as easily been something else. Either way, this enables us to get details such as the colors, sizes, and proportions of all the different elements we need to create.

Oh, and we still need to turn the fingerprint into an SVG. Again, there are plenty of apps that will help us here. I used Adobe Illustrator to trace the fingerprint with the pen tool to get this set of paths:

See the Pen css-t. paths by Kirill Kiyutin (@kiyutink) on CodePen.

We’ll go through the same process with the line chart that appears towards the end of the animation, so might as well keep that vector editor open. 🙂

Step 3: Implement the animations

I’ll explain how the animations work in the final pen, but you can also find some of the unsuccessful approaches I took along the way in the end of the article.

I’ll focus on the important parts here and you can refer to the demos for the full code.

Filling the fingerprint

Let’s create the HTML structure of the phone screen and the fingerprint.

<div class="demo">
  <div class="demo__screen demo__screen--clickable">
    <svg class="demo__fprint" viewBox="0 0 180 320">  
      <!-- removes-forwards and removes-backwards classes will be helpful later on -->
      <path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M46.1,214.3c0,0-4.7-15.6,4.1-33.3"/>
      <path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M53.5,176.8c0,0,18.2-30.3,57.5-13.7"/>
      <path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M115.8,166.5c0,0,19.1,8.7,19.6,38.4"/>
      <!-- ... and about 20 more paths like this -->
    </svg>
  

The styles are quite simple so far. Note that I am using Sass throughout the demo — I find that it helps keep the work clean and helps with some of the heavier lifting we need to do.

// I use a $scale variable to quickly change the scaling of the whole pen, so I can focus on the animation and decide on the size later on.
$scale: 1.65;
$purplish-color: #8742cc;
$pinkish-color: #a94a8c;
$bg-color: #372546;

// The main container
.demo {
  background: linear-gradient(45deg, lighten($pinkish-color, 10%), lighten($purplish-color, 10%));
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 0;
  user-select: none;
  overflow: hidden;
  position: relative;
  
  // The screen that holds the login component
  &__screen {
    position: relative;
    background-color: $bg-color;
    overflow: hidden;
    flex-shrink: 0;
    &--clickable {
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
    }
  }
  
  // Styles the fingerprint SVG paths
  &__fprint-path {
    stroke-width: 2.5px;
    stroke-linecap: round;
    fill: none;
    stroke: white;
    visibility: hidden;
    transition: opacity 0.5s ease;
    
    &--pinkish {
      stroke: $pinkish-color;
    }
    
    &--purplish {
      stroke: $purplish-color;
    }    
  }
  
  // Sizes positions the fingerprint SVG
  &__fprint {
    width: 180px * $scale;
    height: 320px * $scale;
    position: relative;
    top: 20px * $scale;
    overflow: visible;
    // This is going to serve as background to show "unfilled" paths. we're gonna remove it at the moment where the filling animation is over
    background-image: url('https://kiyutink.github.io/svg/fprintBackground.svg');
    background-size: cover;
    
    &--no-bg {
      background-image: none;
    }
  }
}

Now the hard part: making the fingerprint interactive. You can read about the animation of SVG lines here. That’s the method we’ll use to fill in each individual path.

Let’s create a class that describes a path element so that it’s easier to manipulate the paths later on.

class Path {
  constructor(selector, index) {
    this.index = index;
    this.querySelection = document.querySelectorAll(selector)[index];
    this.length = this.querySelection.getTotalLength();
    this.$ = $(selector).eq(index);
    this.setDasharray();
    this.removesForwards = this.$.hasClass('demo__fprint-path--removes-forwards');
  }
  
  setDasharray() {
    this.$.css('stroke-dasharray', `${this.length} ${this.length + 2}`);
    return this;
  }
  
  offset(ratio) {
    this.$.css('stroke-dashoffset', -this.length * ratio + 1);
    return this;
  }
  
  makeVisible() {
    this.$.css('visibility', 'visible');
    return this;
  }
}

The general idea is this: Create an instance of this class for each path that we have in the fingerprint, and modify them in every frame. The paths will start with an offset ratio of -1 (fully invisible) and then will increase the offset ratio (which we’ll refer to as “offset” from here on) by a constant value each frame until they get to 0 (fully visible). The filling animation will be over at this point.

If you’ve never animated anything with this frame-by-frame approach, here’s a very simple demo to help understand how this works:

See the Pen 60fps raf animation proof of concept by Kirill Kiyutin (@kiyutink) on CodePen.

We should also handle the case where the user stops tapping or pressing the mouse button. In this case, we will animate in the opposite direction (subtracting a constant value from the offset each frame until it gets to -1 again).

Let’s create the function that calculates the offset increment for every frame — this’ll be useful later on.

function getPropertyIncrement(startValue, endValue, transitionDuration) {
  // We animate at 60 fps
  const TICK_TIME = 1000 / 60;
  const ticksToComplete = transitionDuration / TICK_TIME;
  return (endValue - startValue) / ticksToComplete;
}

Now it’s time to animate! We will keep the fingerprint paths in a single array:

let fprintPaths = [];

// We create an instance of Path for every existing path. 
// We don't want the paths to be visible at first and then 
// disappear after the JavaScript runs, so we set them to 
// be invisible in CSS. That way we can offset them first 
// and then make them visible.
for (let i = 0; i < $(fprintPathSelector).length; i++) {
  fprintPaths.push(new Path(fprintPathSelector, i));
  fprintPaths[i].offset(-1).makeVisible();
}

We will go through that array for each frame in the animation, animating the paths one by one:

let fprintTick = getPropertyIncrement(0, 1, TIME_TO_FILL_FPRINT);

function fprintFrame(timestamp) {
  
  // We don't want to paint if less than 1000 / 65 ms elapsed 
  // since the last frame (because there are faster screens 
  // out there and we want the animation to look the same on 
  // all devices). We use 65 instead of 60 because, even on 
  // 60 Hz screens, `requestAnimationFrame` can sometimes be called 
  // a little sooner, which can result in a skipped frame.
  if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
    lastRafCallTimestamp = timestamp;
    curFprintPathsOffset += fprintTick * fprintProgressionDirection;
    offsetAllFprintPaths(curFprintPathsOffset);
  }
  
  // Schedule the next frame if the animation isn't over
  if (curFprintPathsOffset >= -1 && curFprintPathsOffset <= 0) {
    isFprintAnimationInProgress = true;
    window.requestAnimationFrame(fprintFrame);
  }
  
  // The animation is over. We can schedule next animation steps
  else if (curFprintPathsOffset > 0) {
    curFprintPathsOffset = 0;
    offsetAllFprintPaths(curFprintPathsOffset);
    isFprintAnimationInProgress = false;
    isFprintAnimationOver = true;
    // Remove the background with grey paths
    $fprint.addClass('demo__fprint--no-bg');
    // Schedule the next animation step - transforming one of the paths into a string 
    // (this function is not implemented at this step yet, but we'll do that soon)
    startElasticAnimation();
    // Schedule the fingerprint removal (removeFprint function will be implemented in the next section)
    window.requestAnimationFrame(removeFprint);
  }
  // The fingerprint is back to the original state (the user has stopped holding the mouse down)
  else if (curFprintPathsOffset < -1) {
    curFprintPathsOffset = -1;
    offsetAllFprintPaths(curFprintPathsOffset);
    isFprintAnimationInProgress = false;
  }
}

And we’ll attach some event listeners to the demo:

$screen.on('mousedown touchstart', function() {
  fprintProgressionDirection = 1;
  // If the animation is already in progress,
  // we don't schedule the next frame since it's 
  // already scheduled in the `fprintFrame`. Also, 
  // we obviously don't schedule it if the animation 
  // is already over. That's why we have two separate 
  // flags for these conditions.
  if (!isFprintAnimationInProgress && !isFprintAnimationOver)
    window.requestAnimationFrame(fprintFrame);
})

// On `mouseup` / `touchend` we flip the animation direction
$(document).on('mouseup touchend', function() {
  fprintProgressionDirection = -1;
  if (!isFprintAnimationInProgress && !isFprintAnimationOver)
    window.requestAnimationFrame(fprintFrame);
})

…and now we should be done with the first step! Here’s how our work looks at this step:

See the Pen css-t. step 1 by Kirill Kiyutin (@kiyutink) on CodePen.

Removing the fingerprint

This part is pretty similar to the first one, only now we have to account for the fact that some of the paths remove in one direction and the rest of them in the other. That’s why we added the --removes-forwards modifier earlier.

First, we’ll have two additional arrays: one for the paths that are removed forwards and another one for the ones that are removed backwards:

const fprintPathsFirstHalf = [];
const fprintPathsSecondHalf = [];

for (let i = 0; i < $(fprintPathSelector).length; i++) {
  // ...
  if (fprintPaths[i].removesForwards)
    fprintPathsSecondHalf.push(fprintPaths[i]);
  else
    fprintPathsFirstHalf.push(fprintPaths[i]);
}

…and we’ll write a function that offsets them in the right direction:

function offsetFprintPathsByHalves(ratio) {    
  fprintPathsFirstHalf.forEach(path => path.offset(ratio));
  fprintPathsSecondHalf.forEach(path => path.offset(-ratio));
}

We’re also going to need a function that draws the frames:

function removeFprintFrame(timestamp) {
  // Drop the frame if we're faster than 65 fps
  if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
    curFprintPathsOffset += fprintTick * fprintProgressionDirection;
    offsetFprintPathsByHalves(curFprintPathsOffset);
    lastRafCallTimestamp = timestamp;
  }
  // Schedule the next frame if the animation isn't over
  if (curFprintPathsOffset >= -1)
    window.requestAnimationFrame(removeFprintFrame);
  else {
    // Due to the floating point errors, the final offset might be 
    // slightly less than -1, so if it exceeds that, we'll just 
    // assign -1 to it and animate one more frame
    curFprintPathsOffset = -1;
    offsetAllFprintPaths(curFprintPathsOffset);
  }
}

function removeFprint() {
  fprintProgressionDirection = -1;
  window.requestAnimationFrame(removeFprintFrame);
}

Now all that’s left is to call removeFprint when we’re done filling the fingerprint:

function fprintFrame(timestamp) {
  // ...
  else if (curFprintPathsOffset > 0) {
    // ...
    window.requestAnimationFrame(removeFprint);
  }
  // ...
}

Let’s check our work now:

See the Pen css-t. part 2 by Kirill Kiyutin (@kiyutink) on CodePen.

Animating the path ends

You can see that, as the fingerprint is almost removed, some of its paths are longer than they were in the beginning. I moved them into separate paths that start animating at the right moment. I could incorporate them into the existing paths, but it would be much harder and at 60fps would make next-to-no difference.

Let’s create them:

<path class="demo__ending-path demo__ending-path--pinkish" d="M48.4,220c-5.8,4.2-6.9,11.5-7.6,18.1c-0.8,6.7-0.9,14.9-9.9,12.4c-9.1-2.5-14.7-5.4-19.9-13.4c-3.4-5.2-0.4-12.3,2.3-17.2c3.2-5.9,6.8-13,14.5-11.6c3.5,0.6,7.7,3.4,4.5,7.1"/>
<!-- and 5 more paths like this -->

…and apply some basic styles:

&__ending-path {
  fill: none;
  stroke-width: 2.5px;
  stroke-dasharray: 60 1000;
  stroke-dashoffset: 61;
  stroke-linecap: round;
  will-change: stroke-dashoffset, stroke-dasharray, opacity;
  transform: translateZ(0);
  transition: stroke-dashoffset 1s ease, stroke-dasharray 0.5s linear, opacity 0.75s ease;
  
  &--removed {
    stroke-dashoffset: -130;
    stroke-dasharray: 5 1000;
  }
  
  &--transparent {
    opacity: 0;
  }
  
  &--pinkish {
    stroke: $pinkish-color;
  }
  
  &--purplish {
    stroke: $purplish-color;
  }
}

Now, we have to add the --removed modifier to flow these paths in at the right moment:

function removeFprint() {
  $endingPaths.addClass('demo__ending-path--removed');
  setTimeout(() => {
    $endingPaths.addClass('demo__ending-path--transparent');
  }, TIME_TO_REMOVE_FPRINT * 0.9);
  // ...
}

Now our work is really starting to take shape:

See the Pen css-t. part 3 by Kirill Kiyutin (@kiyutink) on CodePen.

Morphing the fingerprint

OK, I found this part to be really hard to do on my own, but it’s really easy to implement with GSAP’s morphSVG plugin.

Let’s create the invisible paths (well, a path and a line to be exact 🙂) that will be the keyframes for our string:

<line id='demo__straight-path' x1="0" y1="151.3" x2="180" y2="151.3"/>
<path class="demo__hidden-path" id='demo__arc-to-top' d="M0,148.4c62.3-13.5,122.3-13.5,180,0"/>

Then we’ll use morphSVG to transition the path in between the keyframes:

const $elasticPath = $('#demo__elastic-path');

const ELASTIC_TRANSITION_TIME_TO_STRAIGHT = 250;
const WOBBLE_TIME = 1000;

function startElasticAnimation() {
  $elasticPath.css('stroke-dasharray', 'none');
  const elasticAnimationTimeline = new TimelineLite();
  
  elasticAnimationTimeline
    .to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_STRAIGHT / 1000, {
      delay: TIME_TO_REMOVE_FPRINT / 1000 * 0.7,
      morphSVG: '#demo__arc-to-top'
    })
    .to('#demo__elastic-path', WOBBLE_TIME / 1000, {
      morphSVG: '#demo__straight-path',
      // I played with the easing a bit to get that "vibration" effect
      ease: Elastic.easeOut.config(1, 0.3)
    })
}

We’ll call this function inside the fprintFrame once the fingerprint is filled:

function fprintFrame(timestamp) {
  // ...
  else if (curFprintPathsOffset > 0) {
    // ...
    startElasticAnimation();
    // ...
  }
  // ...
}

The outcome is this:

See the Pen css-t. part 4 by Kirill Kiyutin (@kiyutink) on CodePen.

Animating the floating bullet

For this, I used some simple straightforward CSS animations. I chose the timing functions to emulate the gravity. You can play around with the timing functions here or here.

Let’s create a div:

<div class="demo__bullet"></div>

…and apply some styles to it:

&__bullet {
  position: absolute;
  width: 4px * $scale;
  height: 4px * $scale;
  background-color: white;
  border-radius: 50%;
  top: 210px * $scale;
  left: 88px * $scale;
  opacity: 0;
  transition: all 0.7s cubic-bezier(0.455, 0.030, 0.515, 0.955);
  will-change: transform, opacity;
  
  // This will be applied after the bullet has descended, to create a transparent "aura" around it
  &--with-aura {
    box-shadow: 0 0 0 3px * $scale rgba(255, 255, 255, 0.3);
  }
  // This will be applied to make the bullet go up
  &--elevated {
    transform: translate3d(0, -250px * $scale, 0);
    opacity: 1;
  }
  // This will be applied to make the bullet go down
  &--descended {
    transform: translate3d(0, 30px * $scale, 0);
    opacity: 1;
    transition: all 0.6s cubic-bezier(0.285, 0.210, 0.605, 0.910);
  }
}

Then we tie it together by adding and removing classes based on a user’s interactions:

const DELAY_TO_BULLET_AURA = 300;
const ELEVATION_TIME = 700;
const DELAY_AFTER_ELEVATION = 700;

const $bullet = $('.demo__bullet');

function elevateBullet() {
  $bullet.addClass('demo__bullet--elevated');
}

function descendBullet() {
  $bullet.addClass('demo__bullet--descended').removeClass('demo__bullet--elevated');
  animateBulletAura();
}
  
function animateBulletAura() {
  setTimeout(() => $bullet.addClass('demo__bullet--with-aura'), DELAY_TO_BULLET_AURA);
}

function animateBullet() {
  elevateBullet();
  $screen.removeClass('demo__screen--clickable');
  setTimeout(descendBullet, ELEVATION_TIME + DELAY_AFTER_ELEVATION);
}

Now, we need to call the animateBullet function:

function startElasticAnimation() {
  // ...
  animateBullet();
}

Here’s where we are at this point:

See the Pen css-t. part 5 by Kirill Kiyutin (@kiyutink) on CodePen.

Morphing the string into a graph

Now, let’s turn that string into a graph where the bullet can land. We’ll add another keyframe to the morphSVG animation.

<path class="demo__hidden-path" id='demo__curve' d="M0,140.2c13.1-10.5,34.7-17,48.5-4.1c5.5,5.2,7.6,12.1,9.2,19.2c2.4,10.5,4.3,21,7.2,31.4c2.4,8.6,4.3,19.6,10.4,26.7c4.3,5,17.7,13.4,23.1,4.8c5.9-9.4,6.8-22.5,9.7-33c4.9-17.8,13-14.6,15.7-14.6c1.8,0,9,2.3,15.4,5.4c6.2,3,11.9,7.7,17.9,11.2c7,4.1,16.5,9.2,22.8,6.6"/>

We add this keyframe into our timeline like this:

const DELAY_TO_CURVE = 350;
const ELASTIC_TRANSITION_TIME_TO_CURVED = 300;

function startElasticAnimation() {
  // ...
  elasticAnimationTimeline
    // ...
    .to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_CURVED / 1000, {
      delay: DELAY_TO_CURVE / 1000, 
      morphSVG: '#demo__curve'
    })
    // ...
}

Here’s what we get:

See the Pen css-t. part 6 by Kirill Kiyutin (@kiyutink) on CodePen.

Exploding the particles

This is a fun animation. First, we’ll create a couple of new divs that contain the particles that explode:

<div class="demo__logo-particles">
    <div class="demo__logo-particle"></div>
    <!-- and several more of these -->
</div>
<div class="demo__money-particles">
    <div class="demo__money-particle"></div>
    <!-- and several more of these -->
</div>

The two explosions are practically the same with the exception of a few parameters. That’s where SCSS mixins will come in handy. We can write the function once and use it on our divs.

@mixin particlesContainer($top) {
  position: absolute;
  width: 2px * $scale;
  height: 2px * $scale;
  left: 89px * $scale;
  top: $top * $scale;
  // We'll hide the whole container to not show the particles initially
  opacity: 0;

  &--visible {
    opacity: 1;
  }
}

// The $sweep parameter shows how far from the center (horizontally) the initial positions of the particles can be
@mixin particle($sweep, $time) {
  width: 1.5px * $scale;
  height: 1.5px * $scale;
  border-radius: 50%;
  background-color: white;
  opacity: 1;
  transition: all $time ease;
  position: absolute;
  will-change: transform;
  
  // Phones can't handle the particles very well :(
  @media (max-width: 400px) {
    display: none;
  }

  @for $i from 1 through 30 {
    &:nth-child(#{$i}) {
      left: (random($sweep) - $sweep / 2) * $scale + px;
      @if random(100) > 50 {
        background-color: $purplish-color;
      }
      @else {
        background-color: $pinkish-color;
      }
    }
    &--exploded:nth-child(#{$i}) {
      transform: translate3d((random(110) - 55) * $scale + px, random(35) * $scale + px, 0);
      opacity: 0;
    }
  }
}

Note the comment in the code that the particles don’t perform particularly well on less powerful devices such as phones. Perhaps there’s another approach here that would solve this if anyone has ideas and wants to chime in.

Alright, let’s put the mixins to use on the elements:

&__logo-particles {
  @include particlesContainer(15px);
}

&__logo-particle {
  @include particle(50, 1.7s);
}

&__money-particles {
  @include particlesContainer(100px);
}

&__money-particle {
  @include particle(100, 1.5s);
}

Now we add the classes to the divs at the right time in JavaScript:

const DELAY_TO_ANIMATE_MONEY_PARTICLES = 300;
const DELAY_TO_ANIMATE_LOGO_PARTICLES = 500;

const $moneyParticles = $('.demo__money-particle');
const $moneyParticlesContainer = $('.demo__money-particles');
const $logoParticlesContainer = $('.demo__logo-particles');
const $logoParticles = $('.demo__logo-particle');

function animateMoneyParticles() {
  setTimeout(() => {
    $moneyParticlesContainer.addClass('demo__money-particles--visible')
    $moneyParticles.addClass('demo__money-particle--exploded');
  }, DELAY_TO_ANIMATE_MONEY_PARTICLES);    
}

function animateLogoParticles() {
  setTimeout(() => {
    $logoParticlesContainer.addClass('demo__logo-particles--visible')
    $logoParticles.addClass('demo__logo-particle--exploded');
  }, DELAY_TO_ANIMATE_LOGO_PARTICLES);    
}

function elevateBullet() {
  // ...
  animateMoneyParticles();
  animateLogoParticles();
}

Here’s where we’re at:

See the Pen css-t. part 7 by Kirill Kiyutin (@kiyutink) on CodePen.

Animating the account balance

Every digit will have a few random numbers that we’ll scroll through:

<div class="demo__money">
  <div class="demo__money-currency">$</div>
  <!-- every digit will be a div like this one -->
  <div class="demo__money-digit">
    1
    2
    3
    4
    5
    6
    7
    8
    1
  </div>
  // ...
</div>

We will put different transition times on all of the digits so that the animations are staggered. We can use a SCSS loop for that:

&__money-digit {
  // ...
  // we start from 2 because the first child is the currency sign :)
  @for $i from 2 through 6 {
    &:nth-child(#{$i}) {
      transition: transform 0.1s * $i + 0.2s ease;
      transition-delay: 0.3s;
      transform: translate3d(0, -26px * $scale * 8, 0);
    }
    
    &--visible:nth-child(#{$i}) {
      transform: none;
    }
  }
}

All that’s left is to add the CSS classes at the right time:

const $money = $('.demo__money');
const $moneyDigits = $('.demo__money-digit');

function animateMoney() {
  $money.addClass('demo__money--visible');
  $moneyDigits.addClass('demo__money-digit--visible');
}

function descendBullet() {
  // ...
  animateMoney();
  // ...
}

Now sit back and marvel at our work:

See the Pen css-t. part 8 by Kirill Kiyutin (@kiyutink) on CodePen.

The rest of the animations are fairly simple and involve light CSS transitions, so I won’t get into them to keep things brief. You can see all of the final code in the completed demo.

View Demo

Some final words

  • In my early attempts I tried using CSS transitions for all of the animation work. I found it virtually impossible to control the progress and direction of the animation, so shortly I abandoned that idea and waited a month or so before starting again. In reality, if I knew back then that the Web Animations API was a thing, I would have tried to make use of it.
  • I tried making the explosion with Canvas for better performance (using this article as a reference), but I found it difficult to control the frame rate with two separate requestAnimationFrame chains. If you know how to do that, then maybe you can tell me in the comments (or write an article for CSS-Tricks 🙂).
  • After I got a first working prototype, I was really unhappy with its performance. I was hitting around 40-50fps on a PC, not to mention phones at all. I spent a lot of time optimizing the code and this article was a lot of help.
  • You can see that the graph has a gradient. I did that by declaring a gradient directly in the SVG defs block:
<defs>
  <linearGradient id="linear" x1="0%" y1="0%" x2="100%" y2="0%">
    <stop offset="0%"   stop-color="#8742cc"/>
    <stop offset="100%" stop-color="#a94a8c"/>
  </linearGradient>
</defs>

…and then applied it in the CSS properties:

fill: url(#linear);
stroke: url(#linear);

The whole process from start to finish — discovering the Dribbble shot and finishing the work — took me about a year. I was taking month-long breaks here and there either because I didn’t know how to approach a particular aspect or I simply didn’t have enough free time to work on it. The entire process was a really valuable experience and I learned a lot of new things along the way.

That being said, the biggest lesson to take away from this is that there’s no need to shy away from taking on an ambitious task, or feel discouraged if you don’t know how to approach it at first. The web is a big place and there is plenty of space to figure things out as you go along.


Creating an Animated Login Form for TouchID originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-an-animated-login-form-for-touchid/feed/ 5 279803
Touch Devices Should Not Be Judged By Their Size https://css-tricks.com/touch-devices-not-judged-size/ https://css-tricks.com/touch-devices-not-judged-size/#comments Mon, 27 Feb 2017 11:47:13 +0000 http://css-tricks.com/?p=251891 Front-end developers and web designers live in an insane multi-device reality.

A few months ago, the Red Hat UXD team discussed how to design enterprise applications for mobile environments. Sarah and Jenn, my talented colleagues, pointed out that touch …


Touch Devices Should Not Be Judged By Their Size originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Front-end developers and web designers live in an insane multi-device reality.

A few months ago, the Red Hat UXD team discussed how to design enterprise applications for mobile environments. Sarah and Jenn, my talented colleagues, pointed out that touch devices should not be judged by their size alone.

Assumptions are beguiling. If only we could agree on certain boundaries, then wouldn’t web design be so much easier to control?” – Jeremy Keith, Resilient Web Design

Today, there is a new layer of complexity to the already complicated world of interaction design and front-end development.

The hardware industry has created massive touchscreen TVs, really large tablets (like the iPad Pro), and even huge touch desktop PCs (like the new, jaw-dropping Surface Studio). This means we can no longer assume that a small viewport is a touch screen and a large viewport isn’t. Sometimes large screens are touch, requiring the user to use their finger, and small screens have a stylus.

Responsive viewport media queries are great but they’re not enough.

We can detect a touch screen with JS tools like Modernizr, but CSS has a hidden gem that is smarter and more flexible.

Interaction Media Features

Thanks to the W3C CSS Working Group and the CSS community, we have a cleaner solution.

On the Media Queries Level 4 Working Draft, there is a spec for Interaction Media Features that includes three definitions:

These provide the capability to query a document based on the presence and accuracy of the user’s pointing device and whether it has the ability to hover over elements.

Let’s take a closer look at each one:

Pointing Device Quality: The pointer Feature

The pointer media feature is used to query about the presence and accuracy of a pointing device such as a mouse. If a device has multiple input mechanisms, the pointer media feature must reflect the characteristics of the “primary” input mechanism, as determined by the user agent.” – W3C

The key word here is “accuracy” of the pointing device.

  • A mouse or a drawing stylus is very accurate and defines the value of fine.
  • A finger or a Kinect peripheral isn’t, and takes the value of coarse.

Therefore, we can adapt our UI elements to the user’s pointer capabilities. This is useful for making hit areas larger, if the user’s main input mechanism is a finger.

/* The primary input mechanism of the device includes a pointing device of limited accuracy. */
@media (pointer: coarse) { ... }

/* The primary input mechanism of the device includes an accurate pointing device. */
@media (pointer: fine) { ... }

/* The primary input mechanism of the device does not include a pointing device. */
@media (pointer: none) { ... }

An example use case for this query is to size the click area of a checkbox or radio.

Hover Capability: The hover Feature

The hover media feature is used to query the user’s ability to hover over elements on the page. If a device has multiple input mechanisms, the hover media feature must reflect the characteristics of the “primary” input mechanism, as determined by the user agent.” – W3C

It’s important to notice that it only evaluates the primary input mechanism. If the primary input mechanism is not able to hover, but the secondary input can, then the query will resolve to none:

For example, a touchscreen where a long press is treated as hovering would match hover: none.” – W3C

  • A touch screen device, where the primary pointer system is the finger and can’t hover, will take the value of none.
  • A device where the primary input is a mouse and can easily hover parts of the page takes the value of hover.
/* Primary input mechanism system can 
   hover over elements with ease */
@media (hover: hover) { ... }

/* Primary input mechanism cannot hover 
   at all or cannot conveniently hover 
   (e.g., many mobile devices emulate hovering
   when the user performs an inconvenient long tap), 
   or there is no primary pointing input mechanism */
@media (hover: none) { ... }

A good use of this query is a drop-down menu.

Rare Interaction Capabilities: The any-pointer and any-hover Features

On devices that are both touch and have a mouse or a stylus, like the Microsoft Surface, the hover and pointer media query will evaluate the primary input mechanism only.

As Andrea Giammarc pointed out, his Dell XPS 13″ touch takes the value of fine, even though it does have a touch screen because the primary input mechanism is a mouse.

If we want a device like that to take the value of coarse or hover, we can use the Rare Interaction Capabilities.

The any-pointer and any-hover media features are identical to the pointer and hover media features, but they correspond to the union of capabilities of all the pointing devices available to the user. More than one of their values can match, if different pointing devices have different characteristics. They must only match none if all of the pointing devices would match none for the corresponding query, or there are no pointing devices at all.” – W3C

/* One or more available input mechanism(s) 
   can hover over elements with ease */
@media (any-hover: hover) { ... }

/* One or more available input mechanism(s) can hover, 
   but not easily (e.g., many mobile devices emulate 
   hovering when the user performs a long tap) */
@media (any-hover: on-demand) { ... }

/* One or more available input mechanism(s) cannot 
   hover (or there are no pointing input mechanisms) */
@media (any-hover: none) { ... }


/* At least one input mechanism of the device 
   includes a pointing device of limited accuracy. */
@media (any-pointer: coarse) { ... }

/* At least one input mechanism of the device 
   includes an accurate pointing device. */
@media (any-pointer: fine) { ... }

/* The device does not include any pointing device. */
@media (any-pointer: none) { ... }

Device Examples

Typical examples of devices matching combinations of pointer and hover:

pointer: coarse pointer: fine
hover: none smartphones, touch screens stylus-based screens (Cintiq, Wacom, etc)
hover: hover Nintendo Wii controller, Kinect mouse, touch pad

W3C

Patrick H. Lauke has written a great guide about how each device type evaluates interaction media queries.

This is really cool, right? I hear you shouting: what about browser support?

Browser Support Isn’t Bad at All!

Even though this is a working draft, it has pretty good support.

My simple test proved successful on Chrome, Chrome for Android, Safari, Edge, Opera, Samsung browser, and Android Browser, but it didn’t work on FireFox, Opera Mini or IE.

See the Pen Touch screen test by Andres Galante (@andresgalante) on CodePen.

FireFox and IE represent only a bit more than 2% mobile/tablet browser market share. I couldn’t find information about touch TVs or other touch screen devices that are not mobile or tablets.

I think we are ready to use this feature, and as FireFox adds support for it and IE dies once and for all, we will have full support.

The “Cards Selection” Use Case

A month ago, we worked on implementing a multi-select cards component for the new version of PatternFly, an open source design system to which I contribute. It was a perfect case to use the hover and pointer media query.

To select a card, when the user hovers over it, a checkbox is displayed. If the user is not able to hover over elements, then we show the checkbox at all times.

To improve this interaction, we increased the hit area of the checkbox if the primary input mechanism is coarse.

See the Pen Multi select cards by Andres Galante (@andresgalante) on CodePen.

Firefox and IE will display default checkboxes at all times.

Size Isn’t Everything

Devices should be judged by their capabilities since, in the end, it is those capabilities that define them.

This is an underused feature, and it opens the door to exciting new challenges. I can’t wait to see what we, as a community, can do with it.

References

All the descriptions commented on the code are from Mozilla Developer Network.


Touch Devices Should Not Be Judged By Their Size originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/touch-devices-not-judged-size/feed/ 6 251891
You Can’t Detect A Touchscreen https://css-tricks.com/cant-detect-touchscreen/ Sat, 22 Oct 2016 23:26:16 +0000 http://css-tricks.com/?p=246929 Stu Cox explains that there are a ton of ways you might think you can get a yes-or-no answer on whether a browser supports touch or not:

  • Width media queries
  • Touch-related DOM events
  • Touch-related APIs
  • Pointer media queries

The normal …


You Can’t Detect A Touchscreen originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Stu Cox explains that there are a ton of ways you might think you can get a yes-or-no answer on whether a browser supports touch or not:

  • Width media queries
  • Touch-related DOM events
  • Touch-related APIs
  • Pointer media queries

The normal refrain around this is “there are devices that are both, so you’d be wrong on those,” which is true, but it’s actually more problematic than that. These testing methods are often just straight up wrong.

So:

For layouts, assume everyone has a touchscreen. Mouse users can use large UI controls much more easily than touch users can use small ones. The same goes for hover states.

For events and interactions, assume anyone may have a touchscreen. Implement keyboard, mouse and touch interactions alongside each other, ensuring none block each other.

To Shared LinkPermalink on CSS-Tricks


You Can’t Detect A Touchscreen originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
246929