Skip to content

A hero slider component. Includes autoplay, touch swiping, multiple navs, event callbacks, and more.

Notifications You must be signed in to change notification settings

rmolinamir/hero-slider

Repository files navigation

hero-slider

This package contains multiple components with a fair range of options to help developers quickly set up a hero slider.

Storybook NPM JavaScript Style Guide gzip size


Introduction

This package contains multiple components with a fair range of options to help developers quickly set up a hero slider. You can set the sliding animations, the background lazy loaded image animation, navs, buttons, callbacks, and even set your own buttons if you need to.

The idea behind the configurability was to set up clear boundaries between modules and components. The modules will control the behavior of the hero-slider, while the components themselves are self explanatory and mostly relevant to styling. The documentation will be divided in two main sections, Module, and Components.

Edit Hero Slider

Navbar Slider Basic Slider Zoom Slider Vertical Slider


Modules

The modules are clear boundaries that define the overall behavior of the HeroSlider component. You can configure these behaviors by passing the respective props to the HeroSlider.

Controller

This module will define the behavior slide transitions. You can set up the duration of the transitions, delays before the transitions begin, the initial active slide, callbacks for the transition events, and pointers to methods that will slide to the previous or next slides relative to the active slide.

interface ControllerProps {
  /**
   * Sliding duration, in milliseconds.
   * @default 500
   */
  slidingDuration?: number;
  /**
   * Sliding delay, in milliseconds.
   * @default 200
   */
  slidingDelay?: number;
  /**
   * The initial slide can also be set, but the slider starts at the first slide by default.
   * @default 1
   */
  initialSlide?: number;
  /**
   * Callback executed before sliding begins.
   * The previous and next slide numbers are received as arguments, since the sliding event can be delayed, this is useful to handle state changes from the outside (e.g. fire custom animations inside the active slide).
   * @param activeSlide
   * @param nextSlide
   * @default undefined
   */
  onBeforeSliding?(activeSlide: number, nextSlide: number): void;
  /**
   * Callback executed once the sliding starts similar to `onBeforeSliding`.
   * @param activeSlide
   * @param prevSlide
   * @default undefined
   */
  onSliding?(activeSlide: number, prevSlide: number): void;
  /**
   * Callback executed after the sliding ends similar to `onBeforeChange`.
   * @param activeSlide
   * @param prevSlide
   * @default undefined
   */
  onAfterSliding?(activeSlide: number, prevSlide: number): void;
  /**
   * Similar to pointers in C++, objects can work like pointers in JavaScript. React references are mutable objects that can be changed but will always point to an origin. If you declare an `object` and pass it as a reference, then the `current` property of the React reference `object` will be set to be equal to the `goToNextSlide` handler within the slider. This provides the developer with a way to change the slides "from the outside" of the bounds of the HeroSlider component.
   */
  goToNextSlidePointer?: React.MutableRefObject<GoToNextSlide | undefined>;
  /**
   * Similar to `nextSlide`, this sets the `object` to be equal to the `goToPreviousSlide` handler within the HeroSlider component.
   */
  goToPreviousSlidePointer?: React.MutableRefObject<
    GoToPreviousSlide | undefined
  >;
}

Animations

Defines which sliding animations will be used during slide transitions.

interface AnimationsProps {
  /**
   * The sliding animations during transitions.
   * @default 'wipe'
   */
  slidingAnimation?: 'fade' | 'wipe';
  /**
   * Fade in duration of the slider during mount, in milliseconds.
   * @default 100
   */
  sliderFadeInDuration?: number;
  /**
   * Navbars fade in duration, in milliseconds.
   * @default 1000
   */
  navbarFadeInDuration?: number;
  /**
   * Navbars fade in delay, in milliseconds.
   * @default 500
   */
  navbarFadeInDelay?: number;
  /**
   * When `true`, the `hero-slider` will know which animation should be set next.
   * For example, if the user is selecting the next slide, the animation would be different to the one if the user had selected the previous slide.
   * The animations will essentially be the same, but moving to different directions (e.g. left or right, or right to left).
   * @default true
   */
  shouldManageAnimationSequence?: boolean;
}

Autoplay

Autoplay is a feature that the Slider will support if enabled, it is disabled by default. The autoplay will activate a slide transition periodically after a certain duration, and a debounce will happen every time the user interacts with the Slider. The debounce duration can also be configured.

interface Props {
  /**
   * Autoplay duration, interval or duration betweens slide transitions, in milliseconds.
   * If it's lower than the sliding cycle duration (sliding duration + sliding delay), then the sliding cycle duration will be used instead.
   * @default 8000
   */
  autoplayDuration?: number;
  /**
   * Time (in milliseconds) in which the autoplay will be debounced if the user interacts with the slider.
   * The autoplay resumes if the user stops interacting after this duration.
   * Set as 0 to disable this feature.
   * @default 4000
   */
  autoplayDebounce?: number;
}

type AutoplayProps = Props | boolean;

Accessibility

Handles accessibility behaviors such as the orientation of the Slider (which affects the swipe motions used to command slide transtisions), and whether to render next and previous buttons.

/**
 * `AccessibilityOrientation` definition used for the `SliderProps.orientation` prop.
 * Used to define which swipes (depending on directions) will change the slides,
 * and where and how will the buttons render, if set to render.
 */
enum AccessibilityOrientation {
  VERTICAL = 'vertical',
  HORIZONTAL = 'horizontal'
}

interface AccessibilityProps {
  /**
   * Controls render of the next and previous buttons.
   * @default true
   */
  shouldDisplayButtons?: boolean;
  /**
   * When an arrow key is pressed, the active slide will change respectively to the pressed arrow.
   * The left and down arrows will set the previous slide, and the right and up arrows will set the next slide.
   * The left and right will only work if the slider is horizontal, and the up and down arrows will only work if the slider is vertical.
   * @default true
   */
  shouldSlideOnArrowKeypress?: boolean;
  /**
   * The slider orientation can either set to be `horizontal` or `vertical`.
   * The orientation sets the slide buttons respective to the orientation (e.g. if vertical, the buttons will be at the top and at the bottom).
   * Swipe (touch) gestures in mobile devices to change slides will also be configured automatically depending on the orientation (e.g. if horizontal, swiping vertically won't change slides).
   * @default 'horizontal'
   */
  orientation?: `${AccessibilityOrientation}`;
  /**
   * Pixel threshold for the Slider to register a swiping command to change slides.
   * @default 50
   */
  thresholdToSlide?: number;
}

Settings

The settings will allow you to set up CSS variables that will be available to HTML elements inside the slider, as well as debugging levels in case you are running into problems.

interface SettingsProps {
  /**
   * Sets up the `--slider-color` CSS variable.
   * @default 'inherit'
   */
  sliderColor?: CSS.Properties['color'];
  /**
   * Inline CSS styling for the wrapper div element of the component.
   * @default {}
   */
  sliderStyle?: Omit<CSS.Properties, 'width' | 'height'>;
  /**
   * Aesthetics settings. You can configure the base color and the active color of all nav components within the `HeroSlider`. They can be set individually as well.
   * @default
   * {
   *    color: undefined,
   *    activeColor: undefined
   * }
   */
  navbarStyle?: {
    color?: CSS.Properties['color'];
    activeColor?: CSS.Properties['color'];
  };
  /**
   * Debugger logs level. Only useful if you need insights.
   * @default
   * {
   *    verbose: false,
   *    info: false,
   *    debug: false,
   *    warnings: true,
   *    errors: true
   * }
   */
  debug?: LoggerLevels | undefined;
}

Manager

The Manager will handle the Slide components and perform other processes behind the scenes. You can optionally set up whether the user is on a mobile device, but this will default to a standard navigator validation.

interface ManagerProps {
  /**
   * Determines if on a mobile device. If true, the control buttons at the sides of the slider won't render.
   * @default /Mobi|Android/i.test(navigator.userAgent)
   */
  isMobile?: boolean;
}

Components

This package offers multiple components. Below you will find instructions for each component and their respective props.

HeroSlider

The main component and default of the package. The HeroSlider has wrap all of the other components, otherwise you will run into errors due to a lack of Context providers.

HeroSlider accepts the following props:

/**
 * `HeroSlider` props.
 */
interface HeroSliderProps {
  /**
   * Slider className.
   */
  className?: React.HTMLProps<HTMLDivElement>['className'];
  /**
   * CSS property. Defines the width of the slider.
   * @default '100%'
   */
  width?: React.CSSProperties['width'];
  /**
   * CSS property. Defines the height of the slider.
   * @default '100vh'
   */
  height?: React.CSSProperties['height'];
  /**
   * Inline CSS styling.
   */
  style?: Omit<React.CSSProperties, 'width' | 'height'>;

  // Modules
  manager?: ManagerProps;
  settings?: SettingsProps;
  controller?: ControllerProps;
  accessibility?: AccessibilityProps;
  animations?: AnimationsProps;
  autoplay?: AutoplayProps;
}

Example

<HeroSlider
  height="100vh"
  autoplay
  controller={{
    initialSlide: 1,
    slidingDuration: 500,
    slidingDelay: 100,
    onSliding: (nextSlide: number) =>
      console.debug('onSliding(nextSlide): ', nextSlide),
    onBeforeSliding: (previousSlide: number, nextSlide: number) =>
      console.debug(
        'onBeforeSliding(previousSlide, nextSlide): ',
        previousSlide,
        nextSlide
      ),
    onAfterSliding: (nextSlide: number) =>
      console.debug('onAfterSliding(nextSlide): ', nextSlide)
  }}
>
...

HeroSlider Slide

The Slide component holds whatever children you want to be part of each slide, you can also modify the background and its initial mount animation. Bear in mind that background images are lazy loaded.

The Slide component accepts the following props:

/**
 * `Slide` component props.
 */
interface SlideProps {
  /**
   * Slider className.
   */
  className?: React.HTMLProps<HTMLDivElement>['className'];
  /**
   * Each slide has a "Mask" that serves as an adornment.
   * They mimic the background, then offsets it a bit. It has an animation during slide transitions.
   * @default false
   */
  shouldRenderMask?: boolean;
  /**
   * Defines the background of the `Slide`.
   * You may pass CSS properties just like you would style the background of a regular HTML element.
   * The main difference is that the `backgroundImage` property will work just like an image `src` property instead of the typical background image URL.
   */
  background?: Partial<BackgroundProps>;
  /**
   * If the developer is using a `MenuNav` or `ButtonsNav` component, a label for each slide may be passed.
   * These labels will be shown in the nav components.
   */
  label?: string;
  /**
   * Inline CSS styling.
   */
  style?: React.CSSProperties;
  /**
   * Callback that executes when the background image loads.
   */
  onBackgroundLoad?: BackgroundProps['onLoad'];
}

Slide Background prop

The background of the Slide components can be configured just as you would configure the background of any element, with the added bonus of lazy loading and being able to pass data to the alt image attribute.

/**
 * Type definition for `BackgroundProps.backgroundAnimation`.
 */
enum BackgroundAnimation {
  FADE = 'fade',
  ZOOM = 'zoom'
}

/**
 * `BackgroundProps` interface for the `Background` JSX
 * component's props used inside the `Slide` components.
 * The `Slide` components `background` prop is also defined
 * by `BackgroundProps`.
 */
interface BackgroundProps {
  /**
   * CSS property. Defines the width of the background.
   * @default '100%'
   */
  width?: CSS.Properties['width'];
  /**
   * CSS property. Defines the height of the background.
   * @default '100%'
   */
  height?: CSS.Properties['height'];
  backgroundColor?: CSS.Properties['backgroundColor'];
  backgroundAnimationDuration?: CSS.Properties['animationDuration'];
  backgroundAnimationDelay?: CSS.Properties['animationDelay'];
  /**
   * Background animation after the image loads.
   * There are currently two options, a fade-in animation, or a zoom in animation that lasts 30 secs, the background zooms in until it reaches its original size.
   * @default 'fade'
   */
  backgroundAnimation?: `${BackgroundAnimation}`;
  /**
   * Background blend mode CSS property **for the optional mask that could render in each of the Slide components**.
   */
  maskBackgroundBlendMode?: CSS.Properties['backgroundBlendMode'];
  /**
   * Background image.
   */
  backgroundImageClassName?: HTMLImageElement['sizes'];
  backgroundImageBlendMode?: CSS.Properties['mixBlendMode'];
  backgroundImageSizes?: HTMLImageElement['sizes'];
  backgroundImageSrcSet?: HTMLImageElement['srcset'];
  backgroundImageSrc?: HTMLImageElement['src'];
  backgroundImageAlt?: HTMLImageElement['alt'];
  backgroundImageStyle?: React.CSSProperties;
  /**
   * Boolean variable to allow or disable lazy loading.
   * @default true
   */
  shouldLazyLoad?: boolean;
  onLoad?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void;
}

