JSPM

  • Created
  • Published
  • Downloads 24046
  • Score
    100M100P100Q131036F
  • License MIT

Better interactive states than CSS pseudo classes and a callback for interactive state changes

Package Exports

  • react-interactive

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (react-interactive) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

React Interactive

npm npm bundle size (version)

  • Better interactive states than CSS pseudo classes
    • hover, active, mouseActive, touchActive, keyActive
    • focus, focusFromMouse, focusFromTouch, focusFromKey
  • Callback for interactive state changes
    • Know when the hover/active/focus state is entered/exited (impossible to do with CSS)
  • Style interactive states with CSS, inline styles, and CSS-in-JS libraries
  • Eliminates the CSS sticky hover bug on touch devices
  • Allows you to only add focus styles when focus is from the keyboard

Live demo app for React Interactive

Code is in the /demo folder, or open the demo in CodeSandbox


Basics ⚡️ Props ⚡️ eventFrom ⚡️ TypeScript


Basics

v1 is in pre-release so use the @next tag to install it, v0 is available here

npm install --save react-interactive@next
import { Interactive } from 'react-interactive'

...

<Interactive as="button">My Button</Interactive>

Polymorphic as prop

React Interactive accepts a polymorphic as prop that can be a string representing a DOM element (e.g. "button", "a", "div", etc), or a React component (e.g. React Router's Link, etc).

import { Interactive } from 'react-interactive';
import { Link } from 'react-router-dom';

...

<Interactive as="button">My Button</Interactive>
<Interactive as="a" href="https://rafgraph.dev">My Link</Interactive>
<Interactive as={Link} to="/some-page">My React Router Link</Interactive>

Interactive state

The state object used by React Interactive to determine how the <Interactive> component is rendered. The interactive state object is also passed to the onStateChange callback and children (when children is a function).

interface InteractiveState {
  hover: boolean;
  active: 'mouseActive' | 'touchActive' | 'keyActive' | false;
  focus: 'focusFromMouse' | 'focusFromTouch' | 'focusFromKey' | false;
}
  • hover Mouse on the element (unlike CSS pseudo classes the hover state is only entered from mouse input which eliminates the CSS sticky hover bug on touch devices).
  • active
    • mouseActive Mouse on the element and mouse button down.
    • touchActive Touch point on the element.
    • keyActive Element has focus and the enter key is down (or space bar for some elements).
  • focus
    • focusFromMouse Element has focus and focus was entered from mouse input.
    • focusFromTouch Element has focus and focus was entered from touch input.
    • focusFromKey Element has focus and focus was entered from keyboard input (e.g. tab key).

Styling with CSS

CSS classes for the current state are automatically added for easy styling with CSS or CSS-in-JS libraries like Styled Components, Emotion, and Stitches.

  • Hover state adds a hover class.
  • Active state adds both an active class and an [input]Active class, e.g. mouseActive.
  • Focus state adds both a focus class and a focusFrom[input] class, e.g. focusFromKey.
  • For a full class list see interactive className props (the class names for each state can be changed using props).
import { Interactive } from 'react-interactive';

...

// add a className to target the element in CSS
<Interactive as="button" className="my-button">My Button</Interactive>
/* use compound selectors in CSS to style the interactive states */
.my-button.hover, .my-button.active: {
  color: green;
}

.my-button.focusfromkey: {
  outline: 2px solid green;
}

Styling with CSS-in-JS

Use the added CSS classes to style the interactive states with CSS-in-JS libraries like Styled Components, Emotion, and Stitches.

import { Interactive } from 'react-interactive';
import { styled } from '@stitches/react';

const StyledButton = styled(Interactive, {
  '&.hover, &.active': {
    color: 'green',
  },
  '&.focusFromKey': {
    outline: '2px solid green',
  },
});

...

<StyledButton>My Button</StyledButton>

Styling with inline styles

React Interactive uses a separate style prop for each state for easy inline styling.

  • Hover state uses the hoverStyle prop.
  • Active state uses both an activeStyle prop and an [input]ActiveStyle prop.
  • Focus state uses both a focusStyle prop and a focusFrom[input]Style prop.
  • For a full list see interactive style props.
import { Interactive } from 'react-interactive';

const hoverAndActiveStyle = {
  color: 'green',
};

const focusFromKeyStyle = {
  outline: '2px solid green',
};

...

<Interactive
  as="button"
  hoverStyle={hoverAndActiveStyle}
  activeStyle={hoverAndActiveStyle}
  focusFromKeyStyle={focusFromKeyStyle}
>
  My Button
</Interactive>

Reacting to interactive state changes

React Interactive accepts an onStateChange prop callback that is called each time the state changes with both the current and previous states.

import * as React from 'react';
import { Interactive } from 'react-interactive';

...

const handleInteractiveStateChange = React.useCallback(({ state, prevState }) => {
  // both state and prevState are of the shape:
  // {
  //   hover: boolean,
  //   active: 'mouseActive' | 'touchActive' | 'keyActive' | false,
  //   focus: 'focusFromMouse' | 'focusFromTouch' | 'focusFromKey' | false,
  // }
}, []);

...

<Interactive
  as="button"
  onStateChange={handleInteractiveStateChange}
>
  My Button
</Interactive>

Using the interactive state in children

React Interactive uses the children as a function pattern to pass the current interactive state to its children.

import { Interactive } from 'react-interactive';

...

<Interactive as="p">
  {({ hover, active, focus }) => (
    Some text where only one word is{' '}
    <span style={{ color: hover ? 'green' : undefined }}>highlighted</span>{' '}
    when the paragraph is hovered.
  )}
</Interactive>

Props

as, onStateChange, children, disabled, interactive className, interactive style, useExtendedTouchActive, ref


as: string | ReactComponent

Default value: "button"

React Interactive accepts a polymorphic as prop that can be a string representing a DOM element (e.g. "button, "a", "div", etc), or a React component (e.g. React Router's Link, etc).

<Interactive as="button">My Button</Interactive>
<Interactive as={Link} to="/some-page">My React Router Link</Interactive>

Note that if as is a React component, then the component needs to pass through props to the element that it renders, including the ref prop using React.forwardRef(). Most libraries designed for composability do this by default, including React Router's <Link> component.


onStateChange: function

Default value: undefined

Callback function that is called each time the interactive state changes with both the current and previous interactive states (passed in as a single argument of the form { state, prevState }). See Reacting to interactive state changes.


children: ReactNode | function

Default value: undefined

If children is a ReactNode (anything that React can render, e.g. an Element, Fragment, string, boolean, null, etc) then it is passed through to React to render normally.

If children is a function then it is called with an object containing the current interactive state of (note that the function must return a ReactNode that React can render). See Using the interactive state in children.

<Interactive as="div">
  {({ hover, active, focus }) => {
    //   hover: boolean,
    //   active: 'mouseActive' | 'touchActive' | 'keyActive' | false,
    //   focus: 'focusFromMouse' | 'focusFromTouch' | 'focusFromKey' | false,
    ...
    // must return something that React can render
  }}
</Interactive>

disabled: boolean

Default value: false

Passing in a disabled prop is an easy way to temporarily disable a React Interactive component without changing the other props. When disabled is true:

  • The disabledClassName and disabledStyle props will be used for styling the disabled component.
  • disabled will be passed through to the DOM element if it is a <button>, <input>, <select>, or <textarea> (elements that support the disabled attribute).
  • The href prop will not be passed through to <a> and <area> DOM elements (this disables links).
  • onClick, onClickCapture, onDoubleClick, and onDoubleClickCapture props will not be passed through.
  • tabIndex prop will not be passed through.

Interactive state className props: string

Default values: see below table

CSS classes that are added to the DOM element when in an interactive state. These are merged with the standard className prop which is always applied. See Styling with CSS.

Prop Default value
hoverClassName "hover"
activeClassName "active"
mouseActiveClassName "mouseActive"
touchActiveClassName "touchActive"
keyActiveClassName "keyActive"
focusClassName "focus"
focusFromMouseClassName "focusFromMouse"
focusFromTouchClassName "focusFromTouch"
focusFromKeyClassName "focusFromKey"
disabledClassName "disabled"

Note that:

  • activeClassName is added when in any active state. This is in addition to the specific [input]ActiveClassName.
  • focusClassName is added when in any focus state. This is in addition to the specific focusFrom[input]ClassName.
  • disabledClassName is added when the disabled boolean prop is true, in which case none of the other interactive className props are applied.

Interactive state inline style props: style object

Default values: undefined

Inline styles that are added to the DOM element when in an interactive state. These are merged with the standard style prop which is always applied. See Styling with inline styles.

Inline style prop list:

  • hoverStyle
  • activeStyle
  • mouseActiveStyle
  • touchActiveStyle
  • keyActiveStyle
  • focusStyle
  • focusFromMouseStyle
  • focusFromTouchStyle
  • focusFromKeyStyle
  • disabledStyle

Style prop objects for each state are merged with the following precedence (last one wins):

  • style prop (styles that are always applied)
  • ===
  • hoverStyle
  • activeStyle
  • [input]ActiveStyle
  • focusStyle
  • focusFrom[input]Style
  • =OR=
  • disabledStyle (when disabled, only the disabledStyle prop is merged with the style prop)

useExtendedTouchActive: boolean

Default value: false

By default React Interactive only stays in the touchActive state while a click event (from the touch interaction) is still possible. To remain in the touchActive state for as long as the touch point is on the screen then pass in a useExtendedTouchActive prop. This can be useful for implementing functionality such as show on touchActive, long press, etc.


ref: object ref | callback ref

Default value: undefined

React Interactive uses React.forwardRef() to forward the ref prop to the DOM element. Passing a ref prop to an Interactive component will return the DOM element that the Interactive component is rendered as.

React Interactive supports both object refs created with React.useRef() and callback refs created with React.useCallback().


Using eventFrom

React Interactive uses Event From under the hood to determine if browser events are from mouse, touch or key input. The eventFrom and setEventFrom functions are re-exported from Event From and can be useful when building apps with React Interactive.

eventFrom(event)

The eventFrom(event) function takes a browser event and returns 1 of 3 strings indicating the input type that caused the browser event: 'mouse', 'touch', or 'key'. For example, this can be useful to determine what input type generated a click event.

import * as React from 'react';
import { Interactive, eventFrom } from 'react-interactive';

...

const handleClickEvent = React.useCallback((e) => {
  switch (eventFrom(e)) {
    case 'mouse':
      // click event from mouse
      break;
    case 'touch':
      // click event from touch
      break;
    case 'key':
      // click event from key
      break;
  }
}, []);

...

<Interactive
  as="button"
  onClick={handleClickEvent}
>
  My Button
</Interactive>

setEventFrom(inputType)

inputType: "mouse" | "touch" | "key"

This is useful when manually generating events. For example, when calling focus() on an <Interactive> component and you want it to enter the focusFromKey state.

import * as React from 'react';
import { Interactive, setEventFrom } from 'react-interactive';

...

const myButtonRef = React.useRef(null);

// called from someplace else in your code
const focusButton = () => {
  if (myButtonRef.current) {
    // so the <Interactive> component will enter the focusFromKey state
    setEventFrom('key');
    myButtonRef.current.focus()
  }
};

...

<Interactive
  as="button"
  ref={myButtonRef}
  focusFromKeyStyle={{ outline: '2px solid green' }}
>
  My Button
</Interactive>

Using with TypeScript

React Interactive is fully typed, including the polymorphic as prop. The props that an <Interactive> component accepts are a union of its own props and the props that the as prop accepts.

<Interactive
  as="a" // render as an anchor link
  href="https://rafgraph.dev" // TS knows href is a string b/c as="a"
>
  My Link
</Interactive>

Typing props passed to <Interactive>

Sometimes you need to type the props object that is passed to an <Interactive> component, to do this use the type InteractiveProps<as>.

import { Interactive, InteractiveProps } from 'react-interactive';

// props object passed to <Interactive>
// InteractiveProps includes types for `as` and `ref`
const propsForInteractiveButton: InteractiveProps<'button'> = {
  as: 'button',
  type: 'submit', // button specific prop
  ...
};

// for as={Component} use InteractiveProps<typeof Component>
const propsForInteractiveAsComponent: InteractiveProps<typeof Component> = {
  as: Component,
  ...
};

...

<Interactive {...propsForInteractiveButton} />
<Interactive {...propsForInteractiveAsComponent} />

Typing components that wrap <Interactive>

Sometimes when creating components that wrap an <Interactive> component you want to extend the <Interactive> component and pass through props to <Interactive>. To do this use the type InteractiveExtendableProps<as>.

Note that usually it makes more sense to create a limited interface for components that wrap <Interactive> instead of extending the entire <Interactive> component interface, but sometimes it is necessary.

import { Interactive, InteractiveExtendableProps } from 'react-interactive';

// the same props interface is used for wrapping with and without forwardRef
// note that InteractiveExtendableProps doesn't include `as` or `ref` props
// when using forwardRef the ref type will be added by the forwardRef function
interface WrapperProps
  extends InteractiveExtendableProps<'button'> /* OR InteractiveExtendableProps<typeof Component> */ {
  additionalProp?: string;
}

// without ref
const WrapperWithoutRef: React.VFC<WrapperProps> = ({
  additionalProp,
  ...props
}) => <Interactive {...props} as="button" />;

// with ref
const WrapperWithRef = React.forwardRef<
  HTMLButtonElement, // OR React.ElementRef<typeof MyComponent>
  WrapperProps
>(({ additionalProp, ...props }, ref) => (
  <Interactive {...props} as="button" ref={ref} />
));

CSS sticky hover bug

The CSS sticky hover bug on touch devices occurs when you tap an element that has a CSS :hover pseudo class. The :hover state sticks until you tap someplace else on the screen. This causes :hover styles to stick on touch devices and prevents proper styling of touch interactions (like native apps).

The reason for CSS sticky hover is that back in the early days of mobile the web relied heavily on hover menus, so on mobile you could tap to see the hover menu (it would stick until you tapped someplace else). Sites are generally no longer built this way, so now the sticky hover feature has become a bug.

React Interactive fixes the sticky hover bug by only entering the hover state from mouse input and creating a separate touchActive state for styling touch interactions.