Neoanthropic Preternatural Murmurings


    4.2.1 • Public • Published

    Core Scroll

    @nrk/core-scroll enhances any tag with content to be scrollable with mouse interaction on non-touch-devices. It also hides the scrollbars and automatically disables animation for users who prefers reduced motion.


    <button data-for="my-scroll-js" value="up" aria-label="Rull opp">&uarr;</button>
    <button data-for="my-scroll-js" value="down" aria-label="Rull ned">&darr;</button>
    <button data-for="my-scroll-js" value="left" aria-label="Rull til venstre">&larr;</button>
    <button data-for="my-scroll-js" value="right" aria-label="Rull til høyre">&rarr;</button>
    <div class="my-wrap my-wrap-js">
      <core-scroll id="my-scroll-js" class="my-scroll">
        <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>
        <div>1</div><div><div class="my-wrap">
          <core-scroll class="my-scroll">
            <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>
            <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>
            <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>
        </div></div><div>3</div><div>4</div><a href="#">5</a>
        <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>
        <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>

    Example: React

    <div id="jsx-scroll"></div>
    <script type="text/jsx">
      class MyScroll extends React.Component {
        constructor (props) {
          this.state = {}
          this.onScroll = this.onScroll.bind(this)
        onScroll ({target}) {
            left: target.scrollLeft ? () => target.scroll('left') : null,
            right: target.scrollRight ? () => target.scroll('right') : null
        render () {
          return <div>
            <button disabled={!this.state.left} onClick={this.state.left}>Left JSX</button>
            <button disabled={!this.state.right} onClick={this.state.right}>Right JSX</button>
            <div className="my-wrap">
              <CoreScroll className="my-scroll" onScrollChange={this.onScroll}>
                <div>1</div><div>2</div><div>3</div><div>4</div><a href="#">5</a>
      ReactDOM.render(<MyScroll />, document.getElementById('jsx-scroll'))

    Example: custom child items

    core-scroll calculates scroll distance based on currently visible direct children. When using substructures like <ul><li>... you, must tell core-scroll what elements are considered items, by using the items attribute/property:

    <core-scroll items="li">
        <li>List-item 1</li>
        <li>List-item 2</li>
    <button data-for="my-scroll-child" value="left" aria-label="Rull til venstre">&larr;</button>
    <button data-for="my-scroll-child" value="right" aria-label="Rull til høyre">&rarr;</button>
    <div class="my-wrap">
      <core-scroll id="my-scroll-child" class="my-scroll" items="li">
          <li>List-item 1</li><li>List-item 2</li><li>List-item 3</li><li>List-item 4</li>
          <li>List-item 5</li><li>List-item 6</li><li>List-item 7</li><li>List-item 8</li>
          <li>List-item 9</li><li>List-item 10</li><li>List-item 11</li><li>List-item 12</li>


    Using NPM provides own element namespace and extensibility. Recommended:

    npm install @nrk/core-scroll  # Using NPM

    Using static registers the custom element with default name automatically:

    <script src=""></script>  <!-- Using static -->

    Remember to polyfill custom elements if needed.


    Buttons can control a core-scroll by targeting its ID and specifying a direction. The disabled attribute is automatically added/removed to controller buttons when there is no more pixels to scroll in specified direction. Important: core-scroll manipulates styling to hide scrollbars, see how to work with margin and height →

      data-for="my-scroll-js"      <!-- {String} Id of <core-scroll> -->
      value="up"              <!-- {String} Sets direction of scroll. Possible values: "left", "right", "up" or "down" -->
      aria-label="Rull opp">  <!-- {String} Sets label -->
      id="my-scroll-js"       <!-- {String} Id corresponding to for attribute of <button> -->
      friction=".2">          <!-- {Number} Optional. Default 0.8. Controls scroll speed. Lower friction means higher speed -->
      <div>1</div>            <!-- Direct children is used to calculate natural stop points for scroll -->
    import CoreScroll from '@nrk/core-scroll'                 // Using NPM
    window.customElements.define('core-scroll', CoreScroll)   // Using NPM. Replace 'core-scroll' with 'my-scroll' to namespace
    const myScroll = document.querySelector('core-scroll')
    // Getters
    myScroll.scrollLeft                   // Amount of pixels remaining in scroll direction left
    myScroll.scrollRight                  // Amount of pixels remaining in scroll direction right
    myScroll.scrollTop                    // Amount of pixels remaining in scroll direction up
    myScroll.scrollBottom                 // Amount of pixels remaining in scroll direction down
    myScroll.items                        // Get all items
    // Setters
    myScroll.items                        // Set to String to specify scroll children (see example above)
    // Methods
    myScroll.scroll('left')               // Scroll in specified direction
    myScroll.scroll({x: 0, y: 10})        // Scroll to exact position
    myScroll.scroll({x: 0, move: 'down'}) // Scroll with position and direction

    React / Preact

    import CoreScroll from '@nrk/core-scroll/jsx'
    <CoreScroll friction={Number}         // Optional. Default 0.8. Controls scroll speed
                ref={(comp) => {}}        // Optional. Get reference to React component
                forwardRef={(el) => {}}   // Optional. Get reference to underlying DOM custom element
                onScrollChange={Function} // Optional. Scroll change event handler
                onScrollClick={Function}> // Optional. Scroll click event handler
      {/* elements */}


    Note: Starting a core-scroll mousemove inside a iframe, and releasing the mouse outside, will fail to end movement. This is due to mouseup not bubbling though iframes. Please avoid iframes.


    Fired regularly during a scroll. The event is throttled to run every 500ms and ensure better performance:

    document.addEventListener('scroll.change', (event) => {   // The scroll element

    Fired when clicking a button controlling core-scroll:

    document.addEventListener('', (event) => {        // The scroll element
      event.detail.move   // Direction to move (left, right, up, down)


    A native event fired for every scrolled pixel. Be cautious about performance when listening to scroll; heavy or many read/write operations will slow down your page. The event does not bubble, and you therefore need useCapture set to true when listening for scroll events from a parent element:

    document.addEventListener('scroll', (event) => {        // NB: Can be any scrolling element since this is a native event
      // Example check if the is the correct @nrk/core-scroll
      if ( === 'ID-OF-MY-CORE-SCROLL-HERE') {
        // Do Something
    }, true) // Note the true parameter, activating capture listening


    Scrollbar hiding

    core-scroll adds negative margins in some browsers to hide scrollbars. Therefore, make sure to place core-scroll inside a wrapper element with overflow: hidden:

    <div style="overflow:hidden"><core-scroll>...</core-scroll></div>

    Setting height

    By default, core-scroll scales based on content. If you want to set a fixed height, set this on the wrapper element (not directly on the core-scroll element):

    Do 🚫 Don't
    <div style="overflow:hidden;height:200px"><core-scroll>...</core-scroll></div> <div style="overflow:hidden"><core-scroll style="height:200px"></core-scroll></div>

    Button states

    The <button> elements receive disabled attributes reflecting the current scroll state:

    .my-scroll-button {}                  /* Target button in any state */
    .my-scroll-button:disabled {}         /* Target button in disabled state */
    .my-scroll-button:not(:disabled) {}   /* Target button in enabled state */

    NB: Safari scrollbar bug

    If you are creating a horizontal layout, you might experience unwanted vertical scrolling in Safari. This happens when children of @nrk/core-scroll have half-pixel height values (due to images/videos/elements with aspect-ratio sizing). Avoid the vertical scrolling by setting padding-bottom: 1px on the @nrk/core-scroll element.

    NB: iOS 12.2+ bug

    core-scroll automatically adds -webkit-overflow-scrolling: touch as this is required by iOS to enable momentum scrolling. An unfortunate side effect (introduced in iOS 12.2) is that the scrollable area is rendered on the GPU, which breaks position: fixed on child elements. Please place elements with position: fixed (i.e. a <core-dialog>) outside the markup of <core-scroll>.




    npm i @nrk/core-scroll

    DownloadsWeekly Downloads






    Unpacked Size

    164 kB

    Total Files


    Last publish


    • suxiang
    • norasv
    • eschoien
    • unaons
    • trulsl
    • syndrol
    • henit
    • mslhm
    • cbjerkan
    • hermangudesen
    • andreeldareide
    • henningkoller
    • davhu
    • oleob
    • meisen
    • espenhalstensen
    • hakonknutzen2
    • danjohnrk
    • olapeter
    • teodor-elstad
    • michaelpande
    • lysebraate
    • yngvar-nrk
    • lorecaster
    • wwalmnes
    • nrk-ps-teamcity
    • vincent.andersson
    • swla
    • nrk-midas-jenkins
    • andorpandor
    • jarlelin
    • aevare
    • nrkrichard
    • gesi
    • gundelsby-nrk
    • jonstalecarlsen
    • machineboy
    • vagifabilov
    • nrk-sofie-ci
    • nytamin
    • jesperstarkar
    • loftum
    • emte123
    • skjalgepalg
    • eirikhalvard
    • astokke
    • n640071
    • n07073
    • kristian.rosenvold
    • henrik-mattsson
    • eirikbacker
    • haavardm
    • popeindustries
    • yr
    • nrk-kurator-jenkins
    • toshb
    • torgeilo
    • nrk-user-sync
    • dhdeploy
    • janerikbr
    • espenwa
    • rogerhmar
    • ovstetun
    • eivind
    • stianlj
    • haraldkj
    • mariusu
    • spesialsnorre
    • cristobal
    • knuthaug
    • thohalv
    • johnarne
    • nrk-sup-jenkins
    • eshaswini
    • morrow
    • oyvindeh
    • laat
    • toggu
    • nrk-jenkins
    • plomma
    • evjand
    • moltubakk
    • ingridguren
    • lu-lux
    • andersli
    • silje
    • stiandg
    • sjurlur
    • anderscan
    • andipodnrk
    • pkej
    • yosrimti
    • zenangst
    • morten.nyhaug
    • ingvildcath
    • erlend.jones
    • brneirik
    • mollerse
    • eriksalhus
    • frdrks
    • tbnrk
    • nordanke
    • balte
    • mikaelrss
    • simonmitternacht
    • christiankarlsson9
    • martintorgersen
    • rebchr
    • steipal
    • discobus
    • ingvesund
    • martingundersen
    • tinkajts
    • hallvardlid
    • tomivar
    • ajaco
    • tobinus
    • mortenok
    • nrk-ark-deploy
    • jeangilbertlouis
    • heidimork
    • ingriddraagen
    • fridajalborg
    • bruusi
    • rosvoll
    • christianeide
    • enordby
    • artzag
    • glen_imrie
    • mia.aasbakken
    • elathamna
    • evjjan17
    • olatoft
    • kongsrud
    • chrpeter
    • ingvildforseth
    • esseb
    • talepre
    • haraldk76
    • stigok
    • dagfinno
    • johannesodland
    • anders993
    • vildefj
    • vildepk
    • malinaandahl
    • andreakn
    • rolerboler
    • meloygutt
    • anders.baggethun