Slide Example

<Slide
  background={{
    backgroundImage: salta
  }}
>
  <div>Salta - Argentina</div>
</Slide>

Nav

The basic Nav component, worth noting that there are three other types of (slide) navigation components named SideNav, MenuNav, and ButtonsNav. This component is nothing more than a nagivation bar. By default the component is positioned at the bottom, centered.

/**
 * Defines the position of the navigation component.
 */
interface NavPosition {
  top?: React.CSSProperties['top'];
  left?: React.CSSProperties['left'];
  bottom?: React.CSSProperties['bottom'];
  right?: React.CSSProperties['right'];
  transform?: React.CSSProperties['transform'];
}

/**
 * `Nav` component props.
 */
interface NavProps {
  /**
   * Object of CSS properties `top`, `left`, `bottom`, and `right` used to absolutely position elements.
   * Aside from the former, you can also set the CSS `transform` property to help you center the element.
   * @default
   * {
   *    bottom: '1.5rem',
   *    left: '50%',
   *    transform: 'translateX(-50%)'
   * }
   */
  position?: NavPosition;
  /**
   * Defines `--nav-color` CSS variable.
   */
  color?: string;
  /**
   * Defines `--nav-active-color` CSS variable.
   */
  activeColor?: string;
}

NavPosition

The position settings are nothing more than the top, left, bottom, and right CSS properties used by absolutely position elements. Aside from the former, you can also set the CSS transform property to help you position the element.

NavExample

<HeroSlider
  ...
>
  ...

  <Slide
    ...
  />

  <Slide
    ...
  />

  <Nav />
</HeroSlider>

SideNav

When it comes to props it extends the props of the Nav component. There are a couple more props that can be passed to this component. Aside from that, it's worth mentioning that this component is intented to be placed at the sides of the slider.

/**
 * `SideNav` component props.
 */
interface SideNavProps extends NavProps {
  /**
   * Defines the inline CSS property `right` of the component.
   */
  right?: React.CSSProperties['right'];
  /**
   * Defines the inline CSS property `left` of the component.
   */
  left?: React.CSSProperties['left'];
  /**
   * Defines the position. If you set it to the left, set this to false to disable any existing `right` CSS properties and avoid any conflicts.
   * @default true
   */
  isPositionedRight?: boolean;
  /**
   * Object of CSS properties `top`, `left`, `bottom`, and `right` used to absolutely position elements.
   * Aside from the former, you can also set the CSS `transform` property to help you center the element.
   * @default
   * {
   *    bottom: undefined,
   *    top: '50%',
   *    left: !isPositionedRight ? left || '1rem' : undefined,
   *    right: isPositionedRight ? right || '1rem' : undefined,
   *    transform: 'translateY(-50%)'
   * }
   */
  position?: NavPosition;
}

SideNav Example

This example would render two SideNav components at both sides of the screen:

<HeroSlider
  ...
>
  ...

  <Slide
    ...
  />

  <SideNav
    isPositionedRight={false}
    position={{
      top: '50%',
      left: '0',
      transform: 'translateY(-50%)'
    }}
  />

  <SideNav />
</HeroSlider>

MenuNav

Extends from the Nav component, with a few additional props.

/**
 * `MenuNav` component props.
 */
interface MenuNavProps extends NavProps {
  /**
   * Determines how the browser distributes space between and around nav items along the component.
   */
  justifyContent?: React.CSSProperties['justifyContent'];
  /**
   * Given the nature of this component, it doesn't work well with devices of relatively small width.
   * The mobile threshold is the point in which this component turns into a basic `Nav` component or `null`.
   */
  mobileThreshold?: number;
  /**
   * Determines if the nav should render `null` or a basic `Nav` component after the threshold is reached.
   * @default false
   */
  isNullAfterThreshold?: boolean;
  /**
   * An extra button rendered among the nav items in case the developer may want any extra functionality in the component.
   */
  extraButton?: React.ReactNode;
  /**
   * Renders the button to the right side of the nav if true, otherwise it will appear at the left side.
   * @default true
   */
  isExtraButtonRight?: boolean;
}

