Inline styles for hover/active/focus interactive states
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.

README.md

React Interactive

Live example

  • Style touch interactions in web apps to look like native apps
  • Style keyboad interactions separate from mouse and touch interactions (focus from tab key, etc)
  • Makes every Interactive div/span/etc accessible by default (tab index, role and key click handler added)
  • Use inline styles for all interactive states - hover, active, focus, etc... (no style tags or CSS added to the page), or use class names if you prefer to write styles separately with CSS
  • State change hook to easily incorporate the interactive state into your component (not possible with CSS)
  • Separate active states for mouse, touch, and keyboard interactions (not possible with CSS)
  • Separate focus states based on how it was entered - from mouse, touch, or tab key (not possible with CSS)
  • Easily style and show/hide children based on the Interactive parent's state (only possible with complex CSS selectors)
  • Built in touch device and keyboard support - a click event is generated on mouse click, touch tap (without delay), and enter keydown
import Interactive from 'react-interactive';
...
<Interactive
  as="div" // what the Interactive component is rendered as, can be anything

  hover={{ color: 'green' }} // style object, can use any styles you'd like

  active={{ color: 'blue' }}
  // OR
  hoverActive={{ color: 'red' }}
  touchActive={{ color: 'blue' }}
  keyActive={{ color: 'orange' }}

  focus={{ outline: '2px solid green' }}
  // OR
  focusFromTab={{ outline: '2px solid orange' }}
  focusFromMouse={{ outline: '2px solid green' }}
  focusFromTouch={{ outline: '2px solid blue' }}

  // hook called on every state change, receives prevState and nextState objects
  onStateChange={this.handleInteractiveStateChange}
  onClick={this.handleClick}
  style={{ fontSize: '16px', padding: '3px', color: 'black' }}
>This is an interactive and focusable div</Interactive>

Table of Contents

The Basics

Interactive State Machine

Interactive state machine as a React component. There are 5 mutually exclusive iStates, plus 3 mutually exclusive focus states that can be combined with the 5 iStates (the total number of states that RI can be in is 19, see State Machine Notes below).

  • The 5 mutually exclusive iStates are:
    • normal
    • hover
    • *hoverActive
    • *touchActive
    • *keyActive
  • The 3 mutually exclusive focus states are:
    • **focusFromTab
    • **focusFromMouse
    • **focusFromTouch

*The 3 separate [type]Active states can be treated as a single active state if desired. hoverActive (mouse on and button down), touchActive (touch on screen), keyActive (has focus and enter key down).

**The 3 separate focusFrom[Type] states can be treated as a single focus state if desired.

Compared to CSS, React Interactive is a simpler state machine with better touch device and keyboard support, and state change hooks. See comparison below.

Basic Examples

// Interactive div with state change hook
<Interactive
  as="div"
  normal={{ color: 'black' }}
  hover={{ color: 'green' }}
  active="hover" // use the hover state style for the active state
  style={{ fontSize: '16px', padding: '3px', border: '2px dotted black' }}
  onClick={this.handleClick}
  onStateChange={this.handleInteractiveStateChange}
>This is an interactive div with state change hook</Interactive>
// Interactive as a React Router Link component
import { Link } from 'react-router-dom';
...
<Interactive
  as={Link}
  to="/some/path"
  hover={{ color: 'green' }}
  active={{ color: 'blue' }}
  style={{ color: 'black', padding: '3px' }}
>This is an interactive React Router Link component</Interactive>
// Interactive link with separate styles for mouse, touch, and keyboard interactions
<Interactive
  as="a"
  href="https://example.tld"
  normal={{ color: 'black' }}

  // mouse interactions: normal -> hover -> hoverActive
  hover={{ color: 'green' }}
  hoverActive={{ color: 'red' }}

  // touch interactions: normal -> touchActive
  touchActive={{ color: 'blue' }}

  // keyboard interactions: normal -> normal with focusFromTab -> keyActive with focusFromTab
  focusFromTab={{ outline: '2px solid orange' }}
  keyActive={{ color: 'orange' }}

>This is an interactive link with separate styles for each type of interaction</Interactive>
// Interactive div with class names instead of styles
<Interactive
  as="div"
  hover={{ className: 'hover-class' }}
  hoverActive={{ className: 'hover-active-class' }}
  touchActive={{ className: 'touch-active-class' }}
  keyActive={{ className: 'key-active-class' }}
  // use focusFromTab to only apply the class when focus comes from the keyboard
  focusFromTab={{ className: 'tab-focus-class' }}
  className="some-class"
>This is an interactive div with classes instead of inline styles</Interactive>

Installing react-interactive

$ yarn add react-interactive
# OR
$ npm install --save react-interactive
import Interactive from 'react-interactive';
// OR
var Interactive = require('react-interactive');

Or use the UMD build that's available on Unpkg (the component will be available to use as Interactive)

<script src="https://unpkg.com/react-interactive/dist/ReactInteractive.min.js"></script>

API

API for <Interactive />

Note that there are no default values for any prop, and the only required prop is as.
For the definition of when each state is entered, see the state machine definition below.

Prop Type Example Description
as string (html tag name)
or
ReactComponent
or
JSX/ReactElement
"div"
or
MyComponent
or
<div>...</div>
<MyComponent />
What the Interactive component is rendered as. It can be an html tag name (as a string), or it can be a ReactComponent (RI's callbacks are passed down as props to the component), or it can be a JSX/ReactElement (see as prop type notes for more info). Note that as is hot-swappable on each render and RI will seamlessly maintain the current interactive state. The as prop is required (it is the only required prop).
normal style object
or
options object
or
string
{ color: 'black' }
or
{ style: { color: 'black' }, className: 'some-class' }
or
'hover'
Style or options object for the normal state, or a string indicating a state to match. If it's an object, it can be either a style object or an options object with the keys style and className. The style object is merged with both the style prop and the focus state style (see merging styles for the order that styles are merged in). The className is a string of space separated class names and is merged as a union with the className prop and the focus state className. If the value of the normal prop is a string, it must indicate one of the other states, e.g. 'hover', and that state's style and className properties will be used for both states.
hover style object
or
options object
or
string
{ color: 'green' }
or... (same as above)
Same as normal, but for the hover state. Note that if there is no hoverActive or active prop, then the hover prop's style and classes are used for the hoverActive state. This state is entered when the mouse is on the RI element.
active style object
or
options object
or
string
{ color: 'red' }
or... (same as above)
Same as normal, but for the active state. Note that the active state is the union of the hoverActive, touchActive, and keyActive states. The active prop is only used in place of the [type]Active prop if the respective [type]Active prop is not present.
hoverActive style object
or
options object
or
string
{ color: 'red' }
or... (same as above)
Same as normal, but for the hoverActive state. Note that if there is no hoverActive or active prop, then the hover prop's style and classes are used for the hoverActive state. This state is entered when the mouse is on the RI element and the mouse button is down.
touchActive style object
or
options object
or
string
{ color: 'blue' }
or... (same as above)
Same as normal, but for the touchActive state. This state is entered when a touch point is on the RI element.
keyActive style object
or
options object
or
string
{ color: 'yellow' }
or... (same as above)
Same as normal, but for the keyActive state. This state is entered when the RI element has focus and the enter key is down.
focus style object
or
options object
or
string
{ outline: '2px solid green' }
or... (same as above)
Same as normal, but for the focus state. Note that the focus state is the union of the focusFromTab, focusFromTouch, and focusFromMouse states. The focus prop is only used in place of the focusFrom[Type] prop if the respective focusFrom[Type] prop is not present.
focusFromTab style object
or
options object
or
string
{ outline: '2px solid green' }
or... (same as above)
Same as normal, but for the focusFromTab state. This state is entered if focus is from the tab key (i.e. tabbing through the focusable elements on the page). Also, any focus calls not from a mouse or touch interaction (e.g. from assistive tech) will match with focusFromTab.
focusFromMouse style object
or
options object
or
string
{ outline: '2px solid red' }
or... (same as above)
Same as normal, but for the focusFromMouse state. This state is entered when focus is from a mouse interaction.
focusFromTouch style object
or
options object
or
string
{ outline: '2px solid blue' }
or... (same as above)
Same as normal, but for the focusFromTouch state. This state is entered when focus is from a touch interaction.
style style object { margin: '10px' } Styles that are always applied. Styles are merged with state styles. State styles have priority when there are conflicts.
className string "some-class other-class" Classes that are always applied to the element, and are merged as a union with state classes.
onStateChange function function({ prevState, nextState, event }) {...} Function called on each state change. Receives an object with prevState, nextState and event keys as the sole argument. prevState and nextState are state objects. The event is the event that caused the state change (a synthetic React event).
setStateCallback function function({ prevState, nextState }) {...} Function passed in as a callback when RI calls setState. Receives the same object as onStateChange as its sole argument, except without the event key (setState is asynchronous and React events don't persist asynchronously). Use this hook if you need to wait until the DOM is updated before executing the callback.
onClick function function(event, clickType) {...}

Where clickType is one of:
'mouseClick'
'tapClick'
'keyClick'
Function called for mouse clicks, touch taps with 1 touch point/finger (called without delay), enter keydown events (if the element has focus), and synthetic click events. The event argument will always be a click event (node.click() is called to generate a click event if needed). The clickType argument will always be one of mouseClick, tapClick, or keyClick. It will be mouseClick for mouse clicks and for synthetic click events on mouse only and hybrid devices. It will be tapClick for touch taps with 1 touch point and for synthetic click events on touch only devices. It will be keyClick if the click event was generated from a enter keydown event (or, for some elements, a space keyup event). Note that RI will call node.click() for enter keydown events only if there is an onClick prop.
onTapTwo function function(event) {...} Function called for taps with 2 touch points, e.g. a 2 finger tap. Event passed in is the touchend event from last touch point to leave the surface.
tapTimeCutoff whole number 500 Number of ms to allow for a tap. This is the cutoff time that separates a tap from a long press. This prop is not required and the default is 500.
onLongPress function function(event) {...} Function called on long press if touch is present after the tapTimeCutoff and if the touch has not moved more than is allowed for a tap. Event passed in is the touch start event that started the long press.
touchActiveTapOnly boolean touchActiveTapOnly Add this prop to only remain in the touchActive state while a tap is possible. If the touch is moved more than the tolerance for a tap, or held on the screen longer than the time allowed for a tap, then the touchActive state is exited. This is useful when the intention of the touchActive state is to indicate to the user that they are tapping something. Note that without this prop React Interactive will remain in the touchActive state as long as the touch point is on the screen.
extraTouchNoTap boolean extraTouchNoTap Add this prop to cancel taps while touching someplace else on the screen. By default RI will ignore extra touches on the screen and allow taps on the RI element regardless of other touches.
nonContainedChild boolean nonContainedChild Add this prop if the DOM node's children are not contained inside of it on the page. For example, a child that is absolutely positioned outside of its parent. React Interactive does some quality control checks using node.getBoundingClientRect(), and by default the children are assumed to be within the parent's rectangle, but if this is not the case, then add this prop and the children will be checked.
initialState state object { iState: 'normal', focus: 'tab' } Optional initial state to enter when the component is mounted. A state object with keys for one or both of iState and focus. Note that for an active iState, you must specify [type]Active and not just active. Used in the constructor to set iState and in componentDidMount to set focus (RI can't set focus until after it has a reference to the DOM node).
forceState state object { iState: 'normal', focus: false } Force enter this state. Same as initialState except not used for the initial render. Note that if only one key is present, a shallow merge is done with the current state, for example, use { focus: 'tab' } to only focus the element. Only used in componentWillReceiveProps.
stylePriority object { hover: true, hoverActive: true } By default the focus state style takes precedence over the iState style when merged (except for the keyActive iState). Use this prop to specify specific iStates whose style should take precedence over the focus state style. Note that for an active iState, you must specify [type]Active and not just active.
refDOMNode function function(node) {...} Function is passed in a reference to the DOM node, and is called whenever the node changes. You shouldn't need to use this for anything related to React Interactive, but it's available in case you need to use it for other things. Note that if you need to focus/blur the DOM node, use the forceState or initialState prop and set the focus state instead of calling focus/blur directly on the DOM node.
focusToggleOff boolean focusToggleOff Add this prop to prevent focus from toggling on mouseup/tap. With this prop RI will enter the focus state normally and will remain in the focus state until the browser sends a blur event.
mutableProps boolean mutableProps Add this prop if you are passing in mutable props so the component will always update. By default it's assumed that props passed in are immutable. A shallow compare is done, and if the props are the same, the component will not update. If you're not sure and notice buggy behavior, then add this prop.
interactiveChild boolean interactiveChild Add this prop if Interactive's children use the Interactive Children API.
wrapperStyle style object { display: 'block' } Styles that are applied to the span wrapper if as is a ReactComponent.
wrapperClassName string "ri-wrapper-class other-class" Classes that are applied to the span wrapper if as is a ReactComponent.
... anything id="some-id", tabIndex="1", etc... All additional props received are passed through.

Merging Styles and Classes

  • Styles are merged in the following order (last one takes precedence):
    1. The style prop
    2. The iState style (except keyActive)
    3. The focus state style if in the focus state
    4. The keyActive state style
  • If you want an iState style to take precedence over the focus style, then use the stylePriority prop and specify which iStates should have priority over focus, e.g. stylePriority={{ hover: true, hoverActive: true }}
  • Classes are merged as a union without preference:
    • focus state classes if in the focus state
    • iState classes
    • The className prop

as Prop Type

  • If as is a string:
    • E.g. as="div"
    • The string must be an html tag name, for example, div, span, a, h1, p, ul, li, input, select, etc...
    • Note that for SVG images, as="svg" works fine except that in general SVGs are not focusable by the browser, so if you need focus then wrap the svg element in a Interactive span. Also with SVGs you can make a specific path interactive, e.g. as="path" to create interactions within the SVG.
  • If as is a ReactComponent:
    • E.g. as={MyComponent}
    • Strictly speaking this means that as is either a ReactClass or a ReactFunctionalComponent as defined in the React Glossary.
    • In order for React Interactive to work as a ReactComponent, the component must pass down the props it receives from React Interactive to the top DOM node that it renders, and it cannot override any of the passed down event handlers, e.g. onMouseEnter. Also, the component cannot replace its top DOM node once it's rendered unless the replacement is the result of new props (note that mutations are okay, e.g. changing style, classes, children, etc is fine). This is because React Interactive keeps a reference to the component's top DOM node so it can do things like call focus(), and if the top DOM node is replaced without React Interactive's knowledge, then things start to break. Note that React Router's Link component meets these requirements.
    • When as is a ReactComponent it is wrapped in a <span> in order for React Interactive to maintain a reference to the top DOM node without breaking encapsulation. Without the span wrapper the only way to access the top DOM node would be through using ReactDOM.findDOMNode(component), which breaks encapsulation and is discouraged, and also doesn't work with stateless functional components.
      • The <span> wrapper can be styled by passing down the props wrapperClassName (class string) and wrapperStyle (style object).
  • If as is a JSX/ReactElement:
    • E.g. as={<div>...</div>} or as={<MyComponent />}
    • The props of the top ReactElement are merged with, and have priority over, Interactive's props. For example:
    const jsxElement = <div hover={{ color: 'blue' }}>Some jsxElement text</div>;
    <Interactive
      as={jsxElement}
      hover={{ color: 'green' }}
      active={{ color: 'red' }}
    >Some other text</Interactive>
    • This will create a div with text that reads 'Some jsxElement text' and will be blue on hover and red on active. When the props are merged, jsxElement's hover prop and children have priority over Interactive's hover prop and children, but since jsxElement didn't specify an active prop, Interactive's active prop is still valid.
    • After the props are merged, the JSX/ReactElement's type (html tag name or ReactComponent) determines how as is processed - either like a string or like a ReactComponent.
    • Note that when as is a ReactElement you cannot attach a ref to it (only the Interactive element is rendered and you can attach a ref to Interactive (or use the refDOMNode prop), but it is not possible to have two refs to the same element).
    • Note that this is not a very practical example of using a JSX/ReactElement for as. For a more practical example see, Hot Swappable as.

state Object

  • The React Interactive state object looks like this:
// this.state
{
  // iState is always 1 of 5 strings
  iState: 'normal' / 'hover' / 'hoverActive' / 'touchActive' / 'keyActive',
  // focus is always 1 or 4 values
  focus: false / 'tab' / 'mouse' / 'touch',
}
  • In RI's API, the onStateChange and setStateCallback hooks receive the previous and next state objects when they are called, and the forceState and initialState props pass in a state object to the RI component.

Default role and tabIndex

  • If you add an onClick prop without a role prop, and it's not clear what the role of the element is (i.e. it's not for user input, a link, or an area tag), then RI will automatically add role="button" for better accessibility. If you don't want any role added to the DOM element, then pass in the prop role={null}.
  • If you add a focus or onClick prop without a tabIndex prop, then a tabIndex of 0 is added to make the element focusable by the browser. If you don't want any tabIndex added to the DOM element, then pass in the prop tabIndex={null}.
  • Note that for buttons as="button" is discouraged because browsers are inconsistent in how they display and handle button interactions. For better consistency, use as="div"/"span" and add an onClick handler. By default RI will add role="button", tabIndex="0", and a key click handler (which will call onClick), so it will work just like a button. You can override these with your own role and tabIndex if you prefer.

Focus State

  • The focus state can be applied to any element, not just inputs, and will toggle on click/tap unless the element is for user input.
  • React Interactive's focus state is always kept in sync with the browser's focus state. Added functionality like focus toggle and focusFrom are implemented by controlling the browser's focus state.
  • Focus toggle
    • All elements will toggle focus except if the element is for user input, that is, the element's tag name is input, textarea, or select.
    • For mouse interactions, the focus state is entered on mouse down, and toggled off on mouse up providing it didn't enter the focus state on the preceding mouse down.
    • For touch interactions, the focus state in entered with a 1 touch point/finger tap, and toggled off on the next 1 finger tap. Also, on touchOnly devices, a click event not preceded by a touch event (e.g. a synthetic click event) will toggle focus on/off.
    • If you want to turn off focus toggle, then add the focusToggleOff prop. With this prop RI will enter the focus state normally and will remain in the focus state until the browser sends a blur event.
  • The focus state change occurs in the same setState call as the iState change, so the onStateChange hook is only called once. For example, onMouseDown enters the focus state and the hoverActive state in a single state change (and render). This achieved by controlling the browser's focus state - without this control the browser would fire the focus event immediately after the mouse down event resulting in two setState calls (and two onStateChange calls), one to enter the hoverActive state and one to enter the focus state.

Default Styles

  • If a focus prop is passed to React Interactive, then RI will prevent the browser's default focus outline from being applied.
  • If clicking the mouse does something, then the cursor will default to a pointer.
  • If you want to use the browser default styles without any interference from RI, then add the below props:
    • useBrowserOutlineFocus
    • useBrowserCursor
  • If a touchActive or active prop is passed to React Interactive, then RI will prevent the browser's default webkit tap highlight color from being applied.
    • To use the WebkitTapHighlightColor for styling, don't provide a touchActive or active prop and set the WebkitTapHighlightColor style in the main style prop.
    • Note that if there is no active or touchActive prop, RI will let the browser fully manage what it considers to be a click from a touch interaction. This results in a better match of when the WebkitTapHighlightColor is active to what results in a click. RI won't call node.click(), so there may be a delay in the click event in some browsers.

Interactive Children API

  • Note that you must add the interactiveChild prop to <Interactive /> to use the Interactive Children API (by default RI will not inspect its children and will render them as is).
  • If you have nested Interactive components, the children will be styled based on the state of their closest Interactive parent.
function InteractiveChild() {
  return (
    <Interactive
      as="ul"
      interactiveChild // so Interactive will style the children based on its state
      focusFromTab={{}} // so the Interactive component is focusable
      touchActive={{}} // so Interactive will control taps and remove the browser's default style
    >
      <li>This list item will not change style based on the state of the Interactive parent.</li>

      <li
        onParentHover={{ color: 'green' }}
        onParentHoverActive="hover" // use the onParentHover style for onParentHoverActive
        onParentTouchActive={{ color: 'blue' }}
        onParentFocusFromTab={{ outline: '2px solid green' }}
      >
        This list item will change style based on the state of the Interactive parent.
      </li>

      <li
        showOnParent="hover hoverActive touchActive focusFromTab"
      >
        This list item is only rendered when the Interactive parent is in the
        hover, hoverActive, touchActive or focusFromTab states.
      </li>
    </Interactive>
  );
}
Prop Type Example Description
showOnParent space separated string 'hover touchActive focusFromTab' Add this props to only render the child when the parent is in any of the listed states. Without this prop, RI will always render the child. The acceptable state values are: hover, active (union of the 3 [type]Active states), hoverActive, touchActive, keyActive, focus (union of the 3 focusFrom[Type] states), focusFromTab, focusFromMouse, and focusFromTouch. List as a space separated string.
onParentNormal style object
or
options object
or
string
{ color: 'black' }
or
{ style: { color: 'black' }, className: 'some-class' }
or
'hover'
Style or options object when the parent is in the normal state, or a string indicating a state to match. If it's an object, it can be either a style object or an options object with the keys style and className. The style object is merged with both the child's style prop and the onParentFocusFrom[Type] style in the same order as the Interactive parent. The className is a string of space separated class names and is merged as a union with the child's className prop and the onParentFocusFrom[Type] className. If the value of the onParentNormal prop is a string, it must indicate one of the other states, e.g. 'hover' (without the onParent prefix), and that state's onParent[State] style and className properties will be used for both states. Note that the interface is the same as <Interactive />'s normal prop.
onParentHover style object
or
options object
or
string
{ color: 'green' }
or... (same as above)
Same as onParentNormal, but for the parent's hover state. Note that if there is no onParentHoverActive or onParentActive prop, then the onParentHover prop's style and classes are used for the onParentHoverActive prop.
onParentActive style object
or
options object
or
string
{ color: 'red' }
or... (same as above)
Same as onParentNormal, but for the parent's active state. Note that the onParentActive prop is only used in place of the onParent[Type]Active prop if the respective onParent[Type]Active prop is not present.
onParentHoverActive style object
or
options object
or
string
{ color: 'red' }
or... (same as above)
Same as onParentNormal, but for the parent's hoverActive state. Note that if there is no onParentHoverActive or onParentActive prop, then the onParentHover prop's style and classes are used for the onParentHoverActive prop.
onParentTouchActive style object
or
options object
or
string
{ color: 'blue' }
or... (same as above)
Same as onParentNormal, but for the parent's touchActive state.
onParentKeyActive style object
or
options object
or
string
{ color: 'yellow' }
or... (same as above)
Same as onParentNormal, but for the parent's keyActive state.
onParentFocus style object
or
options object
or
string
{ outline: '2px solid green' }
or... (same as above)
Same as onParentNormal, but for the parent's focus state. Note that the onParentFocus prop is only used in place of the onParentFocusFrom[Type] prop if the respective onParentFocusFrom[Type] prop is not present.
onParentFocusFromTab style object
or
options object
or
string
{ outline: '2px solid green' }
or... (same as above)
Same as onParentNormal, but for the parent's focusFromTab state.
onParentFocusFromMouse style object
or
options object
or
string
{ outline: '2px solid red' }
or... (same as above)
Same as onParentNormal, but for the parent's focusFromMouse state.
onParentFocusFromTouch style object
or
options object
or
string
{ outline: '2px solid blue' }
or... (same as above)
Same as onParentNormal, but for the parent's focusFromTouch state.

Interactive State Machine Comparison

Compared to CSS, React Interactive is a simpler state machine, with better touch device and keyboard support, and state change hooks.

  • Let's define some basic mouse, touch, and keyboard states:
    • mouseOn: the mouse is on the element
    • buttonDown: the mouse button is down while the mouse is on the element
    • touchDown: at least one touch point is in contact with the screen and started on the element
    • focusKeyDown:
      • For React Interactive, this is if:
        • The element is not a checkbox, radio, or select, and the enter key is down
        • The element is a button and the space bar or enter key is down
        • The element is a checkbox, radio, or select and the space bar is down
        • Convention is buttons are activated by both the space bar and enter key, and checkboxes, radio buttons and selects are only activated by the space bar
      • For CSS, this is something like, if the element is a button, checkbox, or radio button, and the space bar is down, then it is in the active state (i.e. the foucsKeyDown state for the purposes of this abstraction), but is not consistent across browsers. Note that even though the enter key triggers links and buttons, pressing the enter key won't cause an element to enter the active state, which means that with CSS there is no way to give visual feedback when triggering an element with the enter key.

React Interactive State Machine

Interactive state Mouse, touch and keyboard states
base styles Not an interactive state, always applied, everything merges with them
normal !mouseOn && !buttonDown && !touchDown && !focusKeyDown
hover mouseOn && !buttonDown && !touchDown && !focusKeyDown
active hoverActive OR keyActive OR touchActive
hoverActive mouseOn && buttonDown && !touchDown && !focusKeyDown
keyActive focusKeyDown && !touchDown
touchActive touchDown

The three focusFrom states can be combined with any of the above states, and the keyActive state is only available while in the focus state.

CSS Interactive State Machine

Note that since a state machine can only be in one state at a time, to view interactive CSS as a state machine it has to be thought of as a combination of pseudo class selectors that match based on the mouse, keyboard and touch states.

Interactive state Note Mouse, touch and keyboard states CSS Selector(s)
base styles Always applied, everything merges with them Not an interactive state .class
normal Not commonly used in CSS (zeroing out/overriding base styles is used instead) !mouseOn && !buttonDown && !touchDown && !focusKeyDown .class:not(:hover):not(:active)
hover Only hover styles applied (mouseOn && !buttonDown && !focusKeyDown) OR (after touchDown and sticks until you tap someplace else) - the sticky hover CSS bug on touch devices .class:hover
hoverActive Both hover and active styles applied (mouseOn && buttonDown) OR (mouseOn && focusKeyDown) OR (touchDown, but not consistent across browsers) .class:hover, .class:active
active Only active styles applied (buttonDown && !mouseOn currently, but had mouseOn when buttonDown started) OR (focusKeyDown && !mouseOn) OR (touchDown but not on the element currently, but not consistent across browsers) .class:active

The focus state can be combined with any of the above CSS interactive states to double the total number of states that the CSS interactive state machine can be in.

Note that you could achieve mutually exclusive hover and active states if you apply hover styles with the .class:hover:not(:active) selector, and there are other states that you could generate if you wanted to using CSS selectors. You could also create a touch active state by using Current Input, so CSS has some flexibility, but it comes at the cost of simplicity, and in CSS touch and keyboard interactions are not well supported.

State Machine Notes

  • The total number of states that the React Interactive state machine can be in is 19.
  • There are 5 mutually exclusive and comprehensive iStates: normal, hover, hoverActive, touchActive, and keyActive. These are combined with 4 mutually exclusive and comprehensive focus states: false, tab, mouse, and touch, with the exception of keyActive, which is only available while focus is not false, for a total of 19 states:
normal hover hoverActive touchActive N/A
normal with focusFromTab hover with focusFromTab hoverActive with focusFromTab touchActive with focusFromTab keyActive with focusFromTab
normal with focusFromMouse hover with focusFromMouse hoverActive with focusFromMouse touchActive with focusFromMouse keyActive with focusFromMouse
normal with focusFromTouch hover with focusFromTouch hoverActive with focusFromTouch touchActive with focusFromTouch keyActive with focusFromTouch
  • The onStateChange hook is called each time a transition occurs between any of the 19 states. Note that a transition will never occur between two focusFrom states as focusFrom is based on how the focus state was entered, so have to transition to focus false before transitioning to a different focusFrom state.
  • The active prop is just a convenience wrapper around the 3 specific active states: hoverActive, touchActive, and keyActive, and is not a state in its own right.
  • The focus prop is just a convenience wrapper around the 3 focusFrom states: tab, mouse and touch, and is not a state in its own right.

More Examples

Using Interactive's State in Parent Component

import React from 'react';
import Interactive from 'react-interactive';

class MyComponent extends React.Component {
  constructor() {
    super();
    this.state = {
      iState: 'normal',
      focus: false,
    };
  }

  handleOnStateChange = ({ nextState }) => {
    this.setState(nextState);
    // equivalent to the line above:
    // this.setState({
    //   iState: nextState.iState,
    //   focus: nextState.focus,
    // });
  }

  render() {
    return (
      <div>
        <Interactive
          as="div"
          onStateChange={this.handleOnStateChange}
          // ...and any other props as needed
        >RI component</Interactive>
        {
          // create your component using:
          // this.state.iState === 'normal' / 'hover' / 'hoverActive' / 'touchActive' / 'keyActive'
          // this.state.focus === false / 'tab' / 'mouse' / 'touch'
        }
      </div>
    );
  }
}

Enter or Leave a Specific State Hook

  • Note that this example is written as a ReactFunctionalComponent, but the same handleOnStateChange logic would apply when creating a ReactClass.
import React from 'react';
import Interactive from 'react-interactive';

function MyFunctionalComponent() {
  function enterFocus() {
    // do something when enter the focus state
  }
  function leaveFocus() {
    // do something when leave the focus state
  }
  function enterTouchActive() {
    // do something when enter the touchActive state
  }
  function leaveTouchActive() {
    // do something when leave the touchActive state
  }

  function handleOnStateChange({ prevState, nextState }) {
    !prevState.focus && nextState.focus && enterFocus();
    prevState.focus && !nextState.focus && leaveFocus();

    if (nextState.iState === 'touchActive' && prevState.iState !== nextState.iState) {
      enterTouchActive();
    } else if (prevState.iState === 'touchActive' && prevState.iState !== nextState.iState) {
      leaveTouchActive();
    }
  }

  return (
    <Interactive
      as="div"
      onStateChange={handleOnStateChange}
      // ...and any other props as needed
    >RI component</Interactive>
  );
}

Show On hover and active

  • Show Div1 if the mouse is on the React Interactive element, that is, RI is in the hover or hoverActive state.
  • Show Div2 if RI is in an active state, one of hoverActive, touchActive, or keyActive.
  • Note that both Div1 and Div2 will be shown when RI is in the hoverActive state.
import React from 'react';
import Interactive from 'react-interactive';

class MyComponent extends React.Component {
  constructor() {
    super();
    this.state = {
      hover: false,
      active: false,
    };
  }

  handleOnStateChange = ({ nextState }) => {
    this.setState({
      // hover and hoverActive both contain hover, so check nextState for hover
      hover: /hover/.test(nextState.iState),
      // hoverActive, touchActive, and keyActive all contain Active, note the capitalization
      active: /Active/.test(nextState.iState),
    });
  }

  render() {
    return (
      <div>
        <Interactive
          as="div"
          onStateChange={this.handleOnStateChange}
          // ...and any other props as needed
        >RI element</Interactive>

        {this.state.hover && <div>Div1 shown if RI is in the hover or hoverActive state</div>}

        {this.state.active && <div>Div2 shown if RI is in one of the active states</div>}
      </div>
    );
  }
}

Show On hover, touchActive and focusFromTab

import React from 'react';
import Interactive from 'react-interactive';

class MyComponent extends React.Component {
  constructor() {
    super();
    this.state = {
      showInfo: false,
    };
  }

  shouldComponentUpdate(nextProps, nextState) {
    return this.state.showInfo !== nextState.showInfo;
  }

  handleOnStateChange = ({ nextState }) => {
    this.setState({
      showInfo:
        nextState.iState === 'hover' ||
        nextState.iState === 'touchActive' ||
        nextState.focus === 'tab'
    });
  }

  render() {
    return (
      <div>
        {this.state.showInfo && <div>Some info about something</div>}
        <Interactive
          as="div"
          onStateChange={this.handleOnStateChange}
          // ...and any other props as needed
        >Show info</Interactive>
      </div>
    );
  }
}

Hot Swappable as

  • Hot-swap JSX/ReactElements while loading something.
  • Seamlessly maintains the current interactive state while allowing for separate interactive styling of the two JSX elements.
  • Note that the onClick prop is only present on the clickToLoad JSX element and not on the currentlyLoading element, so any clicks that come through while loading will be ignored.
import React, { PropTypes } from 'react';
import Interactive from 'react-interactive';

class MyComponent extends React.Component {
  static propTypes = {
    load: PropTypes.func.isRequired,
  }
  constructor() {
    super();
    this.state = {
      loading: false,
    };
  }

  loadSomething = () => {
    this.setState({ loading: true });
    this.props.load(() => {
      this.setState({ loading: false });
    });
  }

  render() {
    const clickToLoad = (
      <span
        onClick={this.loadSomething}
        hover={{ color: 'green' }}
        active={{ color: 'blue' }}
        focusFromTab={{ outline: '2px solid green' }}
      >Load Something</span>
    );
    const currentlyLoading = (
      <span
        hover={{ color: 'gray' }}
        active={{ color: 'lightgray' }}
        focusFromTab={{ outline: '2px solid gray' }}
      >Loading...</span>
    );
    return (
      <Interactive
        as={this.state.loading ? currentlyLoading : clickToLoad}
        style={{ fontSize: '14px', padding: '5px' }}
        normal={{ color: 'black' }}
      />
    );
  }
}