Saturday, February 21, 2026

Shiny: Highlighting DT Rows

I am working on a Shiny application involving data tables (displayed using the DT library). There is one particular table in which I would like my code to highlight rows meeting certain criteria, which the application will evaluate. Not knowing how to set a background color for an entire row in a table, I went down a rabbit hole with Shiny Assistant, the relatively knew AI bot trained to help with Shiny coding. It took way too many attempts, but eventually I stumbled on prompts that got me more or less what I need.

It turns out there are some tricky bits, so I put together a small demonstration application. The demo app displays a portion of the mtcars dataset (which installs with R) and lets the user select, via check boxes, which rows to highlight. You can download the source code here. I will just mention a few things about the code.

  • The check boxes to select which rows are highlighted belong to a single checkboxGroupInput control named "selected". Initially I used observeEvent() to watch for changes to input$selected, which almost worked. The problem was that when the user removed the last check mark, signalling that no rows should be highlighted, the observer did not see an event and the table continued to have one highlighted row. Switching to observe()fixed the part about not seeing an event. I have no explanation as to why. See the next paragraph for the rest of the fix.
  • Early on, the table itself would not display until there was at least one row selected for highlighting. After switching to observe(), removing the last checkmark still failed to clear all formatting. I was forced to use an if-else approach (the highlightRows() function), in which the code applies the row formatting only if at least one row is selected. I am still in the dark as to why this is necessary.
  • The formatting code ends up being translated to JavaScript to communicate with the DataTables library. DataTables uses zero-based indexing, which explains why the specification of which columns to highlight (in this case, all of them) is 0:(ncol(df) - 1) rather than 1:ncol(df).
  • Using reactiveVal() may be overkill in the demo application, since input$selected is itself reactive (I think) (maybe). I used it in part because I intend to use it in the larger application I am building, and I wanted to be sure it worked.

 

Tuesday, February 3, 2026

Finding a Row that Moved

This post deals with programmatically scrolling a table in a Shiny app (using the DT library) to a specific row. 

I've been working on a Shiny app involving a number of tables, which are displayed using the R DT package. The tables include controls to let the user sort them by any column, which plays a role in today's post. Controls external to the table let the user insert, delete and edit records. The reason I use external controls and not the editing capabilities built into DT is that there are a number of limitations on edits that the application code enforces.

Before getting into the weeds, I need to establish a bit of terminology. The underlying data in any table has associated row numbers, just as in a spreadsheet. Those do not change when the user sorts the displayed table. I'll refer to that as the "row index", and to the position (1 if first, 2 if second, etc.) of a row in the displayed table as the "display index". If the seventeenth row of a table winds up at the top after sorting, it has row index 17 and display index 1. (Sorry if I'm belaboring the obvious.)

Here is the problem I ran into. Suppose that a user selects a row and edits it. When the user commits the change, DT updates the display, and the edited row may wander quite far in either direction and in particular leave the portion of the table being displayed. So I want the application to automatically scroll the table to the new location of the edited record. I initially assumed this would be easy. It was not. In fact, 20+ attempts with the Shiny Assistant coding AI and 10+ attempts with Claude AI failed to solve the problem (although they did produce some insights, and a bit of code described below).

We can split the problem into two parts: finding the new location of the record, and then scrolling to it. It turns out that the latter is the easier part. At least it's the easier part if you know JavaScript, which I do not. One of the bots (I forget which) provided me with the following code to embed in an R function. The arguments to the R function are outputID (the name of the table in the UI) and row (the row number to which to scroll).

js_code <- sprintf(
  "setTimeout(function() {
     var table = $('#%s table').DataTable();
     // Scroll to the row.
     setTimeout(function() {
       var rowNode = table.row(%d).node();
       if (rowNode) {
         $(rowNode).addClass('highlight-row');
         rowNode.scrollIntoView({behavior: 'smooth', block: 'center'});
       }
     }, 300);
   }, 200);",
  outputId,
  row - 1
)
runjs(js_code)

I'm not positive, but I believe you need to load the shinyjs R library for this to work. (My app was already using it.) The values 200 and 300 in the code are apparently delays (in milliseconds) intended to allow time for DT to finish rendering the table before scrolling occurs.

On to what I originally thought would be the easier part: finding the target record. What the JS code is looking for in the second argument (row number) is actually what I'm calling the display index. My code knows the row index of the target record. Assume the output ID of the table is "A". After some digging, I discovered that input$A_rows_all contains the vector of row indices in display order (meaning the first entry is the row index of the record with display index 1 etc.). So if variable r contains the row index of the record we want, which(input$A_rows_all == r) will give us the display index of that row. Simple enough ... except that it did not work.

Poking around my code, I discovered that after the user committed a change the table display would update "immediately" but that input$A_rows_all retained its pre-change value. This turns out to be a synchronization issue -- DT updates the table and then updates the rows_all vector at it's own pace, while my function attempts to move immediately from the line committing the change to the line looking for the new display index.

The answer was to wrap the code doing the scrolling in an event observer: observeEvent(input$A_rows_all, { ... }) where "..." is the code that finds the target row and scrolls to it. This effectively pauses my code until DT has gotten around to updating the rows_all vector.