MenuNav Example

<HeroSlider
  ...
>
  ...

  <Slide
    ...
  />

  <MenuNav />
</HeroSlider>

ButtonsNav

Extends from the previous MenuNav component props. It works the same, with the addition of additional props defined as alignItems and backgroundColor. The alignItems prop will align the slide navigation items relative to the Slider. The backgroundColor sets the background of the buttons, while the color prop is used for the color of the text, and the activeColor prop is used for the background of the active navigation item.

/**
 * `ButtonsNav` component props.
 */
interface ButtonsNavProps extends MenuNavProps {
  /**
   * CSS background color property for the nav buttons.
   */
  backgroundColor?: React.CSSProperties['backgroundColor'];
  /**
   * Aligns the nav items to the center, top, or bottom of its container working exactly as how the CSS flex-box `align-items` property works.
   */
  alignItems?: React.CSSProperties['alignItems'];
}

AutoplayButton

The AutoplayButton component allows the user to control the behavior of the Autoplay module. This button will play or pause the autoplay when clicked. It's really easy to setup.

You can position it just like you would position a navigation component by using passing the position prop, but you may also pass a CSS class or inline styling as props.

/**
 * `AutoplayButton` component props.
 */
interface AutoplayButtonProps {
  /**
   * CSS class name.
   */
  className?: React.HTMLAttributes<HTMLElement>['className'];
  /**
   * Object of CSS properties `top`, `left`, `bottom`, and `right` used to absolutely position elements.
   * Aside from the former, you can also set the CSS `transform` property to help you center the element.
   * @default
   * {
   *    bottom: '0',
   *    left: '0'
   * }
   */
  position?: NavPosition;
  /**
   * Inline CSS styling.
   */
  style?: React.CSSProperties;
}

AutoplayButton Example

<HeroSlider
  ...
>
  <Overlay>
    ...
    <AutoplayButton />
  </Overlay>

  ...
</HeroSlider>

Overlay

The Overlay is a useful component that will superpose its children over the content of the Slide components.

/**
 * `Overlay` component props.
 */
interface OverlayProps {
  /**
   * Slider className.
   */
  className?: React.HTMLProps<HTMLDivElement>['className'];
}

Overlay Example

<HeroSlider
  ...
>
  <Overlay>
    <div>On Top of the World</div>
  </Overlay>

  <Slide
    ...
  />

  <Nav />
</HeroSlider>

React Server Components

If you are working with React Server Components (e.g. Next.js), it recommended to import the CSS file of the package to avoid "popping" effects when the CSS is loaded. You can do this by importing the index.css file from the hero-slider package. For example:

'use client';
import 'hero-slider/dist/index.css';
import HeroSlider, { Nav, Overlay, Slide } from 'hero-slider';

export default function BasicSlider() {
  return (
    <HeroSlider className='h-full w-full'>
      ...
    </HeroSlider>
  );
}

If you don't import the CSS file, the HeroSlider component will still work, but the CSS will be loaded after the component is mounted, which will cause a "popping" effect.

Development

This project is using Storybook for development. On top of that, it's using Turborepo to manage the monorepo. The project is divided in two main packages, hero-slider and storybook. The former is the package that contains the hero-slider component, and the latter is a package that runs a Storybook instance to showcase the hero-slider component, interact, and do manual end-to-end testing.

The hero-slider package is using Rollup to bundle the code, and TypeScript to type check the code.

Finally, the project is using Prettier and ESLint to format and lint the code, Jest for unit testing, and pnpm as the package manager.

Below you will find instructions on how to install, develop, build, and test the project.

Install

To install the project, you will need to install pnpm first. Then, you can run the following command:

pnpm i

Develop

When developing, you will need to run the following command:

pnpm dev

This will start the Storybook instance and a watcher for the hero-slider package.

Build

To build the project, you will need to run the following command:

pnpm build

Test

To test the project, you will need to run the following command:

pnpm test

License

MIT © rmolinamir