rshinybslib

How to set up a horizontally scrollable navset?


I'm building a ShinyApp with several nav_panel per page. The default behaviour of shiny/bslib is to separate panel in multiple rows:

See code and image:

library(shiny)
library(bslib)

# UI
ui <- page_fixed(
  theme = bs_theme(),
  
  # Basic use of navset_underline to showcase tabs with long names
  navset_underline(
    nav_panel("Very Long Name 1", "Content Tab 1"),
    nav_panel("Very Long Name 2", "Content Tab 2"),
    nav_panel("Very Long Name 3", "Content Tab 3"),
    nav_panel("Very Long Name 4", "Content Tab 4"),
    nav_panel("Very Long Name 5", "Content Tab 5"),
    nav_panel("Very Long Name 6", "Content Tab 6"),
    nav_panel("Very Long Name 7", "Content Tab 7"),
    nav_panel("Very Long Name 8", "Content Tab 8"),
    nav_panel("Very Long Name 9", "Content Tab 9"),
  )
)

# Server
server <- function(input, output, session) {}

# Run the app
shinyApp(ui = ui, server = server)


enter image description here

Instead, I'd like to have the panel in a single row with arrows to scroll non-visible panel. ChatGPT gave me this working solution, but is there a better, less hacky way to reach this goal? Maybe something working also with page_fluid. I don't like HTML inserts in code like this:

library(shiny)
library(bslib)

ui <- page_fixed(
  theme = bs_theme(),
  tags$head(
    tags$style(HTML("
      /* Wrapper for arrows */
      .navset-scroll-wrapper {
        position: relative;
        display: flex;
        align-items: center;
      }

      /* Navigatin bar style */
      .nav.nav-underline {
        display: flex;
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch; /* scrolling fluido su mobile */
        scroll-behavior: smooth; /* scrolling fluido */
        margin-bottom: 0; /* Evita spazi sotto la barra */
      }

      /* Tab*/
      .nav.nav-underline .nav-item {
        flex-shrink: 0;
      }

      /* Remove visible scrollbar  */
      .nav.nav-underline::-webkit-scrollbar {
        display: none;
      }

      /* Arrow Style */
      .scroll-arrow {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        z-index: 10;
        cursor: pointer;
        font-size: 20px;
        color: #333;
        background-color: #fff;
        border: 1px solid #ddd;
        border-radius: 50%;
        padding: 5px;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .scroll-arrow.left {
        left: -30px; /* Posiziona la freccia a sinistra */
      }

      .scroll-arrow.right {
        right: -30px; /* Posiziona la freccia a destra */
      }
    ")),
    tags$script(HTML("
      // Add horizontal scrolling
      document.addEventListener('DOMContentLoaded', function() {
        const leftArrow = document.querySelector('.scroll-arrow.left');
        const rightArrow = document.querySelector('.scroll-arrow.right');
        const navTabs = document.querySelector('.nav.nav-underline');

        leftArrow.addEventListener('click', () => {
          navTabs.scrollBy({ left: -100, behavior: 'smooth' });
        });

        rightArrow.addEventListener('click', () => {
          navTabs.scrollBy({ left: 100, behavior: 'smooth' });
        });
      });
    "))
  ),
  tags$div(
    class = "navset-scroll-wrapper",
    tags$div(class = "scroll-arrow left", "❮"), # Left Arrow
    navset_underline(
      nav_panel("Very Long Name 1", "Content Tab 1"),
      nav_panel("Very Long Name 2", "Content Tab 2"),
      nav_panel("Very Long Name 3", "Content Tab 3"),
      nav_panel("Very Long Name 4", "Content Tab 4"),
      nav_panel("Very Long Name 5", "Content Tab 5"),
      nav_panel("Very Long Name 6", "Content Tab 6"),
      nav_panel("Very Long Name 7", "Content Tab 7"),
      nav_panel("Very Long Name 8", "Content Tab 8"),
      nav_panel("Very Long Name 9", "Content Tab 9"),
    ),
    tags$div(class = "scroll-arrow right", "❯") # Right Arrow
  )
)

server <- function(input, output, session) {}

shinyApp(ui = ui, server = server)

enter image description here


Solution

  • I solved the problem. Here is the solution:

    script.js

    $(document).ready(function() {
      $('.scroll-left').click(function() {
        $('.nav-underline').animate({ scrollLeft: '-=200' }, 300);
      });
      
      $('.scroll-right').click(function() {
        $('.nav-underline').animate({ scrollLeft: '+=200' }, 300);
      });
    });
    

    styles.css

    .nav-container {
      position: relative;
      padding: 0 40px;  /* Space for buttons */
    }
    
    .nav-underline {
      flex-wrap: nowrap !important;
      overflow-x: scroll;
      scrollbar-width: none;  /* Firefox */
      -ms-overflow-style: none;  /* IE and Edge */
      white-space: nowrap;
    }
    
    .nav-underline::-webkit-scrollbar {
      display: none;  /* Chrome, Safari, Opera */
    }
    
    .nav-item {
      white-space: nowrap;
    }
    
    .scroll-button {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      z-index: 1000;
      background: #ffffff;
      border: 1px solid #dee2e6;
      border-radius: 4px;
      padding: 8px;
      cursor: pointer;
    }
    
    .scroll-left { left: 0; }
    .scroll-right { right: 0; }
    

    app.R

    library(shiny)
    library(bslib)
    
    # UI
    ui <- page_fluid(
      theme = bs_theme(),
      
      tags$head(
        tags$link(rel = "stylesheet", type = "text/css", href = "styles.css"),
        tags$script(src = "script.js")
      ),
      
      div(class = "nav-container",
          tags$button(icon("chevron-left"), class = "scroll-button scroll-left"),
          
          navset_underline(
            nav_panel("Very Long Name 1", "Content Tab 1"),
            nav_panel("Very Long Name 2", "Content Tab 2"),
            nav_panel("Very Long Name 3", "Content Tab 3"),
            nav_panel("Very Long Name 4", "Content Tab 4"),
            nav_panel("Very Long Name 5", "Content Tab 5"),
            nav_panel("Very Long Name 6", "Content Tab 6"),
            nav_panel("Very Long Name 7", "Content Tab 7"),
            nav_panel("Very Long Name 8", "Content Tab 8"),
            nav_panel("Very Long Name 9", "Content Tab 9")
          ),
          
          tags$button(icon("chevron-right"), class = "scroll-button scroll-right")
      )
    )
    
    # Server
    server <- function(input, output, session) {
    }
    
    shinyApp(ui = ui, server = server)