DEV Community

Cover image for Let's create Data Table. Part 7: Dark theme and refactoring
Dima Vyshniakov
Dima Vyshniakov

Posted on

Let's create Data Table. Part 7: Dark theme and refactoring

This is a single article from a series about creating of an advanced Data table component using React, TanStack Table 8, Tailwind CSS and Headless UI.

In the previous episode, we implemented a column filtering feature for the table columns.

This article will focus on a collection of smaller, yet impactful enhancements to our project. Think of it as an Interdimensional Cable episode in Rick and Morty, addressing a few important features and refactorings that, while not the main event, are crucial for improving the overall code quality, maintainability, and user experience.

Rick and Morty

Tokenization

Tokenization refers to the practice of representing design elements and their properties as reusable variables (tokens).

Since we are using sizes and paddings from Tailwind, we need to tokenize only color values. Instead of hard-coding specific values (e.g., teal-600), we will assign meaningful names to these elements (primary, secondary, etc.).

This way, changes to a token affect all elements that use it, ensuring consistency across the entire application. This will be particularly useful for the dark theme.

We put our tokenized colors into ./tailwind.config.js file as a theme.

const colors = require('tailwindcss/colors');

export default {
  //...
  theme: {
    extend: {
      colors: {
        primary: colors.stone['600'],
        secondary: colors.cyan['800'],
        backgroundLight: colors.white,
        textDark: colors.stone['100'],
        textLight: colors.stone['950'],
        hoverColor: colors.cyan['100'],
        borderColor: colors.stone['300'],
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Then we apply these theme variables instead of directly using Tailwind colors. So bg-stone-600 CSS class becomes bg-primary and so on.

Dark theme

In the past, Windows 95 graced our screens with its predominantly white-blueish backgrounds, a hallmark of its time. As an advanced operating system, it came equipped with state-of-the-art hardware and software, which garnered much appreciation from my generation. WYSYWIG interface paradigm was a refreshing departure from the older, terminal-based environments dominated by black and green hues. Interestingly, today's trend has shifted in the opposite direction.

Now when we have our colors organized we can start making dark theme to respect modern generation's aesthetic preferences.

Dark table demo

Modern browsers provide built-in support for dark mode preferences through the prefers-color-scheme media query. This media query acts as a bridge between the user's system-wide dark mode setting and your application's styles.

Tailwind CSS offers a convenient way to implement dark mode with its dark: helper class. This class allows you to easily define styles specific to the dark mode presentation.

Here are our new dark color tokens. We have background token assigned #000 (pure black) value to make the table dark. For the rest of the tokens, we adjusted saturation to achieve the same contrast as we had with a white background.

To achieve a seamless dark mode experience, we'll begin by extending your existing theme to include dark variants of your color tokens. These dark color tokens will be used to style the Data table in dark mode. For instance, we can set the background token to #000 (pure black) to create a dark background for the table.

For the remaining color tokens, it's essential to adjust their saturation levels. This ensures that the text and other elements maintain sufficient contrast against the dark background, preserving readability.

primaryDark: colors.stone['700'],
backgroundDark: colors.black,
textDark: colors.stone['100'],
hoverColorDark: colors.cyan['600'],
borderColorDark: colors.stone['600'],
Enter fullscreen mode Exit fullscreen mode

Here is how we apply the dark theme to the component.

<Input
  //...
  className={classNames(
    //..
    // focus styles light
    'focus:bg-backgroundLight/85 focus:text-textLight focus:placeholder-transparent',
    // focus styles dark
    'dark:focus:bg-backgroundDark/85 dark:focus:text-textDark',
    'transition-all duration-200',
  )}
/>
Enter fullscreen mode Exit fullscreen mode

Columns permissions

To improve the User Experience of the table, we are going to fine tune column capabilities. The goal is to disable some features for some columns.

Disabled actions demo

We will extend the existing hook in src/DataTable/features/useColumnActions.tsx to include a new property: disabled.
This property will be a boolean flag indicating whether a specific feature is disabled for the current column. The necessary information about the column's configuration will be retrieved from the TanStack Table Context API.

type ColumnAction = {
  //...
  /** True when table capability is disabled by config */
  disabled?: boolean;
};

/**
 * React hook which returns an array of table column actions config objects
 */
export const useColumnActions = (
  context: HeaderContext<Row, unknown>,
): ColumnAction[] => {
  //...
  return useMemo<ColumnAction[]>(
    () => [
      // Pin left button
      {
        // ...
        disabled: !context.column.getCanPin(),
      },
      // Pin right button
      {
        //...
        disabled: !context.column.getCanPin(),
      },
      // Sort ASC button
      {
        //...
        disabled: !context.column.getCanSort(),
      },
      // Sort DESC button
      {
        //...
        disabled: !context.column.getCanSort(),
      },
      // Filter button
      {
        //...
        disabled: !context.column.getCanFilter(),
      },
    ],
    [/*...*/],
  );
};
Enter fullscreen mode Exit fullscreen mode

Now, we will use this logic inside src/DataTable/cells/HeaderCell.tsx. We connect the disabled property from the hook to the corresponding Button component prop. Also, we disable pointer events and set 30% opacity for the disabled state using the corresponding Tailwind CSS helper.

In src/DataTable/cells/HeaderCell.tsx, we will integrate the disabled property obtained from the useColumnActions hook into the corresponding Button component.

If the disabled flag is set to true:

  • Button component will be rendered in a disabled state.

  • Pointer events will be disabled for the button using Tailwind CSS pointer-events-none class.

  • Text and icon opacity will be reduced to 30% to visually indicate button state.

{items.map(({ label, icon: Icon, onClick, disabled }) => (
  <MenuItem key={label} as={Fragment}>
    {() => (
      <Button
        disabled={disabled}
        onClick={onClick}
        className={classNames(
          // ...
          // disabled styles
          'disabled:text-textDark/30 disabled:pointer-events-none'
        )}
      >
        {Icon}
        <div>{label}</div>
      </Button>
    )}
  </MenuItem>
))}
Enter fullscreen mode Exit fullscreen mode

Finally, we have to set enableColumnFilter, enablePinning and enableSorting properties for table columns inside src/DataTable/columnsConfig.tsx.

const columnHelper = createColumnHelper<Row>();

export const columns = [
  //...
  columnHelper.accessor('randomDecimal', {
    enableColumnFilter: false,
    enablePinning: false,
    enableSorting: false,
    //...
  }
  //...
]
Enter fullscreen mode Exit fullscreen mode

Debug mode

Debug mode

The last change is a quality of life improvement. Tanstack has a convenient debugging tools available. So we are going to implement debugAll mode.

Inside src/DataTable/DataTable.tsx we will extend component props with debug property. And set this property at useReactTable hook parameters.

type Props = {
  //...
  /**
   * Enable TanStack table debug mode
   * @see https://tanstack.com/table/latest/docs/api/core/table#debugall
   */
  debug?: boolean;
};

export const DataTable: FC<Props> = ({ tableData, locale = 'en-US', debug }) => {
  //...

  const table = useReactTable({
    //...
    debugAll: debug
  });
 //...
};
Enter fullscreen mode Exit fullscreen mode

Chapter summary

In these 7 articles we created the Data table component from scratch, using TanStack table, Tailwind CSS and Headless UI. We implemented the following features:

  • Multidimensional Scroll: Enables smooth and synchronized scrolling in both horizontal and vertical directions, with sticky header maintaining alignment across the viewport

  • Responsive Design: Adapts seamlessly to various screen sizes and supports a wide range of data types, including text, numbers, dates, and limited enumerable lists.

  • Customizable Subcomponents: Dialogs, Dropdown menus, and custom input fields for enhanced user interaction.

  • High-Performance Virtualization: Enables virtualization techniques to efficiently render around 50,000 rows without significant performance degradation, even on less powerful machines.

  • Column Pinning: Allows users to pin columns to the left or right of the viewport, improving data focus and readability.

  • Advanced Sorting and Filtering: Implements intelligent sorting and filtering mechanisms that account for the specific data formats of each column.

  • Dark Mode Support: Provides a user-friendly dark mode option.

Here is the complete demo of the exercise.

To be continued.

Top comments (0)