Repository: mwouts/itables (language: python)

Issue:
Conditional Filtering
As far as I see, it is only possible to search a specific value to filter the table, right? 

Or can I somehow filter a column such as > 1 ?


Discussion / hints:
Hi @on46zohu , right now the search options only search through text. Still, you can have search fields per columns like [in this example](https://mwouts.github.io/itables/advanced_parameters.html#column-filters).

Outside of `itables` (e.g. not working today but if you know enough Javascript there might be something to do), I see this [example](https://datatables.net/examples/plug-ins/range_filtering.html) in the datatables.net documentation:
![image](https://github.com/mwouts/itables/assets/29915202/9e7e3bd8-7603-44a9-812c-45700c5df70a)

I am also aware of the [Column Filters in DT](https://rstudio.github.io/DT/) that are very close to what I imagine you're looking for (but I have no idea how they do that!):
![image](https://github.com/mwouts/itables/assets/29915202/82208f80-fee7-42bb-9bb9-76ee827561c9)


Fix patch:
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 3d7ae113..ed90a54e 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -17,10 +17,10 @@ jobs:
   pre-commit:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Set up Python
-        uses: actions/setup-python@v4
-      - uses: pre-commit/action@v3.0.0
+        uses: actions/setup-python@v5
+      - uses: pre-commit/action@v3.0.1
 
   codeql:
     runs-on: ubuntu-latest
@@ -28,48 +28,45 @@ jobs:
       security-events: write
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v2
+        uses: github/codeql-action/init@v3
         with:
           languages: python, javascript
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v2
+        uses: github/codeql-action/analyze@v3
 
   pytest:
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]
+        python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
         pandas-version: [latest]
         include:
-          - python-version: 3.6
-            pandas-version: 0.22
+          - python-version: 3.7
+            pandas-version: '<1.0'
           - python-version: 3.9
-            pandas-version: 1.5
-          - python-version: "3.11"
+            pandas-version: '<2.0'
+          - python-version: "3.12"
             pandas-version: pre
             polars: true
-          - python-version: "3.11"
+          - python-version: "3.12"
             uninstall_jinja2: true
     runs-on: ubuntu-20.04
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
+
+      - name: Set up Node
+        uses: actions/setup-node@v4
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install -r requirements.txt
-          pip install -r requirements-dev.txt
-
       - name: Install a development version of 'itables'
-        run: pip install -e .
+        run: pip install -e .[test]
 
       - name: Install pandas latest
         if: matrix.pandas-version == 'latest'
@@ -79,7 +76,7 @@ jobs:
         run: pip install pandas --pre
       - name: Install pandas ${{ matrix.pandas-version }}
         if: matrix.pandas-version != 'pre' && matrix.pandas-version != 'latest'
-        run: pip install pandas==${{ matrix.pandas-version }}
+        run: pip install 'pandas${{ matrix.pandas-version }}'
 
       - name: Install polars
         if: matrix.polars
@@ -97,3 +94,19 @@ jobs:
 
       - name: Upload coverage
         uses: codecov/codecov-action@v3
+
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout source
+        uses: actions/checkout@v4
+      - name: Set up Node
+        uses: actions/setup-node@v4
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: 3.11
+      - name: Install hatch
+        run : pip install hatch
+      - name: Build package
+        run: hatch build
diff --git a/.github/workflows/publish-book.yml b/.github/workflows/publish-book.yml
index fba9fb03..5cfbc0f0 100644
--- a/.github/workflows/publish-book.yml
+++ b/.github/workflows/publish-book.yml
@@ -15,11 +15,13 @@ jobs:
   deploy-book:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
+
+    - name: Set up Node
+      uses: actions/setup-node@v4
 
-    # Install dependencies
     - name: Set up Python 3.11
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v5
       with:
         python-version: 3.11
 
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d1576ca1..58a83441 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -3,26 +3,29 @@ on:
   push:
     tags:
       - "v[0-9]+.[0-9]+.[0-9]+"
-      - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"
+      - "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
+      - "v[0-9]+.[0-9]+.[0-9]+dev[0-9]+"
 
 jobs:
   publish:
     runs-on: ubuntu-latest
     environment:
       name: pypi
-      url: https://pypi.org/p/jupytext
+      url: https://pypi.org/p/itables
     permissions:
       id-token: write
     steps:
       - name: Checkout source
-        uses: actions/checkout@v3
-      - name: Set up Python 3.9
-        uses: actions/setup-python@v4
+        uses: actions/checkout@v4
+      - name: Set up Node
+        uses: actions/setup-node@v4
+      - name: Set up Python
+        uses: actions/setup-python@v5
         with:
-          python-version: 3.9
+          python-version: 3.11
       - name: Build package
         run: |
-          pip install wheel requests
-          python setup.py sdist bdist_wheel
+          python -m pip install wheel build
+          python -m build
       - name: Publish
         uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.gitignore b/.gitignore
index 422e63d5..f63987e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,10 +16,12 @@ dist
 # Jupyter Book
 _build
 
-# External dependencies
-itables/external
-
 # Quarto
 .jupyter_cache
 docs/quarto/*.html
 docs/quarto/*_files/
+
+# DataTables bundle
+node_modules
+dt_bundle.js
+dt_bundle.css
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 501a5ba3..2490c2e0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,27 +11,31 @@ repos:
     - id: end-of-file-fixer
     - id: trailing-whitespace
 
-  - repo: https://github.com/pycqa/flake8
-    rev: 6.0.0
-    hooks:
-    - id: flake8
-
   - repo: https://github.com/timothycrosley/isort
-    rev: 5.12.0
+    rev: 5.13.2
     hooks:
     - id: isort
       args: ["--profile", "black", "--filter-files"]
 
   - repo: https://github.com/psf/black
-    rev: 23.1.0
+    rev: 22.3.0
     hooks:
     - id: black
 
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.3.1
+    hooks:
+      - id: ruff
+        args: ["--fix", "--show-fixes"]
+
   - repo: https://github.com/mwouts/jupytext
     rev: v1.14.5
     hooks:
     - id: jupytext
+      exclude: dt_for_itables/
       types: ["markdown"]
-      args: ["--pipe", "black"]
+      args: ["--pipe", "isort {} --treat-comment-as-code '# %%' --profile black", "--pipe", "black", "--check", "ruff check {} --ignore E402"]
       additional_dependencies:
         - black==22.3.0 # Matches hook
+        - ruff==0.3.1
+        - isort==5.13.2
diff --git a/README.md b/README.md
index 78c4634e..006964c5 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,15 @@
 
 ![CI](https://github.com/mwouts/itables/actions/workflows/continuous-integration.yml/badge.svg?branch=main)
 [![codecov.io](https://codecov.io/github/mwouts/itables/coverage.svg?branch=main)](https://codecov.io/github/mwouts/itables?branch=main)
-[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/mwouts/itables.svg)](https://lgtm.com/projects/g/mwouts/itables/context:python)
 [![Pypi](https://img.shields.io/pypi/v/itables.svg)](https://pypi.python.org/pypi/itables)
 [![Conda Version](https://img.shields.io/conda/vn/conda-forge/itables.svg)](https://anaconda.org/conda-forge/itables)
 [![pyversions](https://img.shields.io/pypi/pyversions/itables.svg)](https://pypi.python.org/pypi/itables)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
 
-## Turn your Python DataFrames into Interactive Tables
+## Turn your Python DataFrames into Interactive DataTables
 
 This packages changes how Pandas and Polars DataFrames are rendered in Jupyter Notebooks.
-With `itables` you can display your tables as interactive [datatables](https://datatables.net/)
+With `itables` you can display your tables as interactive [DataTables](https://datatables.net/)
 that you can sort, paginate, scroll or filter.
 
 ITables is just about how tables are displayed. You can turn it on and off in just two lines,
@@ -24,7 +23,7 @@ work with Polars DataFrames).
 ## Documentation
 
 Browse the [documentation](https://mwouts.github.io/itables/) to see
-examples of Pandas or Polars DataFrames rendered as interactive datatables.
+examples of Pandas or Polars DataFrames rendered as interactive DataTables.
 
 ## Quick start
 
@@ -50,7 +49,7 @@ and then render any DataFrame as an interactive table that you can sort, search
 If you prefer to render only selected DataFrames as interactive tables, use `itables.show` to show just one Series or DataFrame as an interactive table:
 ![show](docs/show_df.png)
 
-Since `itables==1.0.0`, the [jquery](https://jquery.com/) and [datatables.net](https://datatables.net/) libraries and CSS
+Since `itables==1.0.0`, the [jQuery](https://jquery.com/) and [DataTables](https://datatables.net/) libraries and CSS
 are injected in the notebook when you execute `init_notebook_mode` with its default argument `connected=False`.
 Thanks to this the interactive tables will work even without a connection to the internet.
 
@@ -67,6 +66,8 @@ execute `init_notebook_mode`.
 - Google Colab
 - VS Code (for both Jupyter Notebooks and Python scripts)
 - PyCharm (for Jupyter Notebooks)
+- Quarto
+- Shiny for Python
 
 ## Try ITables on Binder
 
@@ -90,7 +91,7 @@ and decide whether you should upgrade `itables`.
 
 When the data in a table is larger than `maxBytes`, which is equal to 64KB by default, `itables` will display only a subset of the table - one that fits into `maxBytes`. If you wish, you can deactivate the limit with `maxBytes=0`, change the value of `maxBytes`, or similarly set a limit on the number of rows (`maxRows`, defaults to 0) or columns (`maxColumns`, defaults to `pd.get_option('display.max_columns')`).
 
-Note that datatables support [server-side processing](https://datatables.net/examples/data_sources/server_side). At a later stage we may implement support for larger tables using this feature.
+Note that DataTables support [server-side processing](https://datatables.net/examples/data_sources/server_side). At a later stage we may implement support for larger tables using this feature.
 
 ```{code-cell}
 from itables.sample_dfs import get_indicators
diff --git a/docs/_config.yml b/docs/_config.yml
index f8f02972..26d5c62d 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -1,5 +1,6 @@
 title: Interactive Tables
 author: Marc Wouts
+copyright: "2019-2024"
 execute:
   execute_notebooks: force
 sphinx:
diff --git a/docs/_toc.yml b/docs/_toc.yml
index b9355d41..1df44b5b 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -1,16 +1,25 @@
 format: jb-book
 root: quick_start
-chapters:
-- file: supported_editors
-- file: advanced_parameters
-- file: pandas_style
-- file: custom_css
-- file: downsampling
-- file: sample_dataframes
-- file: polars_dataframes
-- file: quarto
-- file: references
-- file: contributing
-- file: developing
-- file: troubleshooting
-- file: changelog
+parts:
+  - caption: How to use DataTables
+    chapters:
+    - file: advanced_parameters
+    - file: formatting
+    - file: custom_css
+    - file: extensions
+    - file: custom_extensions
+  - caption: ITables
+    chapters:
+      - file: supported_editors
+      - file: quarto
+      - file: downsampling
+      - file: references
+      - file: contributing
+      - file: developing
+      - file: troubleshooting
+      - file: changelog
+  - caption: Example DataFrames
+    chapters:
+    - file: sample_dataframes
+    - file: polars_dataframes
+    - file: pandas_style
diff --git a/docs/advanced_parameters.md b/docs/advanced_parameters.md
index bb773c4b..1c26e512 100644
--- a/docs/advanced_parameters.md
+++ b/docs/advanced_parameters.md
@@ -12,11 +12,11 @@ kernelspec:
   name: itables
 ---
 
-# Advanced parameters
+# The DataTable Arguments
 
-The `itables` package is a wrapper for the Javascript [datatables.net](https://datatables.net/) library, which has a great [documentation](https://datatables.net/), a huge collection of [examples](https://datatables.net/examples/index), and a useful [forum](https://datatables.net/forums/).
+ITables is a wrapper for the Javascript [DataTables](https://datatables.net/) library, which has a great [documentation](https://datatables.net/), a huge collection of [examples](https://datatables.net/examples/index), and a useful [forum](https://datatables.net/forums/).
 
-Below we give a few examples of how the datatables.net examples can be ported to Python with `itables`.
+Below we give a series of examples of how the DataTables examples can be ported to Python with `itables`.
 
 As always, we initialize the `itables` library with
 
@@ -30,32 +30,13 @@ Then we create two sample dataframes:
 
 ```{code-cell}
 import pandas as pd
+
 from itables.sample_dfs import get_countries
 
 df_small = pd.DataFrame({"a": [2, 1]})
 df = get_countries(html=False)
 ```
 
-## Position and width
-
-The default value for the table CSS is `table-layout:auto;width:auto;margin:auto;caption-side:bottom`.
-Without `width:auto`, tables with few columns still take the full notebook width in Jupyter.
-Using `margin:auto` makes non-wide tables centered in Jupyter.
-
-You can change the CSS used for a single table with e.g.
-
-```{code-cell}
-show(df_small, style="table-layout:auto;width:50%;float:right")
-```
-
-or you can also change it for all tables by changing `itables.options.style`:
-
-```python
-import itables.options as opt
-
-opt.style = "table-layout:auto;width:auto"
-```
-
 ```{code-cell}
 :tags: [remove-cell]
 
@@ -64,34 +45,6 @@ import itables.options as opt
 opt.lengthMenu = [5, 10, 20, 50, 100, 200, 500]
 ```
 
-## Theme
-
-Select how your table looks like with the `classes` argument (defaults to `"display nowrap"`) of the `show` function, or by changing `itables.options.classes`.
-
-Add `"compact"` if you want a denser table:
-
-```{code-cell}
-:tags: [full-width]
-
-show(df, classes="display nowrap compact")
-```
-
-Remove `"nowrap"` if you want the cell content to be wrapped:
-
-```{code-cell}
-:tags: [full-width]
-
-show(df, classes="display")
-```
-
-[More options](https://datatables.net/manual/styling/classes#Table-classes) like `"cell-border"` are available:
-
-```{code-cell}
-:tags: [full-width]
-
-show(df, classes="display nowrap cell-border")
-```
-
 ## Caption
 
 You can set additional `tags` on the table like e.g. a [caption](https://datatables.net/blog/2014-11-07):
@@ -102,48 +55,60 @@ You can set additional `tags` on the table like e.g. a [caption](https://datatab
 show(df, "Countries from the World Bank Database")
 ```
 
-The caption appears at the bottom of the table by default. This is governed by `caption-side:bottom`
-in the `style` option which you can change. You can also override the location of the caption in the caption tag itself:
+The caption appears at the bottom of the table by default (except
+in Jupyter Book). This is governed by `caption-side:bottom`
+in the [`style` option](style).
+
+You can also override the location of the caption in the caption tag itself:
 
 ```{code-cell}
 :tags: [full-width]
 
 show(
     df,
-    tags='<caption style="caption-side: top">Countries from the World Bank Database</caption>',
+    tags='<caption style="caption-side: bottom">Countries from the World Bank Database</caption>',
 )
 ```
 
-```{code-cell}
-:tags: [remove-input]
-
-opt.lengthMenu = [5, 10, 20, 50, 100, 200, 500]
-```
-
-## Removing the search box
+(layout)=
+## Table layout
 
 By default, datatables that don't fit in one page come with a search box, a pagination control, a table summary, etc.
 You can select which elements are actually displayed using
-DataTables' [`dom` option](https://datatables.net/reference/option/dom) with e.g.:
+DataTables' [`layout` option](https://datatables.net/reference/option/layout) with e.g.:
 
 ```{code-cell}
-show(df_small, dom="ti")
+show(df_small, layout={"topStart": "search", "topEnd": None})
 ```
 
-The available elements are:
-- `l`: length changing input control
-- `f`: filtering input
-- `t`: the table itself
-- `i`: table information summary
-- `p`: pagination control
-- `r`: processing display element
+The available positions are `topStart, topEnd, bottomStart, bottomEnd`. You can also use `top2Start`, etc... (see more
+in the [DataTables documentation](https://datatables.net/reference/option/layout)).
 
 Like for the other arguments of `show`, you can change the default value of the dom option with e.g.:
 
 ```
 import itables.options as opt
 
-opt.dom = "lfrtip"  # (default value)
+opt.layout =  {
+    "topStart": "pageLength",
+    "topEnd": "search",
+    "bottomStart": "info",
+    "bottomEnd": "paging"
+}  # (default value)
+```
+
+```{tip}
+The `layout` option was introduced with `itables==2.0` and `DataTables==2.0`
+and deprecates the former [`dom` option](https://datatables.net/reference/option/dom).
+If you wish to continue using the `dom` option, set `opt.warn_on_dom = False`.
+```
+
+## Search
+
+The [search option](https://datatables.net/reference/option/search) let you control the initial value for the search field, and whether the query should be treated as a regular expression or not:
+
+```{code-cell}
+show(df, search={"regex": True, "caseInsensitive": True, "search": "s.ain"})
 ```
 
 ## Pagination
@@ -180,7 +145,7 @@ show(df, scrollY="200px", scrollCollapse=True, paging=False)
 
 In the context of the notebook, a horizontal scroll bar should appear when the table is too wide. In other contexts like here in Jupyter Book, you might want to use `scrollX = True`.
 
-## Table footer
+## Footer
 
 Use `footer = True` if you wish to display a table footer.
 
@@ -193,14 +158,14 @@ show(df, footer=True)
 ## Column filters
 
 Use `column_filters = "header"` or `"footer"` if you wish to display individual column filters
-(remove the global search box with [`dom='lrtip'`](https://datatables.net/reference/option/dom) if desired).
+(remove the global search box with a [`layout`](layout) modifier if desired).
 
 ```{code-cell}
 alpha_numeric_df = pd.DataFrame(
     [["one", 1.5], ["two", 2.3]], columns=["string", "numeric"]
 )
 
-show(alpha_numeric_df, column_filters="footer", dom="lrtip")
+show(alpha_numeric_df, column_filters="footer", layout={"topEnd": None})
 ```
 
 As always you can set activate column filters by default with e.g.
@@ -223,62 +188,6 @@ get_dict_of_test_dfs()["multiindex"]
 opt.column_filters = False
 ```
 
-## Pandas formatting
-
-`itables` builds the HTML representation of your Pandas dataframes using Pandas itself, so
-you can use [Pandas' formatting options](https://pandas.pydata.org/pandas-docs/stable/user_guide/options.html).
-For instance, you can change the precision used to display floating numbers:
-
-```{code-cell}
-import math
-import pandas as pd
-
-with pd.option_context("display.float_format", "{:,.2f}".format):
-    show(pd.Series([i * math.pi for i in range(1, 6)]))
-```
-
-Or you can use a custom formatter:
-
-```{code-cell}
-with pd.option_context("display.float_format", "${:,.2f}".format):
-    show(pd.Series([i * math.pi for i in range(1, 6)]))
-```
-
-```{tip}
-ITables in version 1.6.0+ can also render
-[Pandas Style](https://pandas.pydata.org/docs/user_guide/style.html)
-objects as interactive datatables.
-
-This way, you can easily add background color, and even
-tooltips to your dataframes, and still get them
-displayed using datatables.net - see our [example](pandas_style.md).
-```
-
-## Javascript formatting
-
-Numbers are formatted using Pandas, then are converted back to float to ensure they come in the right order when sorted.
-Therefore, to achieve a particular formatting you might have to resort to the
-[`columns.render` option](https://datatables.net/examples/advanced_init/column_render.html)
-of datatables.
-
-For instance, this [example](https://datatables.net/forums/discussion/61407/how-to-apply-a-numeric-format-to-a-column)
-can be ported like this:
-
-```{code-cell}
-from itables import JavascriptCode
-
-
-show(
-    pd.Series([i * math.pi * 1e4 for i in range(1, 6)]),
-    columnDefs=[
-        {
-            "targets": "_all",
-            "render": JavascriptCode("$.fn.dataTable.render.number(',', '.', 3, '$')"),
-        }
-    ],
-)
-```
-
 ## Row order
 
 Since `itables>=1.3.0`, the interactive datatable shows the rows in the same order as the original dataframe:
@@ -317,144 +226,3 @@ or locally by passing an argument `showIndex` to the `show` function:
 df_with_range_index = pd.DataFrame({"letter": list("abcd")})
 show(df_with_range_index, showIndex=True)
 ```
-
-## Advanced cell formatting with JS callbacks
-
-You can use Javascript callbacks to set the cell or row style depending on the cell content.
-
-The example below, in which we color in red the cells with negative numbers, is directly inspired by the corresponding datatables.net [example](https://datatables.net/reference/option/columns.createdCell).
-
-Note how the Javascript callback is declared as `JavascriptFunction` object below.
-
-```{code-cell}
-from itables import JavascriptFunction
-
-show(
-    pd.DataFrame([[-1, 2, -3, 4, -5], [6, -7, 8, -9, 10]], columns=list("abcde")),
-    columnDefs=[
-        {
-            "targets": "_all",
-            "createdCell": JavascriptFunction(
-                """
-function (td, cellData, rowData, row, col) {
-    if (cellData < 0) {
-        $(td).css('color', 'red')
-    }
-}
-"""
-            ),
-        }
-    ],
-)
-```
-
-```{tip}
-Since `itables==1.6.0`, you can also render
-[Pandas style](pandas_style.md) objects as interactive datatables -
-that might be a simpler alternative to the JavaScript callbacks
- documented here.
-```
-
-## Column width
-
-The [`columnDefs.width`](https://datatables.net/reference/option/columns.width) argument let you adjust the column widths.
-
-Note that the default value of `style`, or of `autoWidth` (defaults to `True`), might override custom column widths,
-so you might have to change their values as in the examples below.
-
-You can set a fixed width for all the columns with `"targets": "_all"`:
-
-```{code-cell}
-:tags: [full-width]
-
-show(
-    df,
-    columnDefs=[{"width": "120px", "targets": "_all"}],
-    scrollX=True,
-    style="width:1200px",
-    autoWidth=False,
-)
-```
-
-You can also adjust the width of selected columns only:
-
-```{code-cell}
-:tags: [full-width]
-
-show(
-    df,
-    columnDefs=[{"width": "30%", "targets": [2, 3]}],
-    style="width:100%;margin:auto",
-)
-```
-
-If you wish you can also set a value for `columnDefs` permanently in `itables.options` as demonstrated in the cell alignment example below.
-
-## Cell alignment
-
-You can use the datatables.net [cell classes](https://datatables.net/manual/styling/classes#Cell-classes) like `dt-left`, `dt-center`, `dt-right` etc. to set the cell alignment. Specify it for one table by using the `columnDefs` argument of `show`
-
-```{code-cell}
-show(df, columnDefs=[{"className": "dt-center", "targets": "_all"}])
-```
-
-or globally by setting `opt.columnDefs`:
-
-```{code-cell}
-opt.columnDefs = [{"className": "dt-center", "targets": "_all"}]
-df
-```
-
-```{code-cell}
-del opt.columnDefs
-```
-
-## HTML in cells
-
-```{code-cell}
-pd.Series(
-    [
-        "<b>bold</b>",
-        "<i>italic</i>",
-        '<a href="https://github.com/mwouts/itables">link</a>',
-    ],
-    name="HTML",
-)
-```
-
-## Images in cells
-
-Since HTML is supported you can display images in your tables.
-You can use either
-- `<img src="https://...">` with an url
-- `<img src="data:image/png;base64, ...">` with a [base64 encoded image](https://stackoverflow.com/a/8499716/9817073).
-
-```{code-cell}
-pd.Series(
-    {
-        "url": '<img src="https://storage.googleapis.com/tfds-data/visualization/fig/mnist-3.0.1.png" height="50" alt="MNIST">',
-        "base64": '<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA'
-        "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO"
-        '9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot">',
-    },
-    name="Images",
-)
-```
-
-## The search option
-
-The [search option](https://datatables.net/reference/option/search) let you control the initial value for the search field, and whether the query should be treated as a regular expression or not:
-
-```{code-cell}
-show(df, search={"regex": True, "caseInsensitive": True, "search": "s.ain"})
-```
-
-## Select rows
-
-Not currently implemented. May be made available at a later stage using the [select](https://datatables.net/extensions/select/) extension for datatables.
-
-+++
-
-## Copy, CSV, PDF and Excel buttons
-
-Not currently implemented. May be made available at a later stage thanks to the [buttons](https://datatables.net/extensions/buttons/) extension for datatable.
diff --git a/docs/changelog.md b/docs/changelog.md
index c2004c3c..8c00520e 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,6 +1,20 @@
 ITables ChangeLog
 =================
 
+2.0.0 (2024-03-16)
+------------------
+
+**Added**
+- The CSV, Excel and Print buttons are now included ([#50](https://github.com/mwouts/itables/issues/50), [#155](https://github.com/mwouts/itables/issues/155))
+- We have included a few other extensions like SearchBuilder and SearchPanes and documented how to add more ([#178](https://github.com/mwouts/itables/issues/178), [#207](https://github.com/mwouts/itables/issues/207), [#208](https://github.com/mwouts/itables/issues/208), [#231](https://github.com/mwouts/itables/issues/231))
+- ITables is now tested with Python 3.12
+
+**Changed**
+- ITables uses the latest version `2.0.2` of `DataTables` ([#121](https://github.com/mwouts/itables/issues/121))
+- Large Python integers are now mapped to JavaScript `BigInt` ([#172](https://github.com/mwouts/itables/issues/172))
+- ITables is build using `hatch` and `pyproject.toml`
+
+
 1.7.1 (2024-03-05)
 ------------------
 
diff --git a/docs/custom_css.md b/docs/custom_css.md
index 462f4ad1..7878dd25 100644
--- a/docs/custom_css.md
+++ b/docs/custom_css.md
@@ -12,30 +12,177 @@ kernelspec:
   name: itables
 ---
 
-# Custom CSS
+# Styling
 
-You can change the global CSS used to render the tables
-by either passing a custom CSS to the `show` function, or by
-changing `opt.css`.
-
-Note that the CSS must be the same for all the tables
-in a given notebook. To change the CSS for just one table,
-use the [`style`](advanced_parameters.html#position-and-width) argument of the `show` function.
+As usual, we initialize ITables with `init_notebook_mode`, and we create two sample DataFrames:
 
 ```{code-cell}
+:tags: [hide-input]
+
+import pandas as pd
+
+import itables.options as opt
 from itables import init_notebook_mode, show
 from itables.sample_dfs import get_countries
-import itables.options as opt
 
+df = get_countries(html=False)
+df_small = pd.DataFrame({"a": [2, 1]})
+
+init_notebook_mode(all_interactive=True)
+```
+
+## Classes
+
+Select how your table looks like with the `classes` argument (defaults to `"display nowrap"`) of the `show` function, or by changing `itables.options.classes`.
+
+Add `"compact"` if you want a denser table:
+
+```{code-cell}
+:tags: [full-width]
+
+show(df, classes="display nowrap compact")
+```
+
+Remove `"nowrap"` if you want the cell content to be wrapped:
+
+```{code-cell}
+:tags: [full-width]
+
+show(df, classes="display")
+```
 
-opt.css = """
-.itables table td { font-style: italic; }
-.itables table th { font-style: oblique; }
+[More options](https://datatables.net/manual/styling/classes#Table-classes) like `"cell-border"` are available:
+
+```{code-cell}
+:tags: [full-width]
+
+show(df, classes="display nowrap cell-border")
+```
+
+## CSS
+
+You can use CSS to alter how the interactive DataTables are rendered.
+
+For instance, we change the
+[font size](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size)
+for all the tables in the document with this code:
+
+```{code-cell}
+from IPython.display import HTML, display
+
+css = """
+.dt-container {
+  font-size: small;
+}
 """
+display(HTML(f"<style>{css}</style>" ""))
+```
 
-init_notebook_mode(all_interactive=True)
+This is helpful for instance in the context of
+[Quarto presentations](quarto.md).
+
+With this over CSS, we change _every datatable_ table header
+in the notebook to bold/italic.
+
+```{code-cell}
+css = """
+.dataTable th {
+    font-weight: bolder;
+    font-style: italic;
+}
+"""
+display(HTML(f"<style>{css}</style>" ""))
+```
+
+You might also want to alter the style of specific tables only.
+To do this, add a new class to the target tables, as
+in the example below:
+
+```{code-cell}
+class_specific_css = ".table_with_monospace_font { font-family: courier, monospace }"
+display(HTML(f"<style>{class_specific_css}</style>" ""))
+```
+
+```{code-cell}
+show(df, classes="display nowrap table_with_monospace_font")
+```
+
+(style)=
+## The style argument
+
+The `show` function has a `style` argument that determines the
+style for that particular table.
+
+The default value for `style` is `table-layout:auto;width:auto;margin:auto;caption-side:bottom`.
+Without `width:auto`, tables with few columns still take the full notebook width in Jupyter.
+Using `margin:auto` makes non-wide tables centered in Jupyter.
+
+## Position and width
+
+You can set a specific width or position for a table using with the `style` argument of the show function:
+
+```{code-cell}
+show(df_small, style="table-layout:auto;width:50%;float:right")
+```
+
+or you can also change it for all tables by changing `itables.options.style`:
+
+```python
+import itables.options as opt
+
+opt.style = "table-layout:auto;width:auto"
 ```
 
+```{tip}
+For ajusting the height of a table, see the section on [pagination](advanced_parameters.md#pagination).
+```
+
+## Column width
+
+The [`columnDefs.width`](https://datatables.net/reference/option/columns.width) argument let you adjust the column widths.
+
+Note that the default value of `style`, or of `autoWidth` (defaults to `True`), might override custom column widths,
+so you might have to change their values as in the examples below.
+
+You can set a fixed width for all the columns with `"targets": "_all"`:
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    df,
+    columnDefs=[{"width": "120px", "targets": "_all"}],
+    scrollX=True,
+    style="width:1200px",
+    autoWidth=False,
+)
+```
+
+You can also adjust the width of selected columns only:
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    df,
+    columnDefs=[{"width": "30%", "targets": [2, 3]}],
+    style="width:100%;margin:auto",
+)
+```
+
+If you wish you can also set a value for `columnDefs` permanently in `itables.options` as demonstrated in the cell alignment example below.
+
+## Cell alignment
+
+You can use the datatables.net [cell classes](https://datatables.net/manual/styling/classes#Cell-classes) like `dt-left`, `dt-center`, `dt-right` etc. to set the cell alignment. Specify it for one table by using the `columnDefs` argument of `show`
+
+```{code-cell}
+show(df, columnDefs=[{"className": "dt-center", "targets": "_all"}])
+```
+
+or globally by setting `opt.columnDefs`:
+
 ```{code-cell}
-get_countries()
+opt.columnDefs = [{"className": "dt-center", "targets": "_all"}]
+df
 ```
diff --git a/docs/custom_extensions.md b/docs/custom_extensions.md
new file mode 100644
index 00000000..8d644589
--- /dev/null
+++ b/docs/custom_extensions.md
@@ -0,0 +1,104 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: itables
+  language: python
+  name: itables
+---
+
+# Custom Extensions
+
+## Internationalisation
+
+```{code-cell}
+from itables import show
+from itables.sample_dfs import get_countries
+
+df = get_countries(html=False)
+```
+
+DataTables controls can use a different language than English. To
+display the table controls in another language, go to the [internationalisation](https://datatables.net/plug-ins/i18n/)
+plug-ins page and find the language URL, like e.g.
+
+```{code-cell}
+show(
+    df,
+    language={"url": "https://cdn.datatables.net/plug-ins/2.0.2/i18n/fr-FR.json"},
+)
+```
+
+```tip
+You can also use the internationalization in the offline mode. Download the translation file,
+then set `opt.language` accordingly:
+
+~~~python
+import json
+import itables.options as opt
+
+with open("fr-FR.json") as fp:
+    opt.language = json.load(fp)
+~~~
+```
+
+## Creating a custom DataTables bundle
+
+To use custom extensions in the offline mode, you will need
+to create a bundle of jQuery, DataTables, and the desired extensions.
+
+To do so, make a copy of
+[`itables/dt_for_itables`](https://github.com/mwouts/itables/tree/main/itables/dt_for_itables):
+```bash
+$ ls itables/dt_for_itables/
+package.json  package-lock.json  README.md  src.js
+```
+
+Add or remove the desired extensions in `package.json` and `src.js`. To do this,
+you can use the [DataTables download](https://datatables.net/download/) page and
+follow the instructions for the _NPM_ download method.
+
+For instance, say you want to bundle the PDF export button. Change
+`src.js` to this code:
+```javascript
+import JSZip from 'jszip';
+import jQuery from 'jquery';
+import pdfMake from 'pdfmake';
+import DataTable from 'datatables.net-dt';
+import 'datatables.net-dt/css/dataTables.dataTables.min.css';
+
+import 'datatables.net-buttons-dt';
+import 'datatables.net-buttons/js/buttons.html5.mjs';
+import 'datatables.net-buttons/js/buttons.print.mjs';
+import 'datatables.net-buttons-dt/css/buttons.dataTables.min.css';
+
+DataTable.Buttons.jszip(JSZip);
+DataTable.Buttons.pdfMake(pdfMake);
+
+pdfMake.vfs = pdfFonts.pdfMake.vfs;
+
+export { DataTable, jQuery };
+```
+
+and run these commands:
+```bash
+# Install the dependencies in package.json
+npm install
+
+# Install the additional dependencies
+npm install pdfmake --save
+
+# Create dt_bundle.js and dt_bundle.css
+npm run build
+```
+
+Finally, you can either deploy `dt_bundle.js` and `dt_bundle.css` on an
+http server and pass the URL of `dt_bundle.js` as the `dt_url` option to `show`,
+or, in the offline mode, pass the path to `dt_bundle.js`
+as the `dt_bundle` argument of the `init_notebook_mode` method (in either
+case you can set the values permanently on `itables.options`).
diff --git a/docs/downsampling.md b/docs/downsampling.md
index 3136d2df..70de9a44 100644
--- a/docs/downsampling.md
+++ b/docs/downsampling.md
@@ -21,16 +21,15 @@ When the data in a table is larger than `maxBytes`, which is equal to 64KB by de
 If you wish, you can increase the value of `maxBytes` or even deactivate the limit (with `maxBytes=0`). Similarly, you can set a limit on the number of rows (`maxRows`, defaults to 0) or columns (`maxColumns`, defaults to `200`).
 
 ```{code-cell}
+import itables.options as opt
 from itables import init_notebook_mode, show
+from itables.downsample import nbytes
+from itables.sample_dfs import get_indicators
 
 init_notebook_mode(all_interactive=True)
 ```
 
 ```{code-cell}
-from itables.sample_dfs import get_indicators
-from itables.downsample import nbytes
-import itables.options as opt
-
 opt.lengthMenu = [2, 5, 10, 20, 50, 100, 200, 500]
 opt.maxBytes = 10000
 
diff --git a/docs/extensions.md b/docs/extensions.md
new file mode 100644
index 00000000..786a5701
--- /dev/null
+++ b/docs/extensions.md
@@ -0,0 +1,205 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: itables
+  language: python
+  name: itables
+---
+
+# DataTables Extensions
+
+DataTables comes with a series of [extensions](https://datatables.net/extensions/), which are supported by ITables since v2.0.
+A selection of these extensions are included in ITables.
+
+As usual, we activate ITables with:
+
+```{code-cell}
+from itables import init_notebook_mode, show
+
+init_notebook_mode()
+```
+
+and then, we create a few example dataframes:
+
+```{code-cell}
+:tags: [hide-input]
+
+import string
+
+import numpy as np
+import pandas as pd
+
+from itables.sample_dfs import get_countries
+
+df = get_countries(html=False)
+# Add columns for the searchPanes demo
+df["climate_zone"] = np.where(
+    df["latitude"].abs() < 23.43615,
+    "Tropical",
+    np.where(
+        df["latitude"].abs() < 35,
+        "Sub-tropical",
+        # Artic circle is 66.563861 but there is no capital there => using 64
+        np.where(df["latitude"].abs() < 64, "Temperate", "Frigid"),
+    ),
+)
+df["hemisphere"] = np.where(df["latitude"] > 0, "North", "South")
+wide_df = pd.DataFrame(
+    {
+        letter: np.random.normal(size=100)
+        for letter in string.ascii_lowercase + string.ascii_uppercase
+    }
+)
+```
+
+## Buttons
+
+The DataTables [buttons](https://datatables.net/extensions/buttons/) let you copy the table data, or export it as CSV or Excel files.
+
+To display the buttons, you need to pass a `buttons` argument to the `show` function:
+
+```{code-cell}
+:tags: [full-width]
+
+show(df, buttons=["copyHtml5", "csvHtml5", "excelHtml5"])
+```
+
+You can also specify a [`layout`](layout) modifier that will decide
+the location of the buttons (the default is `layout={"topStart": "buttons"}`). And if
+you want to keep the pagination control too, you can add `"pageLength"` to the list of buttons.
+
+As always, it is possible to set default values for these parameters by setting these on `itables.options`. For instance, set
+```python
+opt.buttons = ["copyHtml5", "csvHtml5", "excelHtml5"]
+```
+to get the buttons for all tables.
+
+
+By default, the exported file name is the name of the HTML page. To change it, set a
+[`title` option](https://datatables.net/extensions/buttons/examples/html5/filename.html) on the buttons, like
+here:
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    df,
+    buttons=[
+        "pageLength",
+        {"extend": "csvHtml5", "title": "Countries"},
+        {"extend": "excelHtml5", "title": "Countries"},
+    ],
+)
+```
+
+```{warning}
+The PDF button is not included in ITables's DataTable bundle. This is because the required PDF libraries
+have a large footprint on the bundle size. Still, you can add it to your custom bundle, see the next chapter.
+```
+
+## SearchPanes
+
+[SearchPanes](https://datatables.net/extensions/searchpanes/) is an extension that lets you select rows based on
+unique values. In the example below we have activated the cascade filtering through the
+[`searchPanes.cascadePanes`](https://datatables.net/extensions/searchpanes/examples/initialisation/cascadePanes.html) argument.
+Note that, in Jupyter, the [`searchPanes.layout`](https://datatables.net/extensions/searchpanes/layout)
+argument is required (otherwise the search panes are too wide).
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    df,
+    layout={"top1": "searchPanes"},
+    searchPanes={"layout": "columns-3", "cascadePanes": True},
+)
+```
+
+```{warning}
+When searching, please keep in mind that ITables will [downsample](downsampling.md) your table if it is larger than `maxBytes`,
+so you might not see the full dataset - pay attention to the downsampling message at the bottom left of the table.
+```
+
+## SearchBuilder
+
+[SearchBuilder](https://datatables.net/extensions/searchbuilder/) let you build complex search queries. You just need to add it to the layout
+by passing e.g. `layout={"top1": "searchBuilder"}`.
+
+It is possible to set a predefined search, as we do in the below:
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    df,
+    layout={"top1": "searchBuilder"},
+    searchBuilder={
+        "preDefined": {
+            "criteria": [
+                {"data": "climate_zone", "condition": "=", "value": ["Sub-tropical"]}
+            ]
+        }
+    },
+)
+```
+
+## FixedColumns
+
+[FixedColumn](https://datatables.net/extensions/fixedcolumns/) is an extension
+that let you fix some columns as you scroll horizontally.
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    wide_df,
+    fixedColumns={"start": 1, "end": 2},
+    scrollX=True,
+)
+```
+
+## KeyTable
+
+With the [KeyTable](https://datatables.net/extensions/keytable/) extension you can navigate in a table using the arrow keys:
+
+```{code-cell}
+:tags: [full-width]
+
+show(df, keys=True)
+```
+
+```{tip}
+You can activate this option for all your tables with
+
+~~~python
+import itables.options as opt
+
+opt.keys = True
+~~~
+```
+
+```{warning}
+The KeyTable extension works in Jupyter Book (try it here in the documentation) but not in JupyterLab.
+```
+
+## RowGroup
+
+Use the [RowGroup](https://datatables.net/extensions/rowgroup/) extension to group
+the data according to the content of one colum. Optionally, you can hide the content
+of that column to avoid duplicating the information.
+
+```{code-cell}
+:tags: [full-width]
+
+show(
+    df.sort_values("region"),
+    rowGroup={"dataSrc": 1},
+    columnDefs=[{"targets": 1, "visible": False}],
+)
+```
diff --git a/docs/formatting.md b/docs/formatting.md
new file mode 100644
index 00000000..5822de15
--- /dev/null
+++ b/docs/formatting.md
@@ -0,0 +1,172 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: itables
+  language: python
+  name: itables
+---
+
+# Formatting
+
+## Formatting with Pandas
+
+`itables` builds the HTML representation of your Pandas dataframes using Pandas itself, so
+you can use [Pandas' formatting options](https://pandas.pydata.org/pandas-docs/stable/user_guide/options.html).
+For instance, you can change the precision used to display floating numbers:
+
+```{code-cell}
+from itables import init_notebook_mode, show
+from itables.sample_dfs import get_countries
+
+init_notebook_mode(all_interactive=True)
+```
+
+```{code-cell}
+import math
+
+import pandas as pd
+
+with pd.option_context("display.float_format", "{:,.2f}".format):
+    show(pd.Series([i * math.pi for i in range(1, 6)]))
+```
+
+Or you can use a custom formatter:
+
+```{code-cell}
+with pd.option_context("display.float_format", "${:,.2f}".format):
+    show(pd.Series([i * math.pi for i in range(1, 6)]))
+```
+
+## Formatting with Javascript
+
+Numbers are formatted using Pandas, then are converted back to float to ensure they come in the right order when sorted.
+Therefore, to achieve a particular formatting you might have to resort to the
+[`columns.render` option](https://datatables.net/examples/advanced_init/column_render.html)
+of DataTables.
+
+For instance, this [example](https://datatables.net/forums/discussion/61407/how-to-apply-a-numeric-format-to-a-column)
+can be ported like this:
+
+```{code-cell}
+from itables import JavascriptCode
+
+show(
+    pd.Series([i * math.pi * 1e4 for i in range(1, 6)]),
+    columnDefs=[
+        {
+            "targets": "_all",
+            "render": JavascriptCode("$.fn.dataTable.render.number(',', '.', 3, '$')"),
+        }
+    ],
+)
+```
+
+## Colors based on cell values
+
+You can use Javascript callbacks to set the cell or row style depending on the cell content.
+
+The example below, in which we color in red the cells with negative numbers, is directly inspired by the corresponding datatables.net [example](https://datatables.net/reference/option/columns.createdCell).
+
+Note how the Javascript callback is declared as `JavascriptFunction` object below.
+
+```{code-cell}
+from itables import JavascriptFunction
+
+show(
+    pd.DataFrame([[-1, 2, -3, 4, -5], [6, -7, 8, -9, 10]], columns=list("abcde")),
+    columnDefs=[
+        {
+            "targets": "_all",
+            "createdCell": JavascriptFunction(
+                """
+function (td, cellData, rowData, row, col) {
+    if (cellData < 0) {
+        $(td).css('color', 'red')
+    }
+}
+"""
+            ),
+        }
+    ],
+)
+```
+
+## Formatting with Pandas style
+
+ITables in version 1.6 and above can render
+[Pandas Style](https://pandas.pydata.org/docs/user_guide/style.html)
+objects as interactive DataTables.
+
+This way, you can easily add background color, and even
+tooltips to your dataframes, and still get them
+displayed using DataTables - see our [examples](pandas_style.md).
+
+```{warning}
+Please note that Pandas Style objects are rendered using
+their `.to_html()` method, which is less efficient that
+the default JS data export used by ITables.
+```
+
+## HTML in cells
+
+### A simple example
+
+HTML content is supported, which means that you can have formatted text,
+links or even images in your tables:
+
+```{code-cell}
+pd.Series(
+    [
+        "<b>bold</b>",
+        "<i>italic</i>",
+        '<a href="https://github.com/mwouts/itables">link</a>',
+    ],
+    name="HTML",
+)
+```
+
+### Images in a table
+
+```{code-cell}
+:tags: [full-width]
+
+df = get_countries(html=False)
+
+df["flag"] = [
+    '<a href="https://flagpedia.net/{code}">'
+    '<img src="https://flagpedia.net/data/flags/h80/{code}.webp" '
+    'alt="Flag of {country}"></a>'.format(code=code.lower(), country=country)
+    for code, country in zip(df.index, df["country"])
+]
+df["country"] = [
+    '<a href="https://en.wikipedia.org/wiki/{}">{}</a>'.format(country, country)
+    for country in df["country"]
+]
+df["capital"] = [
+    '<a href="https://en.wikipedia.org/wiki/{}">{}</a>'.format(capital, capital)
+    for capital in df["capital"]
+]
+df
+```
+
+### Base64 images
+
+[Base64 encoded image](https://stackoverflow.com/a/8499716/9817073) are supported, too:
+
+```{code-cell}
+pd.Series(
+    {
+        "url": '<img src="https://storage.googleapis.com/tfds-data/visualization/fig/mnist-3.0.1.png" height="50" alt="MNIST">',
+        "base64": '<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA'
+        "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO"
+        '9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot">',
+    },
+    name="Images",
+)
+```
diff --git a/docs/pandas_style.md b/docs/pandas_style.md
index ddc6f528..8db0feab 100644
--- a/docs/pandas_style.md
+++ b/docs/pandas_style.md
@@ -12,14 +12,15 @@ kernelspec:
   name: itables
 ---
 
-# Pandas Style
+# Pandas Style examples
 
 Starting with `itables>=1.6.0`, ITables provides support for
 [Pandas Style](https://pandas.pydata.org/docs/user_guide/style.html).
 
 ```{code-cell}
-import pandas as pd
 import numpy as np
+import pandas as pd
+
 from itables import init_notebook_mode
 
 init_notebook_mode(all_interactive=True)
diff --git a/docs/polars_dataframes.md b/docs/polars_dataframes.md
index 753d2081..f4967844 100644
--- a/docs/polars_dataframes.md
+++ b/docs/polars_dataframes.md
@@ -168,9 +168,5 @@ show(dict_of_test_dfs["named_column_index"])
 ## big_integers
 
 ```{code-cell}
-import itables.options as opt
-
-opt.warn_on_int_to_str_conversion = False
-
 show(dict_of_test_dfs["big_integers"])
 ```
diff --git a/docs/quarto/quarto_html.qmd b/docs/quarto/quarto_html.qmd
index d6321682..11e3c86e 100644
--- a/docs/quarto/quarto_html.qmd
+++ b/docs/quarto/quarto_html.qmd
@@ -26,17 +26,18 @@ jupyter:
 ```{python}
 #| echo: false
 
+from IPython.display import display, HTML
 import itables.options as opt
 
 # show 5 rows per 'page'
 opt.lengthMenu = [5]
 
 # don't show the length control
-opt.dom = "frtip"
+opt.layout["topStart"] = None
 
 # use a smaller font (default is medium)
 # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size
-opt.css += ".dataTables_wrapper { font-size: small; }"
+display(HTML("<style>.dt-container { font-size: small; }</style>"))
 ```
 
 ```{python}
@@ -70,6 +71,7 @@ get_countries(html=False)
 ## This document uses
 
 ```python
+from IPython.display import display, HTML
 import itables.options as opt
 
 
@@ -77,11 +79,11 @@ import itables.options as opt
 opt.lengthMenu = [5]
 
 # don't show the length control
-opt.dom = "frtip"
+opt.layout["topStart"] = None
 
 # use a smaller font (default is medium)
 # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size
-opt.css += ".dataTables_wrapper { font-size: x-small; }"
+display(HTML("<style>.dt-container { font-size: small; }</style>"))
 ```
 :::
 
@@ -122,7 +124,7 @@ under a MIT license.
 
 ITables renders Pandas or Polars DataFrames as
 interactive HTML tables using the JavaScript
-[datatables-net](https://datatables.net/) library.
+[DataTables](https://datatables.net/) library.
 :::
 
 ::: {.callout-tip}
diff --git a/docs/quarto/quarto_revealjs.qmd b/docs/quarto/quarto_revealjs.qmd
index 890798cb..bd5e5653 100644
--- a/docs/quarto/quarto_revealjs.qmd
+++ b/docs/quarto/quarto_revealjs.qmd
@@ -26,17 +26,18 @@ jupyter:
 ```{python}
 #| echo: false
 
+from IPython.display import display, HTML
 import itables.options as opt
 
 # show 5 rows per 'page'
 opt.lengthMenu = [5]
 
 # don't show the length control
-opt.dom = "frtip"
+opt.layout["topStart"] = None
 
 # use a smaller font (default is medium)
 # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size
-opt.css += ".dataTables_wrapper { font-size: small; }"
+display(HTML("<style>.dt-container { font-size: medium; }</style>"))
 ```
 
 ```{python}
@@ -70,6 +71,7 @@ get_countries(html=False)
 ## This document uses
 
 ```python
+from IPython.display import display, HTML
 import itables.options as opt
 
 
@@ -77,11 +79,11 @@ import itables.options as opt
 opt.lengthMenu = [5]
 
 # don't show the length control
-opt.dom = "frtip"
+opt.layout["topStart"] = None
 
 # use a smaller font (default is medium)
 # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size
-opt.css += ".dataTables_wrapper { font-size: small; }"
+display(HTML("<style>.dt-container { font-size: medium; }</style>"))
 ```
 :::
 
@@ -122,7 +124,7 @@ under a MIT license.
 
 ITables renders Pandas or Polars DataFrames as
 interactive HTML tables using the JavaScript
-[datatables-net](https://datatables.net/) library.
+[DataTables](https://datatables.net/) library.
 :::
 
 ::: {.callout-tip}
diff --git a/docs/quick_start.md b/docs/quick_start.md
index da7a323a..f44e0d30 100644
--- a/docs/quick_start.md
+++ b/docs/quick_start.md
@@ -16,7 +16,6 @@ kernelspec:
 
 ![CI](https://github.com/mwouts/itables/actions/workflows/continuous-integration.yml/badge.svg?branch=main)
 [![codecov.io](https://codecov.io/github/mwouts/itables/coverage.svg?branch=main)](https://codecov.io/github/mwouts/itables?branch=main)
-[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/mwouts/itables.svg)](https://lgtm.com/projects/g/mwouts/itables/context:python)
 [![Pypi](https://img.shields.io/pypi/v/itables.svg)](https://pypi.python.org/pypi/itables)
 [![Conda Version](https://img.shields.io/conda/vn/conda-forge/itables.svg)](https://anaconda.org/conda-forge/itables)
 [![pyversions](https://img.shields.io/pypi/pyversions/itables.svg)](https://pypi.python.org/pypi/itables)
@@ -24,10 +23,10 @@ kernelspec:
 <a class="github-button" href="https://github.com/mwouts/itables" data-icon="octicon-star" data-show-count="true" aria-label="Star mwouts/itables on GitHub">Star</a>
 <script async defer src="https://buttons.github.io/buttons.js"></script>
 
-## Turn your Python DataFrames into Interactive Tables
+## Turn your Python DataFrames into Interactive DataTables
 
 This packages changes how Pandas and Polars DataFrames are rendered in Jupyter Notebooks.
-With `itables` you can display your tables as interactive [datatables](https://datatables.net/)
+With `itables` you can display your tables as interactive [DataTables](https://datatables.net/)
 that you can sort, paginate, scroll or filter.
 
 ITables is just about how tables are displayed. You can turn it on and off in just two lines,
@@ -61,7 +60,7 @@ init_notebook_mode(all_interactive=True)
 ```
 
 After this, any Pandas or Polars DataFrame, or Series,
-is displayed as an interactive [datatables.net](https://datatables.net/) table,
+is displayed as an interactive [DataTables](https://datatables.net/),
 which lets you explore, filter or sort your data.
 
 ```{code-cell}
@@ -74,7 +73,7 @@ df
 ## Offline mode versus connected mode
 
 ITables use two Javascript libraries:
-[jquery](https://jquery.com/) and [datatables.net](https://datatables.net/).
+[jQuery](https://jquery.com/) and [DataTables](https://datatables.net/).
 
 By default `itables` works offline. No internet connection is required
 as the two libraries are embedded into the notebook itself
@@ -86,11 +85,10 @@ To do so, add the argument `connected=True` when you
 execute `init_notebook_mode`. This will also make your notebook lighter by
 about [700kB](https://github.com/mwouts/itables/blob/main/tests/test_connected_notebook_is_small.py).
 
-## Formatting specific tables only
+## Using ITables for specific tables only
 
 If you prefer to render only certain series or dataframes using `itables`,
-or you want to use the [advanced parameters](advanced_parameters.md), then
-use `init_notebook_mode(all_interactive=False)` then `show`:
+then call `init_notebook_mode(all_interactive=False)` then `show`:
 
 ```{code-cell}
 from itables import show
@@ -98,31 +96,6 @@ from itables import show
 show(df, lengthMenu=[2, 5, 10, 25, 50, 100, 250])
 ```
 
-## HTML content
-
-HTML content is supported, which means that you can have formatted text,
-links or even images in your tables:
-
-```{code-cell}
-:tags: [full-width]
-
-df["flag"] = [
-    '<a href="https://flagpedia.net/{code}">'
-    '<img src="https://flagpedia.net/data/flags/h80/{code}.webp" '
-    'alt="Flag of {country}"></a>'.format(code=code.lower(), country=country)
-    for code, country in zip(df.index, df["country"])
-]
-df["country"] = [
-    '<a href="https://en.wikipedia.org/wiki/{}">{}</a>'.format(country, country)
-    for country in df["country"]
-]
-df["capital"] = [
-    '<a href="https://en.wikipedia.org/wiki/{}">{}</a>'.format(capital, capital)
-    for capital in df["capital"]
-]
-df
-```
-
 ## Try ITables on Binder
 
 You can run our examples notebooks directly on [![Lab](https://img.shields.io/badge/Binder-JupyterLab-blue.svg)](https://mybinder.org/v2/gh/mwouts/itables/main?urlpath=lab/tree/docs/quick_start.md), without having to install anything on your side.
diff --git a/docs/references.md b/docs/references.md
index 5ce18ab3..dd3eb118 100644
--- a/docs/references.md
+++ b/docs/references.md
@@ -3,7 +3,7 @@
 ## DataTables
 
 - DataTables is a plug-in for the jQuery Javascript library. It has a great [documentation](https://datatables.net/manual/), and a large set of [examples](https://datatables.net/examples/index).
-- The R package [DT](https://rstudio.github.io/DT/) uses [datatables.net](https://datatables.net/) as the underlying library for both R notebooks and Shiny applications. In addition to the standard functionalities of the library (display, sort, filtering and row selection), RStudio seems to have implemented cell edition.
+- The R package [DT](https://rstudio.github.io/DT/) uses [DataTables](https://datatables.net/) as the underlying library for both R notebooks and Shiny applications. In addition to the standard functionalities of the library (display, sort, filtering and row selection), RStudio seems to have implemented cell edition.
 
 ## Alternatives
 
diff --git a/docs/sample_dataframes.md b/docs/sample_dataframes.md
index 1083f62d..4cbdd726 100644
--- a/docs/sample_dataframes.md
+++ b/docs/sample_dataframes.md
@@ -179,9 +179,5 @@ show(dict_of_test_dfs["named_column_index"])
 ## big_integers
 
 ```{code-cell}
-import itables.options as opt
-
-opt.warn_on_int_to_str_conversion = False
-
 show(dict_of_test_dfs["big_integers"])
 ```
diff --git a/docs/supported_editors.md b/docs/supported_editors.md
index 3b455b50..4b4e8077 100644
--- a/docs/supported_editors.md
+++ b/docs/supported_editors.md
@@ -32,7 +32,7 @@ A short sample notebook is available [here](https://colab.research.google.com/dr
 
 ## VS Code
 
-In VS Code, `itables` works both for Jupyter Notebooks and Python scripts
+In VS Code, `itables` works both for Jupyter Notebooks and Python scripts.
 
 ![](images/code.png)
 
@@ -51,7 +51,7 @@ ITables works well with Quarto - check out our `html` and `revealjs` [examples](
 
 # Exporting a DataFrame to an HTML table
 
-To get the HTML representation of a Pandas DataFrame `df` as an interactive [datatable](https://datatables.net/), you can use `to_html_datatable` as below:
+To get the HTML representation of a Pandas DataFrame `df` as an interactive [DataTable](https://datatables.net/), you can use `to_html_datatable` as below:
 ```python
 from itables import to_html_datatable
 from itables.sample_dfs import get_countries
@@ -65,6 +65,7 @@ html = to_html_datatable(df)
 You can use ITables in Web applications generated with [Shiny](https://shiny.rstudio.com/py/) for Python with e.g.
 ```python
 from shiny import ui
+
 from itables.shiny import DT
 
 ui.HTML(DT(df))
diff --git a/environment.yml b/environment.yml
index 6a1061da..8a19b03d 100644
--- a/environment.yml
+++ b/environment.yml
@@ -3,30 +3,26 @@ channels:
   - defaults
   - conda-forge
 dependencies:
-  - python
+  - nodejs
+  - python<=3.10
   - jupyter
-  - jupyterlab
-  - jupytext>=1.13.8
-  - markdown-it-py>=2.0
+  - jupyterlab>=4
+  - jupytext
   - nbconvert
   - ipykernel
   - pandas
+  - matplotlib
   - polars
   - pyarrow
   - pytest
   - pytest-xdist
   - pytest-cov
   - pre-commit
-  - pylint
-  - flake8
-  - black
-  - isort
   - pip
   - setuptools
   - twine
   - ghp-import
-  # Interactive applications
   - shiny
   - pip:
     - world_bank_data
-    - jupyter_book>=0.12  # jupyter-book-0.12.2-pyhd8ed1ab_0 requires jupytext >=1.11.2,<1.12
+    - jupyter_book
diff --git a/itables/datatables_format.py b/itables/datatables_format.py
index c313d880..c2599c50 100644
--- a/itables/datatables_format.py
+++ b/itables/datatables_format.py
@@ -1,4 +1,5 @@
 import json
+import re
 import warnings
 
 import numpy as np
@@ -65,42 +66,6 @@ def default(self, obj):
     return TableValuesEncoder
 
 
-def convert_bigints_to_str(df, warn_on_int_to_str_conversion):
-    """In Javascript, integers have to remain between JS_MIN_SAFE_INTEGER and JS_MAX_SAFE_INTEGER."""
-    converted = []
-    for i, col in enumerate(df.columns):
-        try:
-            x = df.iloc[:, i]
-            if (
-                x.dtype.kind == "i"
-                and (
-                    ~x.isnull()
-                    & ((x < JS_MIN_SAFE_INTEGER) | (x > JS_MAX_SAFE_INTEGER))
-                ).any()
-            ):
-                _isetitem(df, i, x.astype(str))
-                converted.append(col)
-        except AttributeError:
-            # Polars
-            x = df[col]
-            if x.dtype in pl.INTEGER_DTYPES and (
-                (x.min() < JS_MIN_SAFE_INTEGER) or (x.max() > JS_MAX_SAFE_INTEGER)
-            ):
-                df = df.with_columns(pl.col(col).cast(pl.Utf8))
-                converted.append(col)
-
-    if converted and warn_on_int_to_str_conversion:
-        warnings.warn(
-            "The columns {} contains integers that are too large for Javascript.\n"
-            "They have been converted to str. See https://github.com/mwouts/itables/issues/172.\n"
-            "To silence this warning, please run:\n"
-            "    import itables.options as opt\n"
-            "    opt.warn_on_int_to_str_conversion = False".format(converted)
-        )
-
-    return df
-
-
 def _isetitem(df, i, value):
     """Older versions of Pandas don't have df.isetitem"""
     try:
@@ -109,12 +74,8 @@ def _isetitem(df, i, value):
         df.iloc[:, i] = value
 
 
-def datatables_rows(
-    df, count=None, warn_on_unexpected_types=False, warn_on_int_to_str_conversion=False
-):
+def datatables_rows(df, count=None, warn_on_unexpected_types=False):
     """Format the values in the table and return the data, row by row, as requested by DataTables"""
-    df = convert_bigints_to_str(df, warn_on_int_to_str_conversion)
-
     # We iterate over columns using an index rather than the column name
     # to avoid an issue in case of duplicated column names #89
     if count is None or len(df.columns) == count:
@@ -128,8 +89,32 @@ def datatables_rows(
     try:
         # Pandas DataFrame
         data = list(zip(*(empty_columns + [_format_column(x) for _, x in df.items()])))
-        return json.dumps(data, cls=generate_encoder(warn_on_unexpected_types))
+        has_bigints = any(
+            x.dtype.kind == "i"
+            and ((x > JS_MAX_SAFE_INTEGER).any() or (x < JS_MIN_SAFE_INTEGER).any())
+            for _, x in df.items()
+        )
+        js = json.dumps(data, cls=generate_encoder(warn_on_unexpected_types))
     except AttributeError:
         # Polars DataFrame
         data = list(df.iter_rows())
-        return json.dumps(data, cls=generate_encoder(False))
+        import polars as pl
+
+        has_bigints = any(
+            x.dtype in [pl.Int64, pl.UInt64]
+            and ((x > JS_MAX_SAFE_INTEGER).any() or (x < JS_MIN_SAFE_INTEGER).any())
+            for x in (df[col] for col in df.columns)
+        )
+        js = json.dumps(data, cls=generate_encoder(False))
+
+    if has_bigints:
+        js = n_suffix_for_bigints(js)
+
+    return js
+
+
+def n_suffix_for_bigints(js):
+    def n_suffix(matchobj):
+        return 'BigInt("' + matchobj.group(1) + '")' + matchobj.group(2)
+
+    return re.sub(r"(-?\d{16,})(,|])", n_suffix, js)
diff --git a/itables/dt_for_itables/LICENSE b/itables/dt_for_itables/LICENSE
new file mode 100644
index 00000000..c9789501
--- /dev/null
+++ b/itables/dt_for_itables/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Marc Wouts
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/itables/dt_for_itables/README.md b/itables/dt_for_itables/README.md
new file mode 100644
index 00000000..47966eff
--- /dev/null
+++ b/itables/dt_for_itables/README.md
@@ -0,0 +1,30 @@
+This package is a ESM bundle of [DataTables](https://datatables.net/)
+and some of its extensions for [ITables](https://github.com/mwouts/itables/).
+
+# How to compile the bundle
+
+Run the following commands:
+```bash
+npm install
+npm run build
+```
+
+# How to update the dependencies
+
+Run
+```bash
+npm update
+```
+and check whether there are any outdated package with `npm outdated`.
+
+# How to publish a new version
+
+Update the dependencies, bump the version in `package.json`, and then:
+
+```bash
+# Package the extension
+npm pack
+
+# Publish the package on npm with
+npm publish --access=public
+```
diff --git a/itables/dt_for_itables/package-lock.json b/itables/dt_for_itables/package-lock.json
new file mode 100644
index 00000000..e19e0def
--- /dev/null
+++ b/itables/dt_for_itables/package-lock.json
@@ -0,0 +1,324 @@
+{
+  "name": "dt_for_itables",
+  "version": "2.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "dt_for_itables",
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "datatables.net-buttons": "^3.0.1",
+        "datatables.net-buttons-dt": "^3.0.1",
+        "datatables.net-dt": "^2.0.0",
+        "datatables.net-fixedcolumns-dt": "^5.0.0",
+        "datatables.net-keytable-dt": "^2.12.0",
+        "datatables.net-rowgroup-dt": "^1.5.0",
+        "datatables.net-searchbuilder-dt": "^1.7.0",
+        "datatables.net-searchpanes-dt": "^2.3.0",
+        "datatables.net-select-dt": "^2.0.0",
+        "jquery": "^3.7.1",
+        "jszip": "^3.10.1"
+      },
+      "devDependencies": {
+        "esbuild": "*"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.20.1",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz",
+      "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+    },
+    "node_modules/datatables.net": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.0.2.tgz",
+      "integrity": "sha512-uQM+s1oXhpTajUw5DCxarpfAtQJMh0MKFCLZMCc+UWLWPg8ipe6L2zgDMbRC8n9UCwGVpHOwUYlQu2JU1/PhSg==",
+      "dependencies": {
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-buttons": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/datatables.net-buttons/-/datatables.net-buttons-3.0.1.tgz",
+      "integrity": "sha512-VjTEooHtjHW2VY5/iddTLMRy2G2TtT2GyYwN9EmaMsG4MClD5lWfYdkpv/NMwz175gx7fe1lh/NVAYT4W3jMSg==",
+      "dependencies": {
+        "datatables.net": "^2",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-buttons-dt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/datatables.net-buttons-dt/-/datatables.net-buttons-dt-3.0.1.tgz",
+      "integrity": "sha512-kfZrc+954qpg6krz1pcVKVhxOzbZByaGHwwO0CcjKdIREBX4la4HkoIZGWsENSYSfdHUkHu9WWa84p9Rl+JtiQ==",
+      "dependencies": {
+        "datatables.net-buttons": "3.0.1",
+        "datatables.net-dt": "^2",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-dt": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.0.2.tgz",
+      "integrity": "sha512-/Zpy7ZWGgCbCJB9qJZvaB+KFrdo+8JjjOdxL59/QiG3jquz2ZCsE9u814kbaGKTM1ARkvZVUxDB9IlH5qmHaqw==",
+      "dependencies": {
+        "datatables.net": "2.0.2",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-fixedcolumns": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns/-/datatables.net-fixedcolumns-5.0.0.tgz",
+      "integrity": "sha512-7dTJrVDkZCicx9g//N3ufuEjvwrZpcmpjbkwrtC5LiQFCnuL/hMOiOb4CBcvTg5OiTU7VmtbBuQkeAJGs3QM5g==",
+      "dependencies": {
+        "datatables.net": ">=2.0.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-fixedcolumns-dt": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns-dt/-/datatables.net-fixedcolumns-dt-5.0.0.tgz",
+      "integrity": "sha512-5KVH6EHeYSXrxrEhKq8HMKqf4iRW4T1dtiDYZRomiwfAh/0iCeaamxgT5OFN8VJ/qpiwnyrDh3SqXA8Te0kE7w==",
+      "dependencies": {
+        "datatables.net-dt": ">=2.0.0",
+        "datatables.net-fixedcolumns": "5.0.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-keytable": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-keytable/-/datatables.net-keytable-2.12.0.tgz",
+      "integrity": "sha512-vijxMw7ZIB/fDe5FWGiDqe8CPiPXg3lvqK4lL48vQh1zoiE3+0C3za82qM9g/2zbwtIXmOLwBZc2ivrErNVPkA==",
+      "dependencies": {
+        "datatables.net": ">=1.11.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-keytable-dt": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-keytable-dt/-/datatables.net-keytable-dt-2.12.0.tgz",
+      "integrity": "sha512-FubrNaitx13Mq2cny2fxuV/Nk6bbmGw6VrH9Df81CT/jVfwAEkwIFirETEoL/hZfQDcD76ujj0b1wNDZ5Bsv9Q==",
+      "dependencies": {
+        "datatables.net-dt": ">=1.11.0",
+        "datatables.net-keytable": "2.12.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-rowgroup": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-rowgroup/-/datatables.net-rowgroup-1.5.0.tgz",
+      "integrity": "sha512-V/CLJu7rMjxwfhZAv59emZOPw58cwnYmd8NXTTJSnqBDgOZsaC9mtVo0ejBpdqvNw5WmMPy3AJceH+ay6JQ3hA==",
+      "dependencies": {
+        "datatables.net": ">=1.11.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-rowgroup-dt": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-rowgroup-dt/-/datatables.net-rowgroup-dt-1.5.0.tgz",
+      "integrity": "sha512-Dmn+ToLeUXDDg58Skie25xiWDy0GfgAMbiB8CMnOWKXH/gnGRjoU1V80R7ll8HbxGB6YfppXkPsPn00XU7c2xA==",
+      "dependencies": {
+        "datatables.net-dt": ">=1.11.0",
+        "datatables.net-rowgroup": "1.5.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-searchbuilder": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder/-/datatables.net-searchbuilder-1.7.0.tgz",
+      "integrity": "sha512-JGqxDVudgRxrHs/J+VM9O2rcwPgaUjpqBL1MHJAVJw+4vxvAY5Sbb0qP6ayo8h4yZyAE2+aPRm/smJ24N0nztw==",
+      "dependencies": {
+        "datatables.net": ">=1.11.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-searchbuilder-dt": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder-dt/-/datatables.net-searchbuilder-dt-1.7.0.tgz",
+      "integrity": "sha512-FKeL5xDEO4dF0CA+Vp4KkAdOdhfoCB3KaWdHiscki/9x4r03e0lD8g0V2oynHe4EshLHxFb/ug1Rd5GLcW/yAg==",
+      "dependencies": {
+        "datatables.net-dt": ">=1.11.0",
+        "datatables.net-searchbuilder": "1.7.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-searchpanes": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-searchpanes/-/datatables.net-searchpanes-2.3.0.tgz",
+      "integrity": "sha512-AAl03TQXatzQh6gqNot1BAzenMxQ0/mxX+Nzn770mdTUmhVy2VX8pa7/vZlwe0tRbTcZ9VZMBMErCb66i5X3rA==",
+      "dependencies": {
+        "datatables.net": ">=1.11.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-searchpanes-dt": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-searchpanes-dt/-/datatables.net-searchpanes-dt-2.3.0.tgz",
+      "integrity": "sha512-ee4sEYYPRb+uXtAGL9KmIyjyyh8Ox4EI0a/GKHsCRrO1BTrnh7lZ+z8hJUc/TZJFDkgbYHh8XGPQXLOZUpIXHA==",
+      "dependencies": {
+        "datatables.net-dt": ">=1.11.0",
+        "datatables.net-searchpanes": "2.3.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-select": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-select/-/datatables.net-select-2.0.0.tgz",
+      "integrity": "sha512-sKMNoTlJejk5FfZo6Niwdn2/bHSDYiIt5WuMSsXzMGiCTIPtnDiYjNHF843vToKiTTsi+6T0zUuWddHLGPRsxA==",
+      "dependencies": {
+        "datatables.net": ">=2.0.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/datatables.net-select-dt": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/datatables.net-select-dt/-/datatables.net-select-dt-2.0.0.tgz",
+      "integrity": "sha512-w6uP7Q4/U675dkrC2Lg0B3P7cwUTxfyUgcc+Qt4XUyAqa+6rk/YjiYHfsrDZ9vUZdu66DFdj7hNZtZfkBBPjLg==",
+      "dependencies": {
+        "datatables.net-dt": ">=2.0.0",
+        "datatables.net-select": "2.0.0",
+        "jquery": ">=1.7"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.20.1",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz",
+      "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.20.1",
+        "@esbuild/android-arm": "0.20.1",
+        "@esbuild/android-arm64": "0.20.1",
+        "@esbuild/android-x64": "0.20.1",
+        "@esbuild/darwin-arm64": "0.20.1",
+        "@esbuild/darwin-x64": "0.20.1",
+        "@esbuild/freebsd-arm64": "0.20.1",
+        "@esbuild/freebsd-x64": "0.20.1",
+        "@esbuild/linux-arm": "0.20.1",
+        "@esbuild/linux-arm64": "0.20.1",
+        "@esbuild/linux-ia32": "0.20.1",
+        "@esbuild/linux-loong64": "0.20.1",
+        "@esbuild/linux-mips64el": "0.20.1",
+        "@esbuild/linux-ppc64": "0.20.1",
+        "@esbuild/linux-riscv64": "0.20.1",
+        "@esbuild/linux-s390x": "0.20.1",
+        "@esbuild/linux-x64": "0.20.1",
+        "@esbuild/netbsd-x64": "0.20.1",
+        "@esbuild/openbsd-x64": "0.20.1",
+        "@esbuild/sunos-x64": "0.20.1",
+        "@esbuild/win32-arm64": "0.20.1",
+        "@esbuild/win32-ia32": "0.20.1",
+        "@esbuild/win32-x64": "0.20.1"
+      }
+    },
+    "node_modules/immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
+    "node_modules/jquery": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
+    },
+    "node_modules/jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "dependencies": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
+    "node_modules/lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "dependencies": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+    },
+    "node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    }
+  }
+}
diff --git a/itables/dt_for_itables/package.json b/itables/dt_for_itables/package.json
new file mode 100644
index 00000000..fdedb3ee
--- /dev/null
+++ b/itables/dt_for_itables/package.json
@@ -0,0 +1,32 @@
+{
+  "name": "dt_for_itables",
+  "version": "2.0.1",
+  "description": "DataTables bundle for itables",
+  "main": "src.js",
+  "scripts": {
+    "build": "esbuild src.js --format=esm --bundle --outfile=dt_bundle.js --minify"
+  },
+  "author": "Marc Wouts",
+  "license": "MIT",
+  "dependencies": {
+    "datatables.net-buttons": "^3.0.1",
+    "datatables.net-buttons-dt": "^3.0.1",
+    "datatables.net-dt": "^2.0.0",
+    "datatables.net-fixedcolumns-dt": "^5.0.0",
+    "datatables.net-keytable-dt": "^2.12.0",
+    "datatables.net-rowgroup-dt": "^1.5.0",
+    "datatables.net-searchbuilder-dt": "^1.7.0",
+    "datatables.net-searchpanes-dt": "^2.3.0",
+    "datatables.net-select-dt": "^2.0.0",
+    "jquery": "^3.7.1",
+    "jszip": "^3.10.1"
+  },
+  "devDependencies": {
+    "esbuild": "*"
+  },
+  "homepage": "https://mwouts.github.io/itables",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/mwouts/itables.git"
+  }
+}
diff --git a/itables/dt_for_itables/src.js b/itables/dt_for_itables/src.js
new file mode 100644
index 00000000..3b2a178f
--- /dev/null
+++ b/itables/dt_for_itables/src.js
@@ -0,0 +1,31 @@
+import JSZip from 'jszip';
+import jQuery from 'jquery';
+import DataTable from 'datatables.net-dt';
+import 'datatables.net-dt/css/dataTables.dataTables.min.css';
+
+import 'datatables.net-buttons-dt';
+import 'datatables.net-buttons/js/buttons.html5.mjs';
+import 'datatables.net-buttons/js/buttons.print.mjs';
+import 'datatables.net-buttons-dt/css/buttons.dataTables.min.css';
+
+DataTable.Buttons.jszip(JSZip);
+
+import 'datatables.net-fixedcolumns-dt';
+import 'datatables.net-fixedcolumns-dt/css/fixedColumns.dataTables.min.css';
+
+import 'datatables.net-keytable-dt';
+import 'datatables.net-keytable-dt/css/keyTable.dataTables.min.css';
+
+import 'datatables.net-rowgroup-dt';
+import 'datatables.net-rowgroup-dt/css/rowGroup.dataTables.min.css';
+
+import 'datatables.net-searchbuilder-dt';
+import 'datatables.net-searchbuilder-dt/css/searchBuilder.dataTables.min.css';
+
+import 'datatables.net-searchpanes-dt';
+import 'datatables.net-searchpanes-dt/css/searchPanes.dataTables.min.css';
+
+import 'datatables.net-select-dt';
+import 'datatables.net-select-dt/css/select.dataTables.min.css';
+
+export { DataTable, jQuery };
diff --git a/itables/html/datatables_template.html b/itables/html/datatables_template.html
index dced0b7a..4b5f9f72 100644
--- a/itables/html/datatables_template.html
+++ b/itables/html/datatables_template.html
@@ -1,22 +1,17 @@
-<div class="itables">
-<style></style>
 <table id="table_id"><thead><tr><th>A</th></tr></thead></table>
+<link href="https://www.unpkg.com/dt_for_itables@2.0.1/dt_bundle.css" rel="stylesheet">
 <script type="module">
-    const dt = (await import(window._datatables_src_for_itables)).default;
-    dt($);
+    import {DataTable, jQuery as $} from 'https://www.unpkg.com/dt_for_itables@2.0.1/dt_bundle.js';
 
-    $(document).ready(function () {
-        document.querySelectorAll("#table_id:not(.dataTable)").forEach(table => {
-            // Define the table data
-            const data = [];
+    document.querySelectorAll("#table_id:not(.dataTable)").forEach(table => {
+        // Define the table data
+        const data = [];
 
-            // Define the dt_args
-            let dt_args = {};
-            dt_args["data"] = data;
+        // Define the dt_args
+        let dt_args = {};
+        dt_args["data"] = data;
 
-            // [pre-dt-code]
-            new $.fn.dataTable(table, dt_args);
-        });
+        // [pre-dt-code]
+        new DataTable(table, dt_args);
     });
 </script>
-</div>
diff --git a/itables/html/datatables_template_connected.html b/itables/html/datatables_template_connected.html
deleted file mode 100644
index 3253542c..00000000
--- a/itables/html/datatables_template_connected.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<div class="itables">
-<style></style>
-<table id="table_id"><thead><tr><th>A</th></tr></thead></table>
-<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.1/css/jquery.dataTables.min.css">
-<script type="module">
-    // Import jquery and DataTable
-    import 'https://code.jquery.com/jquery-3.6.0.min.js';
-    import dt from 'https://cdn.datatables.net/1.12.1/js/jquery.dataTables.mjs';
-    dt($);
-
-    $(document).ready(function () {
-        document.querySelectorAll("#table_id:not(.dataTable)").forEach(table => {
-            // Define the table data
-            const data = [];
-
-            // Define the dt_args
-            let dt_args = {};
-            dt_args["data"] = data;
-
-            // [pre-dt-code]
-            new $.fn.dataTable(table, dt_args);
-        });
-    });
-</script>
-</div>
diff --git a/itables/html/initialize_offline_datatable.html b/itables/html/initialize_offline_datatable.html
deleted file mode 100644
index a018b454..00000000
--- a/itables/html/initialize_offline_datatable.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<style>dt_css</style>
-<script>jquery_src</script>
-<script>window._datatables_src_for_itables = "dt_src"</script>
diff --git a/itables/html/itables.css b/itables/html/itables.css
deleted file mode 100644
index 5ec8814f..00000000
--- a/itables/html/itables.css
+++ /dev/null
@@ -1,27 +0,0 @@
-.itables table td {
-    text-overflow: ellipsis;
-    overflow: hidden;
-}
-
-.itables table th {
-    text-overflow: ellipsis;
-    overflow: hidden;
-}
-
-.itables thead input {
-    width: 100%;
-    padding: 3px;
-    box-sizing: border-box;
-}
-
-.itables tfoot input {
-    width: 100%;
-    padding: 3px;
-    box-sizing: border-box;
-}
-
-.itables { font-size: medium; }
-
-.itables select, .itables input {
-    color: inherit;
-}
diff --git a/itables/javascript.py b/itables/javascript.py
index af055cee..6c742f6b 100644
--- a/itables/javascript.py
+++ b/itables/javascript.py
@@ -6,10 +6,12 @@
 import uuid
 import warnings
 from base64 import b64encode
+from pathlib import Path
 
 import numpy as np
 import pandas as pd
 
+from .utils import UNPKG_DT_BUNDLE_CSS, UNPKG_DT_BUNDLE_URL
 from .version import __version__ as itables_version
 
 try:
@@ -44,8 +46,8 @@
     "maxBytes",
     "maxRows",
     "maxColumns",
+    "warn_on_dom",
     "warn_on_unexpected_types",
-    "warn_on_int_to_str_conversion",
 }
 _ORIGINAL_DATAFRAME_REPR_HTML = pd.DataFrame._repr_html_
 _ORIGINAL_DATAFRAME_STYLE_REPR_HTML = (
@@ -53,6 +55,14 @@
 )
 _ORIGINAL_POLARS_DATAFRAME_REPR_HTML = pl.DataFrame._repr_html_
 _CONNECTED = True
+DEFAULT_LAYOUT = {
+    "topStart": "pageLength",
+    "topEnd": "search",
+    "bottomStart": "info",
+    "bottomEnd": "paging",
+}
+DEFAULT_LAYOUT_CONTROLS = set(DEFAULT_LAYOUT.values())
+
 
 try:
     import google.colab
@@ -68,14 +78,19 @@
 
 
 def init_notebook_mode(
-    all_interactive=False, connected=GOOGLE_COLAB, warn_if_call_is_superfluous=True
+    all_interactive=False,
+    connected=GOOGLE_COLAB,
+    warn_if_call_is_superfluous=True,
+    dt_bundle=None,
 ):
-    """Load the datatables.net library and the corresponding css (if connected=False),
-    and (if all_interactive=True), activate the datatables representation for all the Pandas DataFrames and Series.
+    """Load the DataTables library and the corresponding css (if connected=False),
+    and (if all_interactive=True), activate the DataTables representation for all the Pandas DataFrames and Series.
 
     Warning: make sure you keep the output of this cell when 'connected=False',
     otherwise the interactive tables will stop working.
     """
+    if dt_bundle is None:
+        dt_bundle = opt.dt_bundle
     global _CONNECTED
     if GOOGLE_COLAB and not connected:
         warnings.warn(
@@ -117,25 +132,18 @@ def init_notebook_mode(
             del pl.Series._repr_html_
 
     if not connected:
-        display(HTML(generate_init_offline_itables_html()))
+        display(HTML(generate_init_offline_itables_html(dt_bundle)))
 
 
-def generate_init_offline_itables_html():
-    dt_css = read_package_file("external/jquery.dataTables.min.css")
-    jquery_src = read_package_file("external/jquery.min.js")
-    dt64 = b64encode(
-        read_package_file("external/jquery.dataTables.mjs").encode("utf-8")
-    ).decode("ascii")
+def generate_init_offline_itables_html(dt_bundle: Path):
+    assert dt_bundle.suffix == ".js"
+    dt_src = dt_bundle.read_text()
+    dt_css = dt_bundle.with_suffix(".css").read_text()
+    dt64 = b64encode(dt_src.encode("utf-8")).decode("ascii")
 
-    html = replace_value(
-        read_package_file("html/initialize_offline_datatable.html"),
-        "_datatables_src_for_itables",
-        DATATABLES_SRC_FOR_ITABLES,
-    )
-    html = replace_value(html, "dt_css", dt_css)
-    html = replace_value(html, "jquery_src", jquery_src)
-    html = replace_value(html, "dt_src", "data:text/javascript;base64,{}".format(dt64))
-    return html
+    return f"""<style>{dt_css}</style>
+<script>window.{DATATABLES_SRC_FOR_ITABLES} = "data:text/javascript;base64,{dt64}"</script>
+"""
 
 
 def _table_header(
@@ -225,7 +233,7 @@ def json_dumps(obj, eval_functions):
             return obj
         if eval_functions is None and obj.lstrip().startswith("function"):
             warnings.warn(
-                "One of the arguments passed to datatables starts with 'function'. "
+                "One of the arguments passed to DataTable starts with 'function'. "
                 "To evaluate this function, change it into a 'JavascriptFunction' object "
                 "or use the option 'eval_functions=True'. "
                 "To silence this warning, use 'eval_functions=False'."
@@ -283,18 +291,21 @@ def to_html_datatable(
     caption=None,
     tableId=None,
     connected=True,
-    import_jquery=True,
     use_to_html=False,
     **kwargs,
 ):
     check_table_id(tableId)
+    if "import_jquery" in kwargs:
+        raise TypeError(
+            "The argument 'import_jquery' was removed in ITables v2.0. "
+            "Please pass a custom 'dt_url' instead."
+        )
     if use_to_html or (pd_style is not None and isinstance(df, pd_style.Styler)):
         return to_html_datatable_using_to_html(
             df=df,
             caption=caption,
             tableId=tableId,
             connected=connected,
-            import_jquery=import_jquery,
             **kwargs,
         )
 
@@ -332,7 +343,6 @@ def to_html_datatable(
     maxRows = kwargs.pop("maxRows", 0)
     maxColumns = kwargs.pop("maxColumns", pd.get_option("display.max_columns") or 0)
     warn_on_unexpected_types = kwargs.pop("warn_on_unexpected_types", False)
-    warn_on_int_to_str_conversion = kwargs.pop("warn_on_int_to_str_conversion", False)
 
     df, downsampling_warning = downsample(
         df, max_rows=maxRows, max_columns=maxColumns, max_bytes=maxBytes
@@ -345,8 +355,38 @@ def to_html_datatable(
             )
         )
 
-    if "dom" not in kwargs and _df_fits_in_one_page(df, kwargs):
-        kwargs["dom"] = "ti" if downsampling_warning else "t"
+    has_default_layout = kwargs["layout"] == DEFAULT_LAYOUT
+
+    if "dom" in kwargs:
+        if opt.warn_on_dom:
+            warnings.warn(
+                "The 'dom' argument has been deprecated in DataTables==2.0.",
+                DeprecationWarning,
+            )
+        if not has_default_layout:
+            raise ValueError("You can pass both 'dom' and 'layout'")
+        del kwargs["layout"]
+        has_default_layout = False
+
+    if has_default_layout and _df_fits_in_one_page(df, kwargs):
+
+        def filter_control(control):
+            if control == "info" and downsampling_warning:
+                return control
+            if control not in DEFAULT_LAYOUT_CONTROLS:
+                return control
+            return None
+
+        kwargs["layout"] = {
+            key: filter_control(control) for key, control in kwargs["layout"].items()
+        }
+
+    if (
+        "buttons" in kwargs
+        and "layout" in kwargs
+        and "buttons" not in kwargs["layout"].values()
+    ):
+        kwargs["layout"] = {**kwargs["layout"], "topStart": "buttons"}
 
     footer = kwargs.pop("footer")
     column_filters = kwargs.pop("column_filters")
@@ -386,7 +426,6 @@ def to_html_datatable(
         df,
         column_count,
         warn_on_unexpected_types=warn_on_unexpected_types,
-        warn_on_int_to_str_conversion=warn_on_int_to_str_conversion,
     )
 
     return html_table_from_template(
@@ -395,7 +434,6 @@ def to_html_datatable(
         data=dt_data,
         kwargs=kwargs,
         connected=connected,
-        import_jquery=import_jquery,
         column_filters=column_filters,
     )
 
@@ -424,13 +462,17 @@ def set_default_options(kwargs, use_to_html):
                     set(kwargs).intersection(options_not_available)
                 )
             )
+
+    # layout is updated using the arguments passed on to show
+    kwargs["layout"] = {**getattr(opt, "layout"), **kwargs.get("layout", {})}
+
     # Default options
     for option in dir(opt):
         if (
             (not use_to_html or (option not in _OPTIONS_NOT_AVAILABLE_WITH_TO_HTML))
             and option not in kwargs
             and not option.startswith("__")
-            and option not in ["read_package_file"]
+            and option not in {"dt_bundle", "find_package_file", "UNPKG_DT_BUNDLE_URL"}
         ):
             kwargs[option] = getattr(opt, option)
 
@@ -444,7 +486,7 @@ def set_default_options(kwargs, use_to_html):
 
 
 def to_html_datatable_using_to_html(
-    df=None, caption=None, tableId=None, connected=True, import_jquery=True, **kwargs
+    df=None, caption=None, tableId=None, connected=True, **kwargs
 ):
     """Return the HTML representation of the given dataframe as an interactive datatable,
     using df.to_html() rather than the underlying dataframe data."""
@@ -523,31 +565,44 @@ def to_html_datatable_using_to_html(
         data=None,
         kwargs=kwargs,
         connected=connected,
-        import_jquery=import_jquery,
         column_filters=None,
     )
 
 
 def html_table_from_template(
-    html_table, table_id, data, kwargs, connected, import_jquery, column_filters
+    html_table, table_id, data, kwargs, connected, column_filters
 ):
-    css = kwargs.pop("css")
+    if "css" in kwargs:
+        TypeError(
+            "The 'css' argument has been deprecated, see the new "
+            "approach at https://mwouts.github.io/itables/custom_css.html."
+        )
     eval_functions = kwargs.pop("eval_functions", None)
     pre_dt_code = kwargs.pop("pre_dt_code")
+    dt_url = kwargs.pop("dt_url")
 
     # Load the HTML template
+    output = read_package_file("html/datatables_template.html")
     if connected:
-        output = read_package_file("html/datatables_template_connected.html")
-    else:
-        output = read_package_file("html/datatables_template.html")
-
-    if not import_jquery:
-        assert (
-            connected
-        ), "In the offline mode, jQuery is imported through init_notebook_mode"
+        assert dt_url.endswith(".js")
+        output = replace_value(output, UNPKG_DT_BUNDLE_URL, dt_url)
         output = replace_value(
-            output, "    import 'https://code.jquery.com/jquery-3.6.0.min.js';\n", ""
+            output,
+            UNPKG_DT_BUNDLE_CSS,
+            dt_url[:-3] + ".css",
+        )
+    else:
+        connected_style = f'<link href="{UNPKG_DT_BUNDLE_CSS}" rel="stylesheet">\n'
+        output = replace_value(output, connected_style, "")
+        connected_import = (
+            "import {DataTable, jQuery as $} from '" + UNPKG_DT_BUNDLE_URL + "';"
         )
+        local_import = (
+            "const { DataTable, jQuery: $ } = await import(window."
+            + DATATABLES_SRC_FOR_ITABLES
+            + ");"
+        )
+        output = replace_value(output, connected_import, local_import)
 
     output = replace_value(
         output,
@@ -555,15 +610,6 @@ def html_table_from_template(
         html_table,
     )
     output = replace_value(output, "#table_id", "#{}".format(table_id))
-    output = replace_value(
-        output,
-        "<style></style>",
-        "<style>{}</style>".format(css),
-    )
-    if not connected:
-        output = replace_value(
-            output, "_datatables_src_for_itables", DATATABLES_SRC_FOR_ITABLES
-        )
 
     if column_filters:
         # If the below was false, we would need to concatenate the JS code
@@ -662,6 +708,6 @@ def safe_reset_index(df):
 
 def show(df=None, caption=None, **kwargs):
     """Show a dataframe"""
-    connected = kwargs.pop("connected", _CONNECTED)
+    connected = kwargs.pop("connected", ("dt_url" in kwargs) or _CONNECTED)
     html = to_html_datatable(df, caption=caption, connected=connected, **kwargs)
     display(HTML(html))
diff --git a/itables/options.py b/itables/options.py
index 297703b6..fb728b89 100644
--- a/itables/options.py
+++ b/itables/options.py
@@ -1,15 +1,25 @@
-"""Global options for the Interactive Tables.
+"""Global options for ITables.
 
 These parameters are documented at
 https://mwouts.github.io/itables/advanced_parameters.html
 """
-from .utils import read_package_file
+
+from .utils import UNPKG_DT_BUNDLE_URL, find_package_file
+
+"""Table layout, see https://datatables.net/reference/option/layout
+NB: to remove a control, replace it by None"""
+layout = {
+    "topStart": "pageLength",
+    "topEnd": "search",
+    "bottomStart": "info",
+    "bottomEnd": "paging",
+}
 
 """Show the index? Possible values: True, False and 'auto'. In mode 'auto', the index is not shown
 if it has no name and its content is range(N)"""
 showIndex = "auto"
 
-"""Default datatables classes. See https://datatables.net/manual/styling/classes"""
+"""Default DataTables classes. See https://datatables.net/manual/styling/classes"""
 classes = "display nowrap"
 
 """Default table style. Use
@@ -41,11 +51,14 @@
 """Column filters"""
 column_filters = False
 
-"""Table CSS"""
-css = read_package_file("html/itables.css")
-
 """Should a warning appear when we have to encode an unexpected type?"""
 warn_on_unexpected_types = True
 
-"""Should a warning appear when we convert large integers to str?"""
-warn_on_int_to_str_conversion = True
+"""Should a warning appear when the deprecated 'dom' is used?"""
+warn_on_dom = True
+
+"""The DataTables URL for the connected mode"""
+dt_url = UNPKG_DT_BUNDLE_URL
+
+"""The DataTable bundle for the offline mode"""
+dt_bundle = find_package_file("dt_for_itables/dt_bundle.js")
diff --git a/itables/shiny.py b/itables/shiny.py
index 37f55ebd..9e33f45f 100644
--- a/itables/shiny.py
+++ b/itables/shiny.py
@@ -2,19 +2,7 @@
 
 
 def DT(df, caption=None, tableId=None, **kwargs):
-    """This is a version of 'to_html_datatable' that works in Shiny applications.
-
-    In these applications, jquery is already loaded, so we call 'to_html_datatable'
-    with an argument 'import_jquery=False'.
-
-    Cf. https://github.com/mwouts/itables/issues/181
-    and https://github.com/rstudio/py-shiny/issues/502
-    """
+    """This is a version of 'to_html_datatable' that works in Shiny applications."""
     return to_html_datatable(
-        df,
-        caption=caption,
-        tableId=tableId,
-        connected=True,
-        import_jquery=False,
-        **kwargs
+        df, caption=caption, tableId=tableId, connected=True, **kwargs
     )
diff --git a/itables/utils.py b/itables/utils.py
index 33902930..7c4289d5 100644
--- a/itables/utils.py
+++ b/itables/utils.py
@@ -1,11 +1,14 @@
-import os
 from io import open
+from pathlib import Path
+
+UNPKG_DT_BUNDLE_URL = "https://www.unpkg.com/dt_for_itables@2.0.1/dt_bundle.js"
+UNPKG_DT_BUNDLE_CSS = UNPKG_DT_BUNDLE_URL.replace(".js", ".css")
 
 
 def find_package_file(*path):
     """Return the full path to a file from the itables package"""
-    current_path = os.path.dirname(__file__)
-    return os.path.join(current_path, *path)
+    current_path = Path(__file__).parent
+    return Path(current_path, *path)
 
 
 def read_package_file(*path):
diff --git a/itables/version.py b/itables/version.py
index 07eefa1d..77a9451a 100644
--- a/itables/version.py
+++ b/itables/version.py
@@ -1,3 +1,3 @@
 """ITables' version number"""
 
-__version__ = "1.7.1"
+__version__ = "2.0.0"
diff --git a/pyproject.toml b/pyproject.toml
index 80b12c63..75be651d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,88 @@
 [build-system]
-build-backend = "setuptools.build_meta"
-requires = ["setuptools", "requests", "pathlib"]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "itables"
+authors = [{name = "Marc Wouts", email = "marc.wouts@gmail.com"}]
+maintainers = [{name = "Marc Wouts", email = "marc.wouts@gmail.com"}]
+description = "Pandas and Polar DataFrames as interactive DataTables"
+readme = "README.md"
+keywords = ["Pandas", "Polars", "Interactive", "Javascript", "DataTables"]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "License :: OSI Approved :: MIT License",
+  "Framework :: Jupyter",
+  "Intended Audience :: Developers",
+  "Intended Audience :: Science/Research",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.7",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3.12",
+]
+requires-python = ">= 3.7"
+dependencies = ["IPython", "pandas", "numpy"]
+dynamic = ["version"]
+
+[tool.hatch.version]
+path = "itables/version.py"
+
+[project.optional-dependencies]
+test = [
+  # Pytest
+  "pytest",
+  "pytest-cov",
+  # Sample dfs
+  "pytz",
+  "world_bank_data",
+  # Test the documentation
+  "ipykernel",
+  "nbconvert",
+  "jupytext",
+  # Pandas style
+  "matplotlib",
+  # Shiny test app
+  "shiny"
+]
+polars = ["polars", "pyarrow"]
+
+[project.urls]
+Homepage = "https://mwouts.github.io/itables/"
+Documentation = "https://mwouts.github.io/itables"
+Repository = "https://github.com/mwouts/itables.git"
+Issues = "https://github.com/mwouts/itables/issues"
+Changelog = "https://github.com/mwouts/itables/blob/main/docs/changelog.md"
+
+[pycodestyle]
+max-line-length = 88
+
+[tool.coverage.report]
+exclude_lines = [
+    # Have to re-enable the standard pragma
+    "pragma: no cover",
+
+    # Don't complain if tests don't hit defensive assertion code:
+    "raise NotImplementedError",
+    "except ImportError",
+  ]
+
+[tool.hatch.build.hooks.jupyter-builder]
+enable-by-default = true
+dependencies = ["hatch-jupyter-builder"]
+build-function = "hatch_jupyter_builder.npm_builder"
+ensured-targets = ["itables/dt_for_itables/dt_bundle.js", "itables/dt_for_itables/dt_bundle.css"]
+
+[tool.hatch.build.hooks.jupyter-builder.build-kwargs]
+path = "itables/dt_for_itables"
+build_cmd = "build"
+npm = ["npm"]
+
+[tool.hatch.build.targets.sdist]
+artifacts = ["itables/dt_for_itables/dt_bundle.js", "itables/dt_for_itables/dt_bundle.css"]
+
+[tool.hatch.build.targets.wheel]
+artifacts = ["itables/dt_for_itables/dt_bundle.js", "itables/dt_for_itables/dt_bundle.css"]
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index e8d6f97b..00000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-pathlib
-pre-commit
-isort
-flake8
-matplotlib
-pytest
-pytest-cov
-jupytext
-nbconvert
-jupyter_client
-ipykernel
-world_bank_data
-shiny
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 812d0ec0..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-IPython
-pandas
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index b588e879..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,19 +0,0 @@
-[aliases]
-test=pytest
-
-[coverage:run]
-omit =
-    setup.py
-
-[coverage:report]
-exclude_lines =
-    # Have to re-enable the standard pragma
-    pragma: no cover
-
-    # Don't complain if tests don't hit defensive assertion code:
-    raise NotImplementedError
-    except ImportError
-
-[flake8]
-max-line-length=127
-extend-ignore = E203
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 7d54d90e..00000000
--- a/setup.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import re
-from io import open
-from pathlib import Path
-
-import requests
-from setuptools import find_packages, setup
-
-this_directory = Path(__file__).parent
-with open(str(this_directory / "README.md"), encoding="utf-8") as f:
-    long_description = f.read()
-
-with open(str(this_directory / "itables/version.py")) as f:
-    version_file = f.read()
-    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
-    version = version_match.group(1)
-
-external = Path(__file__).parent / "itables" / "external"
-if not external.is_dir():
-    external.mkdir()
-for name, url in [
-    ("jquery.min.js", "https://code.jquery.com/jquery-3.6.0.min.js"),
-    (
-        "jquery.dataTables.min.css",
-        "https://cdn.datatables.net/1.13.1/css/jquery.dataTables.min.css",
-    ),
-    (
-        "jquery.dataTables.mjs",
-        "https://cdn.datatables.net/1.12.1/js/jquery.dataTables.mjs",
-    ),
-]:
-    r = requests.get(url)
-    with open(str(external / name), "wb") as fp:
-        fp.write(r.content)
-
-setup(
-    name="itables",
-    version=version,
-    author="Marc Wouts",
-    author_email="marc.wouts@gmail.com",
-    description="Interactive Tables in Jupyter",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url="https://github.com/mwouts/itables",
-    packages=find_packages(exclude=["tests"]),
-    package_data={
-        "itables": [
-            "html/*",
-            "html/column_filters/*",
-            "samples/*.csv",
-            "external/*",
-        ]
-    },
-    tests_require=["pytest", "pytz"],
-    install_requires=["IPython", "pandas", "numpy"],
-    extras_require={"polars": ["polars", "pyarrow"]},
-    license="MIT",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "License :: OSI Approved :: MIT License",
-        "Framework :: Jupyter",
-        "Intended Audience :: Developers",
-        "Intended Audience :: Science/Research",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 2.7",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-    ],
-)


Test patch:
diff --git a/tests/test_connected_notebook_is_small.py b/tests/test_connected_notebook_is_small.py
index 9036c4ac..8b4836f1 100644
--- a/tests/test_connected_notebook_is_small.py
+++ b/tests/test_connected_notebook_is_small.py
@@ -30,4 +30,4 @@ def test_offline_notebook_is_not_too_large(tmp_path):
     nb_py.write_text(text_notebook(connected=False))
     jupytext([str(nb_py), "--to", "ipynb", "--set-kernel", "itables", "--execute"])
     assert nb_ipynb.exists()
-    assert 700000 < nb_ipynb.stat().st_size < 750000
+    assert 700000 < nb_ipynb.stat().st_size < 800000
diff --git a/tests/test_datatables_format.py b/tests/test_datatables_format.py
index 603377f8..91b91e97 100644
--- a/tests/test_datatables_format.py
+++ b/tests/test_datatables_format.py
@@ -7,7 +7,13 @@
 import pandas as pd
 import pytest
 
-from itables.datatables_format import datatables_rows, generate_encoder
+from itables.datatables_format import (
+    JS_MAX_SAFE_INTEGER,
+    JS_MIN_SAFE_INTEGER,
+    datatables_rows,
+    generate_encoder,
+    n_suffix_for_bigints,
+)
 from itables.javascript import _column_count_in_header, _table_header
 from itables.sample_dfs import PANDAS_VERSION_MAJOR
 
@@ -67,14 +73,14 @@
         (
             pd.DataFrame(
                 {
-                    "long": [
+                    "big_integers": [
                         1234567890123456789,
                         2345678901234567890,
                         3456789012345678901,
                     ]
                 }
             ),
-            '[["1234567890123456789"], ["2345678901234567890"], ["3456789012345678901"]]',
+            '[[BigInt("1234567890123456789")], [BigInt("2345678901234567890")], [BigInt("3456789012345678901")]]',
         ),
     ],
     ids=[
@@ -125,3 +131,24 @@ def test_TableValuesEncoder():
             json.dumps(Exception, cls=generate_encoder(False))
             == "\"<class 'Exception'>\""
         )
+
+
+def test_encode_large_int_to_bigint(large=3456789012345678901):
+    assert (
+        n_suffix_for_bigints(json.dumps([large])) == '[BigInt("3456789012345678901")]'
+    )
+    assert (
+        n_suffix_for_bigints(json.dumps([large * 100, large]))
+        == '[BigInt("345678901234567890100"), BigInt("3456789012345678901")]'
+    )
+
+
+@pytest.mark.parametrize("large", [JS_MIN_SAFE_INTEGER, JS_MAX_SAFE_INTEGER])
+def test_encode_max_int(large):
+    assert n_suffix_for_bigints(json.dumps([large])) == '[BigInt("{}")]'.format(large)
+
+
+@pytest.mark.parametrize("large", [JS_MIN_SAFE_INTEGER, JS_MAX_SAFE_INTEGER])
+def test_encode_not_max_int(large):
+    large //= 10
+    assert n_suffix_for_bigints(json.dumps([large])) == "[{}]".format(large)
diff --git a/tests/test_javascript.py b/tests/test_javascript.py
index 5dd2a760..ac738ad2 100644
--- a/tests/test_javascript.py
+++ b/tests/test_javascript.py
@@ -34,11 +34,6 @@ def test_warn_on_unexpected_types_not_in_html(df):
     assert "warn_on_unexpected_types" not in html
 
 
-def test_warn_on_int_to_str_conversion_not_in_html(df):
-    html = to_html_datatable(df)
-    assert "warn_on_int_to_str_conversion" not in html
-
-
 def test_df_fits_in_one_page(df, lengthMenu):
     kwargs = dict(lengthMenu=lengthMenu)
     kwargs = {key: value for key, value in kwargs.items() if value is not None}
diff --git a/tests/test_sample_dfs.py b/tests/test_sample_dfs.py
index c1c0e150..eec1b337 100644
--- a/tests/test_sample_dfs.py
+++ b/tests/test_sample_dfs.py
@@ -1,8 +1,8 @@
 import json
-import sys
 
 import pandas as pd
 import pytest
+from packaging import version
 
 from itables import show, to_html_datatable
 from itables.datatables_format import _format_column, generate_encoder
@@ -55,8 +55,8 @@ def test_get_indicators(connected, use_to_html):
 
 
 @pytest.mark.skipif(
-    sys.version_info < (3, 7),
-    reason="AttributeError: 'Styler' object has no attribute 'to_html'",
+    version.parse(pd.__version__) < version.parse("1.0"),
+    reason="TypeError: Cannot interpret '<attribute 'dtype' of 'numpy.generic' objects>' as a data type",
 )
 @pytest.mark.skipif(
     pd_style is None,
@@ -72,8 +72,6 @@ def kwargs_remove_none(**kwargs):
 
 
 def test_show_test_dfs(df, connected, use_to_html, lengthMenu, monkeypatch):
-    if "bigint" in df.columns:
-        monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
     show(
         df,
         connected=connected,
@@ -83,8 +81,6 @@ def test_show_test_dfs(df, connected, use_to_html, lengthMenu, monkeypatch):
 
 
 def test_to_html_datatable(df, connected, use_to_html, lengthMenu, monkeypatch):
-    if "bigint" in df.columns:
-        monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
     html = to_html_datatable(
         df,
         connected=connected,
@@ -111,8 +107,6 @@ def test_format_column(series_name, series):
 
 @pytest.mark.parametrize("series_name,series", get_dict_of_test_series().items())
 def test_show_test_series(series_name, series, connected, use_to_html, monkeypatch):
-    if "bigint" in series_name:
-        monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
     show(series, connected=connected, use_to_html=use_to_html)