Skip to content

Instantly share code, notes, and snippets.

@julianocomg
Last active May 17, 2024 04:24
Show Gist options
  • Save julianocomg/296469e414db1202fc86 to your computer and use it in GitHub Desktop.
Save julianocomg/296469e414db1202fc86 to your computer and use it in GitHub Desktop.
A simple affix React component.
/**
* @author Juliano Castilho <julianocomg@gmail.com>
*/
var React = require('react');
var AffixWrapper = React.createClass({
/**
* @type {Object}
*/
propTypes: {
offset: React.PropTypes.number
},
/**
* @return {Object}
*/
getDefaultProps() {
return {
offset: 0
};
},
/**
* @return {Object}
*/
getInitialState() {
return {
affix: false
};
},
/**
* @return {void}
*/
handleScroll() {
var affix = this.state.affix;
var offset = this.props.offset;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if (!affix && scrollTop >= offset) {
this.setState({
affix: true
});
}
if (affix && scrollTop < offset) {
this.setState({
affix: false
});
}
},
/**
* @return {void}
*/
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
},
/**
* @return {void}
*/
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
},
render() {
var affix = this.state.affix ? 'affix' : '';
var {className, offset, ...props} = this.props;
return (
<div {...props} className={className + ' ' + affix)}>
{this.props.children}
</div>
);
}
});
module.exports = AffixWrapper;
<AffixWrapper className="some-cool-element" id="lalala" offset={200}>
<div>Put whatever you want here</div>
</AffixWrapper>
@dlwalsh
Copy link

dlwalsh commented Nov 17, 2015

This was really useful. Thanks.

There is one improvement to be had though. Firefox doesn't recognise document.body.scrollTop. It does recognise document.documentElement.scrollTop, which is apparently the more standards compliant way.

However, Chrome and Safari don't recognise document.documentElement.scrollTop, so you need to have both:

var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

@fastfrwrd
Copy link

would you ever consider publishing this on npm?

@Ehesp
Copy link

Ehesp commented Jul 5, 2016

Here's an ES6 version of this:

import React, { Component, PropTypes } from 'react';

class Affix extends Component {

  static propTypes = {
    offset: PropTypes.number,
  };

  static defaultProps = {
    offset: 0,
  };

  constructor() {
    super();
    this.state = {
      affix: false,
    };
  }

  componentDidMount() {
    window.addEventListener('scroll', this.handleScroll);
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll);
  }

  handleScroll = () => {
    const affix = this.state.affix;
    const offset = this.props.offset;
    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    if (!affix && scrollTop >= offset) {
      this.setState({
        affix: true,
      });
    }

    if (affix && scrollTop < offset) {
      this.setState({
        affix: false,
      });
    }
  };

  render() {
    const affix = this.state.affix ? 'affix' : '';
    const { className, ...props } = this.props;

    return (
      <div {...props} className={`${className || ''} ${affix}`}>
        {this.props.children}
      </div>
    );
  }
}

export default Affix;

@Ehesp
Copy link

Ehesp commented Jul 5, 2016

@shuchenliu
Copy link

Hey there! I am a React newbie so this question may seem naive, but I'll shoot anyway: how does the render() function make sure the target element is fixed at the top/certain level? Just by adding affix to class?

@jeffvandyke
Copy link

jeffvandyke commented Aug 8, 2017

For future readers: @shuchenliu, yep. .affix is a bootstrap class that enables position: fixed, see the bootstrap docs at http://getbootstrap.com/javascript/#affix for details. You still need to add your own css rules to make sure that it has the right position.

@d8660091
Copy link

d8660091 commented Oct 9, 2019

A functional component with react hook alternative that preserve the width

import React from 'react';

export default function Affix(props: {
  top: number;
  children: React.ReactNode;
  offset?: number;
  className?: string;
}) {
  const element = React.createRef<HTMLDivElement>();
  let oldStyles = {
    position: '',
    top: '',
    width: '',
  };
  // Offset could make the element fixed ealier or later
  const { offset = 0 } = props;

  const checkPosition = (distanceToBody: number, width: number) => {
    const scrollTop = window.scrollY;

    if (distanceToBody - scrollTop < props.top + offset) {
      if (element.current.style.position != 'fixed') {
        for (let key in oldStyles) {
          oldStyles[key] = element.current.style[key];
        }
        element.current.style.position = 'fixed';
        element.current.style.width = width + 'px';
        element.current.style.top = props.top + 'px';
      }
    } else {
      // reset to default
      for (let key in oldStyles) {
        element.current.style[key] = oldStyles[key];
      }
    }
  };

  React.useEffect(() => {
    if (typeof window.scrollY === 'undefined') {
      // don't work in IE
      return;
    }

    const distanceToBody = window.scrollY + element.current.getBoundingClientRect().top;
    const handleScroll = () => {
      requestAnimationFrame(() => {
        checkPosition(distanceToBody, element.current.clientWidth);
      });
    };

    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  });

  return (
    <div ref={element} style={{ zIndex: 1 }} className={props.className}>
      {props.children}
    </div>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment