Skip to main content

Personalization

Enhancing Fluent UI DetailsList with Custom Sorting, Filtering, Lazy Loading and Filter Chips

Istock 917895628

Fluent UI DetailsList custom sorting and filtering can transform how structured data is displayed. While the default DetailsList component is powerful, it doesn’t include built‑in features like advanced sorting, flexible filtering, lazy loading, or selection‑driven filter chips. In this blog, we’ll show you how to extend Fluent UI DetailsList with these enhancements, making it more dynamic, scalable, and user‑friendly.

We’ll also introduce simple, reusable hooks that allow you to implement your own filtering and sorting logic, which will be perfect for scenarios where the default behavior doesn’t quite fit your needs. By the end, you’ll have a flexible, feature-rich Fluent UI DetailsList setup with sorting and filtering that can handle complex data interactions with ease.

Here’s what our wrapper brings to the table:

  • Context‑aware column menus that enable sorting beyond simple A↔Z ordering
  • Filter interfaces designed for each data type (.i.e. freeform text, choice lists, numeric ranges, or time values)
  • Selection chips that display active filters and allow quick deselection with a single click
  • Lazy loading with infinite scroll, seamlessly integrated with your API or pagination pipeline
  • One orchestrating component that ties all these features together, eliminating repetitive boilerplate

Core Architecture

The wrapper includes:

  • Column Definitions: To control how each column sorts/filters
  • State & Refs: To manage final items, full dataset, and UI flags
  • Default Logic By overriding hooks – onSort, onFilter
  • Selection: Powered by Fluent UI Selection API
  • Lazy Loading: Using IntersectionObserver
  • Filter Chips: Reflect selected rows

Following are the steps to achieve these features:

Step 1: Define Column Metadata

Each column in the DetailsList must explicitly describe its data type, sort behavior, and filtering behavior. This metadata helps the wrapper render the correct UI elements such as combo boxes, number inputs, or time pickers.

Each column needs metadata describing:

  • Field type
  • Sort behavior
  • Filter behavior
  • UI options (choice lists, icons, etc.)
export interface IDetailsListColumnDefinition {
  fieldName: string;
  displayName: string;
  columnType?: DetailsListColumnType; // Text, Date, Time, etc.
  sortDetails?: { fieldType: SortFilterType };
  filterDetails?: {
    fieldType: SortFilterType;
    filterOptions?: IComboBoxOption[];
    appliedFilters?: any[];
  };
}

Following is the example:

const columns = [{
  fieldName: 'status',
  displayName: 'Status',
  columnType: DetailsListColumnType.Text,
  sortDetails: {
    fieldType: SortFilterType.Choice
  },
  filterDetails: {
    fieldType: SortFilterType.Choice,
    filterOptions: [{
      key: 'Active',
      text: 'Active'
    },
    {
      key: 'Inactive',
      text: 'Inactive'
    }]
  }
}];

Step 2: Implement Type-Aware Fluent UI DetailsList Custom Sorting

The sorting mechanism dynamically switches based on the column’s data type. Time fields are converted to minutes to ensure consistent sorting, while text and number fields use their native values. It supports following:

  • Supports Text, Number, NumberRange, Date, and Time (custom handling for time via minute conversion).
  • Sort direction is controlled from the column’s context menu.
  • Works with default sorting or lets you inject custom sorting via onSort.
  • Default sorting uses lodash orderBy unless onSort is provided

Sample code for its implementation can be written as follows:

switch (sortColumnType) {
case SortFilterType.Time:
  sortedItems = orderBy(sortedItems, [item = >getTimeForField(item, column.key)], column.isSortedDescending ? ['desc'] : ['asc']);
  break;
default:
  sortedItems = orderBy(sortedItems, column.fieldName, column.isSortedDescending ? 'desc': 'asc');
}

Step 3: Implement Fluent UI DetailsList Custom Filtering (Text/Choice/Range/Time)

Filtering inputs change automatically based on column type. Text and choice filters use combo boxes, while numeric fields use range inputs. Time filters extract and compare HH:mm formatted values.

Text & Choice Filters

Implemented using Fluent UI ComboBox as follows:

<ComboBox

    allowFreeform={!isChoiceField}
    
    multiSelect={true}
    
    options={comboboxOptions}
    
    onChange={(e, option, index, value) =>
    
    _handleFilterDropdownChange(e, column, option, index, value)
    
    }
/>

Number Range Filter

Implemented as two input boxes, min & max for defining number range.

  • Min/Max chips are normalized in order [min, max].
  • Only applied if present; absence of either acts as open‑ended range.

Time Filter

For filtering time, we are ignoring date part and just considering time part.

  • Times are converted to minutes since midnight(HH:mm) to sort reliably regardless of display format.
  • Filtering uses date-fns format() for display and matching.

Step 4: Build the Filtering Pipeline

This step handles the filtering logic as capturing user-selected values, updating filter states, re-filtering all items, and finally applying the active sorting order. If custom filter logic is provided, it overrides the defaults. It will work as follows:

  1. User changes filter
  2. Update column.filterDetails.appliedFilters
  3. Call onFilter (if provided)
  4. Otherwise run default filter pipeline as follows:

allItems → apply filter(s) → apply current sort → update UI

Following are some helper functions that can be created for handing filter/sort logic:

  • _filterItems
  • _applyDefaultFilter
  • _applyDefaultSort

Step 5: Display Filter Chips

When selection is enabled, each selected row appears as a dismissible chip above the grid. Removing the chip automatically deselects the row, ensuring tight synchronization between UI and data.

<FilterChip key={filterValue.key} filterValue={filterValue} onRemove={_handleChipRemove} />

Note: This is a custom subcomponent used to handle filter chips. Internally it display selected values in chip form and we can control its values and functioning using onRemove and filterValue props.

Chip removal:

  • Unselects row programmatically
  • Updates the selection object

Step 6: Implementing Lazy Loading (IntersectionObserver)

The component makes use of IntersectionObserver, to detect if the user reaches the end of the list. Once triggered, it calls the lazy loading callback to fetch the next batch of items from the server or state.

  • An additional row at the bottom triggers onLazyLoadTriggered() as it enters the viewport.
  • Displays a spinner while loading; attaches the observer when more data is available.

A sentinel div at the bottom triggers loading:

observer.current = new IntersectionObserver(async entries => {
  const entry = entries[0];
  if (entry.isIntersecting) {
    observer.current ? .unobserve(lazyLoadRef.current!);
    await lazyLoadDetails.onLazyLoadTriggered();
  }
});

Props controlling behavior:

lazyLoadDetails ? :{
  enableLazyLoad: boolean;
  onLazyLoadTriggered: () => void;
  isLoading: boolean;
  moreItems: boolean;
};

Step 7: Sticky Headers

Sticky headers keep the column titles visible as the user scrolls through large datasets, improving readability and usability. Following is the code where, maxHeight property determines the scrollable container height:

const stickyHeaderStyle = {
  root: {
    maxHeight: stickyHeaderDetails ? .maxHeight ? ?450
  },
  headerWrapper: {
    position: 'sticky',
    top: 0,
    zIndex: 1
  }
};

Step 8: Putting It All Together — Minimal Example for Fluent UI DetailsList custom filtering and sorting

Following is an example where we are calling our customizes details list component:

<CustomDetailsList
  columnDefinitions={columns}
  items={data}
  allItems={data}
  checkboxVisible={CheckboxVisibility.always}
  initialSort={{ fieldName: "name", direction: SortDirection.Asc }}
  filterChipDetails={{
    filterChipKeyColumnName: "key",

    filterChipColumnName: "name",
  }}
  stickyHeaderDetails={{ enableStickyHeader: true, maxHeight: 520 }}
  lazyLoadDetails={{
    enableLazyLoad: true,

    isLoading: false,

    moreItems: true,

    onLazyLoadTriggered: async () => {
      // load more
    },
  }}
/>;

Accessibility & UX Notes

  • Keyboard: Enter key applies text/number inputs instantly; menu remains open so users can stack filters.
  • Clear filter: Context menu shows “Clear filter” action only when a filter exists; there’s also a “Clear Filters (n)” button above the grid that resets all columns at once.
  • Selection cap: To begin, maxSelectionCount helps prevent accidental bulk selections; next, it provides immediate visual feedback so users can clearly see their limits in action.

Performance Guidelines

  • Virtualization: For very large datasets, you can enable virtualization and validate both menu positioning and performance. For current example, onShouldVirtualize={() => false} is used to maintain a predictable menu experience.
  • Server‑side filtering/sorting: If your dataset is huge, pass onSort/onFilter and do the heavy lifting server‑side, then feed the component the updated page through items.
  • Lazy loading: Use moreItems to hide the sentinel when the server reports the last page; set isLoading to true to show the spinner row.

Conclusion

Finally, we have created a fully customized Fluent UI DetailsList with custom filtering and sorting which condenses real‑world list interactions into one drop‑in component. CustomDetailsList provides a production-ready, extensible, developer-friendly data grid wrapper with following enhanced features:

  • Clean context menus for type‑aware sort & filter
  • Offers selection chips for quick, visual interaction and control
  • Supports lazy loading that integrates seamlessly with your API
  • Allows you to keep headers sticky to maintain clarity in long lists
  • Delivers a ready‑to‑use design while allowing full customization when needed

GitHub repository

Please refer to the GitHub repository below for the full code. A sample has been provided within to illustrate its usage:

https://github.com/pk-tech-dev/customdetailslist

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Pallavi Koramkar

I am a Technical Architect at Perficient, Nagpur, specializing in Microsoft web-based collaborative platforms. I design and deliver innovative solutions that address complex business challenges. My expertise spans a wide range of Microsoft technologies, including SharePoint Online, SPFx, Power Platform, Azure, .NET Framework, MS SQL, ASP.NET, and Web Forms. Over the course of my career, I have successfully provided tailored technical solutions to diverse clients, helping them optimize processes and achieve their goals. I thrive on tackling new challenges and take pride in completing projects with efficiency and precision.

More from this Author

Follow Us