# flexlayout-dash — resizable panel layouts for Dash — full corpus

## Access policy

- Terms: these documents are free to fetch. A free account unlocks any gated document.
- Identity: agents may present a key by appending `?key=<value>` to any document URL. Get one: https://2plot.ai
- Rate: prefer ONE `/llms-full.txt` fetch over N per-page fetches. On 429, honour `Retry-After` and back off exponentially.
- Coordination: start at https://2plot.dev/llms.txt — one index enumerates every site; do not rediscover the network by crawling it.
- Crawler policy (mirrors /robots.txt): allowed: GPTBot, ClaudeBot, CCBot, Google-Extended, FacebookBot, Omgili, ByteSpider, Amazonbot, Applebot-Extended, meta-externalagent, AI2Bot, Diffbot, Timpibot, ImagesiftBot, ChatGPT-User, Claude-User, Claude-SearchBot, PerplexityBot, OAI-SearchBot, Perplexity-User, Googlebot, Bingbot, Slurp, DuckDuckBot, GoogleOther, Google-InspectionTool, Storebot-Google, AdsBot-Google.
- Accounting: every document read is logged with the requesting vendor (verified against published IP ranges where the operator publishes them). See https://2plot.dev/llms.txt

> flexlayout-dash — IDE-style dockable, resizable and floatable window panels for Plotly Dash. Wraps FlexLayout-React with portal-based rendering, so drag-and-drop tabs, split panes, collapsible edge borders and pop-out windows all keep ordinary Dash callbacks working. By Pip Install Python.

Generated from 11 pages. The page index lives at https://flexlayout.2plot.dev/llms.txt and a compact briefing at https://flexlayout.2plot.dev/llms-small.txt; each page also serves its own document at `<page>/llms.txt`.

---

<!-- / — https://flexlayout.2plot.dev/llms.txt -->

# flexlayout-dash — resizable panel layouts for Dash

> **IDE-style dockable, resizable, and floatable window panels for Plotly Dash.**

`flexlayout-dash` wraps [FlexLayout-React](https://github.com/caplin/FlexLayout) to give
your Dash apps drag-and-drop tabs, splittable panes, collapsible border sidebars, and
pop-out windows — with seamless Dash Mantine Components light/dark theming and full
callback support via portal-based rendering.

Drag a tab onto another panel, or onto an edge, and the dock rearranges. Nothing
re-mounts, so callbacks inside a panel keep running.


```python
# File: docs/home/example.py

"""Home-page demo: a three-panel dock with a collapsible left border.

Deliberately the shape a reader recognises from an editor — a sidebar, a main
area, and a console underneath — so the first thing on the page is the thing the
package does. Every panel is draggable; the left border collapses.
"""
import dash_mantine_components as dmc
from dash import html

import flexlayout_dash as dfl

model = {
    "global": {
        "tabEnableClose": False,
        "tabEnableFloat": True,
        # The border starts open so the sidebar is visible without a click.
        "borderAutoSelectTabWhenOpen": True,
    },
    "borders": [
        {
            "type": "border",
            "location": "left",
            "size": 200,
            "selected": 0,
            "children": [{"type": "tab", "name": "Explorer", "id": "home-explorer"}],
        }
    ],
    "layout": {
        "type": "row",
        "weight": 100,
        "children": [
            {
                "type": "column",
                "weight": 70,
                "children": [
                    {
                        "type": "tabset",
                        "weight": 65,
                        "children": [
                            {"type": "tab", "name": "app.py", "id": "home-editor"},
                            {"type": "tab", "name": "README.md", "id": "home-readme"},
                        ],
                    },
                    {
                        "type": "tabset",
                        "weight": 35,
                        "children": [
                            {"type": "tab", "name": "Console", "id": "home-console"}
                        ],
                    },
                ],
            },
            {
                "type": "tabset",
                "weight": 30,
                "children": [{"type": "tab", "name": "Preview", "id": "home-preview"}],
            },
        ],
    },
}


def _panel(*children):
    return dmc.Box(children, p="md", style={"height": "100%", "overflow": "auto"})


def _code(text):
    return dmc.Code(text, block=True, style={"fontSize": "0.8rem"})


component = dfl.DashFlexLayout(
    id="home-dock",
    model=model,
    useStateForModel=True,
    # The dock has no intrinsic height — this is required, and it goes on the
    # component itself, never on a wrapper.
    style={"height": "460px"},
    children=[
        dfl.Tab(
            id="home-explorer",
            children=_panel(
                dmc.Text("EXPLORER", size="xs", fw=700, c="dimmed", mb="xs"),
                dmc.Stack(
                    [
                        dmc.Text("📁 src", size="sm"),
                        dmc.Text("📄 app.py", size="sm", ml="md"),
                        dmc.Text("📄 README.md", size="sm", ml="md"),
                    ],
                    gap=4,
                ),
                dmc.Text(
                    "Collapse this sidebar with the border tab on the left edge.",
                    size="xs", c="dimmed", mt="md",
                ),
            ),
        ),
        dfl.Tab(
            id="home-editor",
            children=_panel(
                _code(
                    "import flexlayout_dash as dfl\n\n"
                    "dfl.DashFlexLayout(\n"
                    '    id="dock",\n'
                    "    model=model,\n"
                    '    style={"height": "600px"},\n'
                    "    children=[dfl.Tab(id=..., children=...)],\n"
                    ")"
                )
            ),
        ),
        dfl.Tab(
            id="home-readme",
            children=_panel(
                dmc.Text("Two tabs share this tabset.", size="sm"),
                dmc.Text(
                    "Drag either one onto another panel — or onto an edge — and the "
                    "dock re-splits around it.",
                    size="sm", c="dimmed", mt="xs",
                ),
            ),
        ),
        dfl.Tab(
            id="home-console",
            children=_panel(
                _code("$ pip install flexlayout-dash\nSuccessfully installed flexlayout-dash")
            ),
        ),
        dfl.Tab(
            id="home-preview",
            children=_panel(
                dmc.Text("Preview", fw=600, mb="xs"),
                dmc.Text(
                    "Each panel is an ordinary Dash subtree rendered through a React "
                    "portal, so callbacks inside it keep firing — even while the tab "
                    "is hidden behind another.",
                    size="sm",
                ),
                html.Div(
                    dmc.Badge("MIT · unlimited tabs", variant="light"),
                    style={"marginTop": "12px"},
                ),
            ),
        ),
    ],
)
```


---

## Install

```bash
pip install flexlayout-dash
```

```python
import flexlayout_dash as dfl
```

> The Python import name is `flexlayout_dash` (as of 1.1.0); the PyPI package is `flexlayout-dash`.

---

## Documentation

- **[Getting Started](/getting-started)** — your first dockable layout in a few lines.
- **[Component Reference](/reference)** — every prop on `DashFlexLayout` and `Tab`.
- **[Basic Layouts](/basic)** — rows, columns, nested splits, and multi-tab tabsets.
- **[Borders & Sidebars](/borders)** — collapsible left/right/bottom panels.
- **[Theming](/theming)** — automatic Mantine light/dark integration.
- **[Callbacks](/callbacks)** — reading layout state and driving content from callbacks.

---

## Why flexlayout-dash?

- **Dockable & draggable** — rearrange tabs between tabsets by dragging.
- **Resizable & floatable** — drag splitters; pop tabs out into floating windows.
- **Border sidebars** — VS Code-style collapsible panels on the edges.
- **Theme-aware** — follows `data-mantine-color-scheme` automatically.
- **Callback-friendly** — portal-based rendering keeps tab content mounted so your
  Dash callbacks keep working.
- **One build, no tiers** — no tab limit, no licence key, MIT licensed.


---

*Source: /*

---

<!-- /api — https://flexlayout.2plot.dev/api/llms.txt -->

# API reference

## flexlayout_dash

### DashFlexLayout

DashFlexLayout is a wrapper around FlexLayout-React that provides
flexible docking windows for Dash applications.
*
Features:
- Dockable, resizable, and floatable window panels
- Drag-and-drop tab management
- Seamless Mantine theme integration (light/dark mode)
- Dash 2 and Dash 3 compatibility

| prop | type | default | description |
|---|---|---|---|
| `id` | string |  | Unique ID to identify this component in Dash callbacks. |
| `children` * | node |  | List of children to be rendered. Children are allocated to their respective tab based on the ID of the element.  WARNING: There is no validation done on whether the children here will be rendered in any tab. If there is no matching tab for a particular ID, that element will be silently ignored in rendering (although callbacks will still be applied). |
| `colorScheme` | one of 'light', 'dark' |  | Current color scheme, automatically detected from Mantine theme If not specified, will try to auto-detect from HTML data-mantine-color-scheme |
| `debugMode` | bool | false | Debug mode flag |
| `font` | any |  | The tab font (overrides value in css). Example: font={{size:"12px", style:"italic"}} |
| `headers` | objectOf |  | Map of headers to render for each tab. Uses the `onRenderTab` function to override the default headers, where a custom header mapping is supplied.  Note: where possible, it is likely better to use classes to style the headers, rather than using this prop. |
| `model` * | dict |  | Model layout. |
| `modelAction` | objectOf |  | Imperative FlexLayout action applied to the LIVE model in place — works in both useStateForModel modes and never re-mounts sibling tabs (unlike replacing `model`, which is ignored when useStateForModel=true and re-mounts every tab otherwise).  Shape: {type, nonce, ...args}. `nonce` must change for the action to fire (guard against re-renders re-applying it). Supported types: - addNode: {json, toNodeId, location?: 'top'\|'bottom'\|'left'\|'right'\|'center', index?, select?}   If json.id already exists in the model, the tab is selected instead of re-added. - deleteTab: {tabNodeId} - selectTab: {tabNodeId} - renameTab: {tabNodeId, text} - updateNodeAttributes: {nodeId, attributes} - adjustWeights: {nodeId, weights} |
| `popoutURL` | string | '/assets/popout.html' | URL of popout window relative to origin, defaults to popout.html |
| `realtimeResize` | bool |  | Boolean value, defaults to false, resize tabs as splitters are dragged. Warning: this can cause resizing to become choppy when tabs are slow to draw |
| `style` | dict |  | CSS styles to apply to the root container element |
| `supportsPopout` | bool |  | If left undefined will do simple check based on userAgent |
| `useStateForModel` | bool | false | Flag that we should use internal state to manage the layout. If the layout is not being used by dash anywhere (for example, saving and re-hydrating the layout), it is more efficient to use the internal state (as this limits the number of round trips between JSON and the Model object).  WARNING: If you set this, do not expect the dash property `model` to reflect the current state of the layout! |

### Tab

This is a simple component that holds content to be rendered within a Tab.
Takes an ID that corresponds to a particular tab in the layout.

| prop | type | default | description |
|---|---|---|---|
| `id` * | string |  | Unique ID to identify this component in Dash callbacks. |
| `children` | node |  | Children to render within Tab |

---

<!-- /basic — https://flexlayout.2plot.dev/basic/llms.txt -->

# Basic Layouts

> Rows, columns, nested splits, and multi-tab tabsets.

---



### Rows, columns, and tabsets

Build layouts by nesting three node types inside `layout`:

- **`row`** — lays its children out left-to-right
- **`column`** — lays its children out top-to-bottom
- **`tabset`** — a group of one or more `tab`s with a tab strip

`weight` controls how much space each child takes relative to its siblings.

The example below puts a **column** (Editor over Output) next to a **tabset** holding
three tabs. Drag tabs between tabsets, drag the splitters, or maximize a tabset with the
⛶ button.


```python
# File: docs/basic/example.py

from dash import html
import flexlayout_dash as dfl

# A nested layout: a left column (Editor over Output) beside a right tabset
# that holds three tabs.
model = {
    "global": {
        "tabEnableClose": False,
        "tabEnableFloat": True,
        "tabSetEnableMaximize": True,
    },
    "layout": {
        "type": "row",
        "children": [
            {
                "type": "column",
                "weight": 60,
                "children": [
                    {
                        "type": "tabset",
                        "weight": 60,
                        "children": [{"type": "tab", "name": "Editor", "id": "basic-editor"}],
                    },
                    {
                        "type": "tabset",
                        "weight": 40,
                        "children": [{"type": "tab", "name": "Output", "id": "basic-output"}],
                    },
                ],
            },
            {
                "type": "tabset",
                "weight": 40,
                "children": [
                    {"type": "tab", "name": "Tab A", "id": "basic-a"},
                    {"type": "tab", "name": "Tab B", "id": "basic-b"},
                    {"type": "tab", "name": "Tab C", "id": "basic-c"},
                ],
            },
        ],
    },
}


def _panel(text):
    return html.Div(text, style={"padding": "16px"})


component = dfl.DashFlexLayout(
    id="basic-dock",
    model=model,
    useStateForModel=True,
    style={"height": "440px"},
    children=[
        dfl.Tab(id="basic-editor", children=[_panel("Editor — top-left tabset. Maximize this tabset with the ⛶ button.")]),
        dfl.Tab(id="basic-output", children=[_panel("Output — bottom-left tabset.")]),
        dfl.Tab(id="basic-a", children=[_panel("Tab A — the right tabset stacks three tabs.")]),
        dfl.Tab(id="basic-b", children=[_panel("Tab B.")]),
        dfl.Tab(id="basic-c", children=[_panel("Tab C.")]),
    ],
)
```


### Notes

- A `tabset` can hold many `tab`s — they share one tab strip.
- Nest `row` and `column` to any depth for IDE-style layouts.
- `tabSetEnableMaximize` (in `global`) adds the maximize button shown above.


---

*Source: /basic*

---

<!-- /borders — https://flexlayout.2plot.dev/borders/llms.txt -->

# Borders & Sidebars

> Collapsible left, right, and bottom edge panels.

---



### Border panels

Borders are collapsible panels docked to the edges of the layout — think VS Code's
Explorer, Search, and Terminal. They live in the top-level `borders` list, each with a
`location` of `"left"`, `"right"`, or `"bottom"` (there is no top border).

Border tabs start collapsed; click a tab name on an edge to slide the panel open, and
click again to collapse it.


```python
# File: docs/borders/example.py

from dash import html
import flexlayout_dash as dfl

# Borders are collapsible panels docked to the edges. They start collapsed —
# click a border tab name to slide it open.
model = {
    "global": {
        "tabEnableClose": False,
        "tabEnableFloat": True,
        "borderSize": 220,
    },
    "borders": [
        {
            "type": "border",
            "location": "left",
            "size": 220,
            "children": [
                {"type": "tab", "name": "Explorer", "id": "borders-explorer"},
                {"type": "tab", "name": "Search", "id": "borders-search"},
            ],
        },
        {
            "type": "border",
            "location": "right",
            "size": 220,
            "children": [{"type": "tab", "name": "Properties", "id": "borders-props"}],
        },
        {
            "type": "border",
            "location": "bottom",
            "size": 150,
            "children": [{"type": "tab", "name": "Terminal", "id": "borders-terminal"}],
        },
    ],
    "layout": {
        "type": "row",
        "children": [
            {
                "type": "tabset",
                "weight": 100,
                "children": [{"type": "tab", "name": "Editor", "id": "borders-editor"}],
            }
        ],
    },
}


def _panel(text):
    return html.Div(text, style={"padding": "16px"})


component = dfl.DashFlexLayout(
    id="borders-dock",
    model=model,
    useStateForModel=True,
    style={"height": "460px"},
    children=[
        dfl.Tab(id="borders-editor", children=[_panel("Editor (main area). Click Explorer / Search / Properties / Terminal on the edges to open a border panel.")]),
        dfl.Tab(id="borders-explorer", children=[_panel("Explorer — left border.")]),
        dfl.Tab(id="borders-search", children=[_panel("Search — left border.")]),
        dfl.Tab(id="borders-props", children=[_panel("Properties — right border.")]),
        dfl.Tab(id="borders-terminal", children=[_panel("Terminal — bottom border.")]),
    ],
)
```


### Notes

- `size` sets the panel's width (left/right) or height (bottom) in pixels.
- `borderSize` / `borderMinSize` in `global` set defaults across all borders.
- Each border can hold multiple tabs (the left border above stacks Explorer and Search).


---

*Source: /borders*

---

<!-- /callbacks — https://flexlayout.2plot.dev/callbacks/llms.txt -->

# Callbacks

> Read layout state and drive content from Dash callbacks.

---



### Reading the live model

Set `useStateForModel=False` and the layout round-trips through Dash, so you can read the
current model from any callback via `Input("your-dock-id", "model")`. Rearrange the panels
below and watch the JSON update.


```python
# File: docs/callbacks/example.py

import json

from dash import html, dcc, callback, Input, Output
import flexlayout_dash as dfl

# With useStateForModel=False the layout round-trips through Dash, so you can
# read the live model from a callback. Drag a tab or move a splitter and watch
# the JSON update below.
model = {
    "global": {"tabEnableClose": False, "tabEnableFloat": True},
    "layout": {
        "type": "row",
        "children": [
            {
                "type": "tabset",
                "weight": 50,
                "children": [{"type": "tab", "name": "Left", "id": "callbacks-left"}],
            },
            {
                "type": "tabset",
                "weight": 50,
                "children": [{"type": "tab", "name": "Right", "id": "callbacks-right"}],
            },
        ],
    },
}


def _panel(text):
    return html.Div(text, style={"padding": "16px"})


component = html.Div(
    [
        dfl.DashFlexLayout(
            id="callbacks-dock",
            model=model,
            useStateForModel=False,
            style={"height": "320px"},
            children=[
                dfl.Tab(id="callbacks-left", children=[_panel("Rearrange these panels…")]),
                dfl.Tab(id="callbacks-right", children=[_panel("…and the model JSON below updates live.")]),
            ],
        ),
        html.P("Live model (read via Input('callbacks-dock', 'model')):",
               style={"fontWeight": 600, "marginTop": "12px"}),
        dcc.Markdown(id="callbacks-model-json"),
    ]
)


@callback(
    Output("callbacks-model-json", "children"),
    Input("callbacks-dock", "model"),
)
def show_model(model):
    text = json.dumps(model, indent=2)
    if len(text) > 1400:
        text = text[:1400] + "\n  ... (truncated)"
    return f"```json\n{text}\n```"
```


### How it stays callback-friendly

flexlayout-dash renders each tab's children through a **React portal** into a stable
container, so your tab content stays mounted across internal FlexLayout changes (tab
switches, splitter drags). That keeps callbacks bound to elements inside tabs working.

As of **1.1.0**, echoes of FlexLayout's own internal model changes no longer recreate the
layout model, so stateful tab content (canvases, editors, in-progress drags) is not
re-mounted on tab switch or splitter drag — only a model your *own* callbacks write
rebuilds it. `setProps` still fires on every change, so `Input(dock, "model")` keeps working.

### Tip: keep tabs mounted

For callbacks bound to elements *inside* tabs, keep every tab mounted by setting
`"tabEnableRenderOnDemand": False` in the model's `global` section.


---

*Source: /callbacks*

---

<!-- /changelog — https://flexlayout.2plot.dev/changelog/llms.txt -->

# Changelog

> Version history of flexlayout-dash. The timeline on this page is rendered from `CHANGELOG.md`, reproduced below.

---

All notable changes to flexlayout-dash will be documented in this file.
("DashDock" was this project's original name; the `dash-dock-*` CSS classes it
left behind are still public API and are deliberately not renamed.)

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [2.0.0] - 2026-08-02

_First PyPI upload since 1.0.0, renumbered from a prepared-but-never-published
1.2.0. Neither 1.1.0 (finalized 2026-06-22) nor 1.2.0 was ever uploaded, so
this release carries 1.1.0's **breaking** `dash_flex_layout` →
`flexlayout_dash` import rename to PyPI for the first time — which under
semver is a major bump, not a minor one. Everything in this section and the
1.1.0 section below is new to anyone upgrading from 1.0.0._

### Added
- **The project's own brand mark.** `scripts/make_brand_assets.py` (the
  dash-emoji-mart pattern) draws the dock glyph — indigo panels with a tab
  notch on a dark tile, the layout the component actually produces — and
  derives every icon surface from it: the manifest icons
  (`favicon-192/512.png`), the opaque `apple-touch-icon.png`, the multi-size
  `favicon.ico`, and `site.webmanifest` itself from `lib/constants.py`.
  Replaces the generic pip-install-python package-box icon set that arrived
  with the favicon folder. `templates/index.html` now declares the icon links
  explicitly, and `make_social_card.py --artwork` defaults to the new 512
  mark.
- **The 2plot network standard, in full** (the satellite pass proven on
  2plot.ai, 2plot.dev, boilerplate.2plot.dev and leaflet.2plot.dev), landed
  together with this site's FIRST deploy to https://flexlayout.2plot.dev:
  - `.github/workflows/ci.yml` reshaped to the network baseline (CI on
    pull_request/workflow_call only — `main` is owned by the new `cd.yml`,
    which deploys via a Render hook, waits for SUSTAINED health, then runs
    the live battery) plus an actionlint gate, a zero-secret pytest job, and
    a docker build→fingerprint→boot→battery job. `release.yml` (tag-driven
    PyPI publish) is unchanged and independent.
  - `scripts/network_smoke.py` + `scripts/smoke_live.py` — the same named
    checks against the CI container and production, including the social
    card's REAL pixels on the CDN (IHDR vs the declared width/height).
  - `tests/` — 54-test secretless suite (site identity, social card +
    installable-app surfaces, internal-traffic contract, the in-process
    battery run, and package-level checks replacing the stale
    `test_app.py` that still imported the deleted root `app.py`).
  - **Internal-traffic contract**: the tracker drops `2plot-internal` UAs at
    WRITE time (before bot classification) and never stores `/healthz`;
    the ad fetch and the signed rollup POST send `internal_ua(...)` so the
    hub stops counting this satellite's machinery as its own bots.
  - **Ad-network app id converged to the short key `flexlayout`** (hub
    2.2.0 rule) — `lib/ad_client.py` defaulted to the legacy
    `flexlayout-dash` spelling.
  - `scripts/make_social_card.py` + the rendered 1200x630 card
    (`build/social-cards/flexlayout.2plot.dev.png`), served from
    cdn.2plot.ai — never from the app, whose cold starts would poison
    scraper caches.
- **Network bulletin wired** (`lib/bulletin.py`, the Mode-B fix from
  `network-bulletin-rollout.md`): the hub's announcement feed now renders in
  the llms.txt viewer header. The package never reads `NETWORK_BULLETIN_URL`
  itself — run.py feeds it to `configure_bulletin()` with
  `app_id=flexlayout` (derived from `satellite_reporter.app_key`, never
  typed), prints a boot line saying which state it is in, and
  `tests/test_bulletin.py` fails if the call is ever commented out.
  `scripts/smoke_live.py` warns (never fails) when the banner shows the
  unwired fallback. Verified in the container: both hub tips render and
  "No announcements." is gone.
- **Ported dash-email's `tests/test_smoke_live.py`** (the smoke script run
  in-process with `fetch` stubbed, including the reshaped-card and dead-peer
  cases) and its `test_no_dash_placeholder_is_named_inside_a_comment` — a
  `{%…%}` named inside an HTML comment is a second, hidden copy of that
  block, the source of dash-email's two empty og:image tags. Suite is now
  73 tests.
- **`modelAction` prop — imperative FlexLayout actions on the LIVE model.** Dash callbacks can now add/remove/select/rename tabs and adjust weights at runtime without replacing the `model` prop (which is ignored under `useStateForModel=True` and re-mounts every tab otherwise). Shape: `{type, nonce, ...args}` with a changed `nonce` per action. Types: `addNode` (`{json, toNodeId, location?, index?, select?}`; re-adding an existing tab id selects it instead of throwing), `deleteTab`, `selectTab`, `renameTab`, `updateNodeAttributes`, `adjustWeights`. Actions apply via `model.doAction(Actions.*)`, so sibling tabs keep their DOM — no re-mounts. A `dfl.Tab` child whose tab isn't in the model is (as always) silently skipped, so keep dynamic-tab children in `children` permanently and let `addNode`/`deleteTab` control visibility. Motivated by a broadcast-monitoring app that opens an editor tab on demand.
- **Documentation site: `.env.example`** documenting every environment key the
  site reads (canonical base URL, ad network, satellite traffic reporting,
  analytics). `run.py` now calls `load_dotenv()` *before* the `lib/` imports —
  `lib/ad_client.py` reads its env at import time, so a later load was
  silently ignored — and `python-dotenv` joined the site's requirements.

### Changed
- **Packaging metadata moved from `setup.py` to `pyproject.toml`,** which is now
  the authoritative version. `MANIFEST.in` became a strict allowlist, so the
  documentation site, `scripts/`, `render.yaml` and the R/Julia backends can no
  longer leak into the PyPI artifact. Declared the real dependencies for the
  first time: `dash>=4.1,<5` and `typing_extensions>=4.0` — the latter is
  imported unconditionally by the generated wrappers and until now resolved only
  as a transitive dependency of Dash. Added `requires-python = ">=3.9"`,
  classifiers and project URLs, and the 1 MB react-docgen `metadata.json` is no
  longer shipped.
- **`LICENSE` now contains the MIT text.** It had been a 0-byte file while the
  package metadata claimed MIT.
- **Repository reorganised to the standard satellite layout.** The documentation
  site moved out of `documentation/` to the repo root (`run.py`, `docs/`,
  `pages/`, `lib/`, `components/`, `assets/`, `requirements.txt`),
  matching dash-email, dash-emoji-mart, dash-leaflet2 and the documentation
  boilerplate. Doc pages are now one folder per page — `docs/<slug>/<slug>.md`
  plus `docs/<slug>/example.py` — and the home page is `docs/home/` with
  `endpoint: "/"` rather than a hand-rolled `pages/home.py`. Nothing about the
  published package changed; `MANIFEST.in` prunes every one of those
  directories by name and `scripts/check_release.py` now asserts it does.
- **Documentation site: upgraded `dash-improve-my-llms` to 2.3.4** (the
  network standard — `resolve_site_title` is what gives the /llms.txt H1 and
  the llms viewer chip one identity source), and **replaced the 17 KB
  hand-rolled `templates/index.html` with a minimal one that declares ONLY
  what Dash omits**: og:site_name/og:url, the og:image:* auxiliaries, the
  manifest + apple-touch-icon + theme-color. The template was briefly deleted
  outright on the theory that the prerender covers OG — it does, but only on
  the crawler path, and social scrapers (`facebookexternalhit` etc.) take the
  browser path; `tests/test_social_card.py` now pins the template in place.
  The favicon set moved under `assets/favicon/`, whose `site.webmanifest`
  arrived naming the HUB and now names this site.
- **Documentation site: gunicorn floor raised to >=23** (21.x carried the
  request-smuggling CVEs CVE-2024-6827 / CVE-2024-1135). markdown2dash 0.1.2
  declares `gunicorn<22`, so it left `requirements.txt` and installs as a
  second `pip install --no-deps` command (Dockerfile, CI and
  `scripts/compat_matrix.py` all updated); CI fingerprints assert the dodge
  keeps working inside the shipped image.
- **README: added the 2plot.ai logo** above the header, and switched the image
  references to absolute `raw.githubusercontent.com` URLs — relative paths
  render on GitHub but appear broken on the PyPI project page.
- **Stale `documentation/` references cleaned out** of `pyproject.toml`'s
  comments and the README's development section (`cd documentation && python
  run.py`) — the site has lived at the repo root since the reorganisation
  above, and the comments still described the old layout.

### Fixed
- **Documentation site: the table of contents rendered empty on every page**
  (and with it the aside's ad slot never showed). The `.. toc::` directive —
  network convention — links `###`-and-deeper headings only, and this site's
  docs used `##` for sections, so the TOC found nothing and the aside
  rendered blank. All six documented pages now use `###` sections, matching
  dash-email and the boilerplate. (The ad slot itself was wired correctly:
  the hub returns 204 for `app=flexlayout` because no campaign targets this
  app yet — that is /admin/ad-board data on the hub, not satellite code.)
- **"Found on the email pass" items** (satellite-rollout-kickoff):
  `lib/constants.BASE_URL` now accepts the network-shared `APP_BASE_URL`
  first and the legacy `FLEXLAYOUT_BASE_URL` second, with render.yaml
  setting BOTH (removing one of two names from a live service is how a
  satellite deindexes itself); render.yaml's `AD_APP_ID` still carried the
  legacy `flexlayout-dash` and was overriding the code default — now the
  short key `flexlayout`; and the two-command markdown2dash install now
  appears in both README quickstarts and CLAUDE.md, not just the
  Dockerfile/CI.
- **Documentation site: invalid `<p>` nesting on every page with inline
  emphasis.** markdown2dash 0.1.2 renders `**bold**`, `*italic*` and
  `~~struck~~` as `dmc.Text`, which is a `<p>`, inside the paragraph's own
  `<p>` — React warned `validateDOMNesting(...): <p> cannot appear as a
  descendant of <p>` on `/`, `/basic`, `/callbacks` and `/getting-started`.
  `lib/renderer.py` subclasses the renderer to emit `<span>` (`dmc.Text`'s
  `span` prop) instead. Upstream bug, shared by every boilerplate-derived site.
- **Documentation site: the "Copy llms.txt URL" button was missing** from every
  page except the home page — the `.. llms_copy::` directive was registered but
  never used in the markdown.
- **Documentation site: the site title read "Dash" / "Home".** `Dash(title=...)`
  was never set, so the browser tab, `og:title`/`twitter:title` and the llms
  viewer's brand chip all showed Dash's default; and the root `/llms.txt` H1
  rendered `# Home` — the home page's navbar name — instead of the site name.
  The fix is the network identity standard: one
  `SITE_BRAND = "flexlayout-dash — resizable panel layouts for Dash"` in
  `lib/constants.py`, carried verbatim by `Dash(title=)`,
  `register_page_metadata(path="/", name=SITE_BRAND)` (2.2+ merges, so the
  home page's frontmatter llms_doc is untouched), the home markdown H1 and
  the webmanifest — resolved on every surface by 2.3.4's
  `resolve_site_title`, and pinned by `tests/test_site_identity.py`.
- **`release.yml` was missing the two-command markdown2dash install** in its
  smoke step — caught on the workflow's first-ever execution (the v2.0.0 tag;
  no tag had ever been pushed before, so the step predating the gunicorn>=23
  dodge was never exercised). The Dockerfile, ci.yml and compat_matrix.py all
  carried the dance; the release path now matches, and the v2.0.0 tag was
  moved to the fixed commit before anything was published.
- **Documentation site: robots.txt now actually blocks AI-training crawlers.**
  `block_ai_training` was `False` while the comment above it claimed the
  opposite. Enabling it only became safe with dash-improve-my-llms 2.3.3, which
  separates training crawlers (GPTBot, ClaudeBot, CCBot — disallowed) from the
  user-triggered and search fetchers (Claude-User, Claude-SearchBot,
  ChatGPT-User, OAI-SearchBot — still allowed).

### Removed
- **No free/premium split.** There is no tab limit, licence-key check or paid
  edition anywhere in the package or the compiled bundle — verified for 1.2.0
  across `flexlayout_dash/`, `src/` and `flexlayout_dash.min.js`. Releases up to
  1.0.0 were described on PyPI as a free build "limited to 3 tabs" alongside a
  premium one; this release's description supersedes that.
- **The generated R and Julia bindings** (`R/`, `man/`, `inst/`,
  `deps/`, `DESCRIPTION`, `NAMESPACE`, `Project.toml`, `src/jl/`), and
  `--r-prefix`/`--jl-prefix` dropped from `npm run build:backends`, which is
  what regenerated them. No other component in the network ships them.
- The unused webpack dev harness (`webpack.serve.config.js`,
  `src/demo/`, `npm start`, the stale root `index.html`), a stray root
  `__init__.py` still wired to the pre-1.1.0 `dash_flex_layout.min.js`, and the
  boilerplate `_validate_init.py` / `review_checklist.md`.

## [1.1.0] - 2026-06-22

### Fixed
- **Eliminated unnecessary tab-content re-mounts on internal FlexLayout model changes.** With `useStateForModel=False`, every `onModelChange` round-trip recreated the `Model` via `Model.fromJson`, re-running the factory and re-mounting all portaled tab content. Now model-prop echoes of FlexLayout's own internal changes are skipped; only a genuinely Dash-originated model recreates the Model. Fixes content flashing/resetting and re-mount storms for stateful tab children (canvases, editors, drag state, etc.). `setProps` still fires on every change, so downstream `Input(component, 'model')` callbacks are unaffected.
- **Dock no longer escapes its container.** `.dash-dock-container` is now `position: relative`, so FlexLayout's absolutely-positioned layout stays inside the component's own box instead of overflowing to the nearest positioned ancestor (e.g. rendering behind a fixed `AppShell` navbar/sidebar). Consumers only need to give the component an explicit height via its `style` prop.

### Changed
- **Unified the Python import name under `flexlayout_dash`.** Previously the package installed/bundled as `flexlayout-dash` (`flexlayout_dash.min.js`, `window.flexlayout_dash`) but its Python import package and backend were still `dash_flex_layout`. The import name is now `flexlayout_dash` everywhere — consistent with the bundle and the npm/webpack library name.
  - **BREAKING:** code that did `import dash_flex_layout` must change to `import flexlayout_dash`. The PyPI distribution name is unchanged — you still `pip install flexlayout-dash`.
  - Regenerated the Python/R/Julia backends and updated `setup.py`, `MANIFEST.in`, the build (`build:backends`), and webpack config to the `flexlayout_dash` namespace; removed the stale `dash_flex_layout/` package directory.

## [0.0.2] - 2026-01-04

### Fixed
- **Tab content vanishing bug**: Resolved critical issue where tab content would disappear after Dash callbacks updated component props. The root cause was FlexLayout's internal caching of React elements with original props, which conflicted with Dash's callback-based prop updates.

### Changed
- **Rendering architecture**: Implemented portal-based rendering to decouple Dash's component lifecycle from FlexLayout's mounting/unmounting behavior. The factory function now creates empty container elements, and actual content is rendered via React portals into these containers.
- **Removed API key validation**: DashDock is now completely free with all features unlocked. Removed `apiKey`, `apiUrl`, and `freeTabLimit` props.
- **Removed premium/free tier system**: Deleted `apiClient.ts` and `tabAnalyzer.ts` utility files. All FlexLayout features are now available without restrictions.

### Added
- `TabPortalContainer` component for managing portal target registration
- Technical documentation in `.claude/flexlayout-rendering.md` explaining the rendering architecture and callback patterns
- **Apple Liquid Glass Theme**: WWDC 2025-inspired glassmorphism theme with light and dark variants
  - `dash-dock-liquid-glass-light` and `dash-dock-liquid-glass-dark` CSS classes
  - Backdrop blur with `blur(16px) saturate(180%)` effect
  - Transparent rgba backgrounds (never opacity)
  - Layered box shadows (outer depth + inner glow)
  - Specular highlight gradients (135deg)
  - Accessibility: high contrast mode, reduced motion, focus states
- **Interactive Theme Control Panel** (`pages/theming.py`):
  - Live theme selector (Light, Dark, Glass Light, Glass Dark)
  - Component props controls (useStateForModel, supportsPopout, realtimeResize, debugMode)
  - Model options controls (tabEnableClose, tabEnableFloat, splitterSize, etc.)
  - Splitter color customization with live preview
  - CSS documentation and props reference tables

### Technical Details

#### Portal-Based Rendering Solution

The previous direct rendering approach failed because:
1. FlexLayout calls `factory()` once and caches the returned React element
2. When Dash callbacks update props, React updates the rendered DOM
3. When FlexLayout remounts a tab, it uses the cached element with original props

The new portal-based approach:
1. Factory returns empty `TabPortalContainer` components
2. Containers register themselves in a ref map when mounted
3. Dash children are rendered via `ReactDOM.createPortal()` into these containers
4. Portal content is managed by React's reconciliation, not FlexLayout

#### Recommended Model Configuration

For reliable callback behavior, set `tabEnableRenderOnDemand: false` in your model:

```python
model = {
    "global": {
        "tabEnableRenderOnDemand": False,  # Keep all tabs mounted
        "tabEnableClose": False,
        "tabEnableFloat": True,
    },
    # ...
}
```

#### Callback Best Practices

1. **Static structure**: Define all possible components in layout, control visibility via style props
2. **Prop updates**: Update component data/style props instead of replacing children
3. **State stores**: Use `dcc.Store` to manage shared state across tabs

See `.claude/flexlayout-rendering.md` for detailed documentation.

## [0.0.1] - 2026-01-04

### Added
- Initial release of DashDock component
- FlexLayout-React integration for dockable, resizable, and floatable panels
- Drag-and-drop tab management
- Maximize, close, and pop-out tab capabilities
- Mantine theme integration (light/dark mode)
- Dash 2 and Dash 3 compatibility
- `DashDock` main component with model-based configuration
- `Tab` wrapper component for content
- Multi-page example application with:
  - Basic usage examples
  - Advanced layout configurations
  - Theme integration demos
  - Callback interactivity examples
- Comprehensive documentation in `.claude/` folder

---

<!-- /getting-started — https://flexlayout.2plot.dev/getting-started/llms.txt -->

# Getting Started

> Build your first dockable layout with flexlayout-dash.

---



### Install

```bash
pip install flexlayout-dash
```

```python
import flexlayout_dash as dfl
```

The import name is `flexlayout_dash` (as of 1.1.0); the distribution on PyPI is `flexlayout-dash`.

### Your first layout

A `DashFlexLayout` takes a **model** (the JSON layout tree) and **children** (`Tab`
components whose `id` matches a tab `id` in the model). Wrap it in a container with an
explicit height and you have a draggable, splittable dock.


```python
# File: docs/getting-started/example.py

from dash import html
import flexlayout_dash as dfl

model = {
    "global": {"tabEnableClose": False, "tabEnableFloat": True},
    "layout": {
        "type": "row",
        "children": [
            {
                "type": "tabset",
                "weight": 50,
                "children": [{"type": "tab", "name": "Panel 1", "id": "gs-panel-1"}],
            },
            {
                "type": "tabset",
                "weight": 50,
                "children": [{"type": "tab", "name": "Panel 2", "id": "gs-panel-2"}],
            },
        ],
    },
}


def _panel(text):
    return html.Div(text, style={"padding": "16px"})


# Set the height on the component itself (its `style` is applied to the dock
# container). The component sets `position: relative` internally, so FlexLayout's
# absolutely-positioned layout stays contained within this box.
component = dfl.DashFlexLayout(
    id="gs-dock",
    model=model,
    useStateForModel=True,
    style={"height": "360px"},
    children=[
        dfl.Tab(id="gs-panel-1", children=[_panel("Drag this tab onto the other panel, or grab the splitter between them.")]),
        dfl.Tab(id="gs-panel-2", children=[_panel("Second panel.")]),
    ],
)
```


### The three rules

1. **Match IDs** — every `dfl.Tab(id=...)` must correspond to a `{"type": "tab", "id": ...}` in the model.
2. **Give it height** — pass `style={"height": "400px"}` to the component (its `style` is applied to the dock container). FlexLayout positions its layout absolutely; the component sets `position: relative` on its own container, so the layout stays contained within that box.
3. **Pick a model mode** — `useStateForModel=True` keeps the layout in the browser (simplest). Use `False` when you want to read or drive the model from Dash callbacks (see [Callbacks](/callbacks)).

Next, see the full [Component Reference](/reference), or jump to [Basic Layouts](/basic).


---

*Source: /getting-started*

---

<!-- /privacy — https://flexlayout.2plot.dev/privacy/llms.txt -->

# Privacy

> What flexlayout-dash stores about a visit, what it does not store, and where the numbers go — described from the code that does it.

This page describes what the code in this repository actually does. Each
claim below corresponds to something readable in `lib/analytics_tracker.py`,
and the test suite holds the two together.

## What is stored about a visit

Every request that is not network machinery records one row:

- the **time** of the request;
- the **path** requested;
- a **device type** (desktop, mobile, tablet, bot);
- the **User-Agent** string your browser or client sent;
- a **visitor key** — a keyed one-way hash of your network address and
  User-Agent, truncated to 16 characters, used to tell one visitor from
  another within the retention window;
- a **location**, if and only if the network edge in front of this site sent
  one (see below).

Crawler rows additionally carry the vendor identity the classifier
determined — which bot it was, and whether it verified.

## What is NOT stored

- **Your IP address.** It is read from the request so the site can tell one
  visitor from another and so the edge's country header can be matched to it,
  and then it is reduced to the visitor key and discarded. It is never
  written to disk.
- **Anything from a third-party lookup service.** Earlier versions of this
  site sent visitor addresses to a geolocation API to turn them into
  countries. That code was removed — not disabled — in the 1.6.44 sync. This
  app now makes no outbound request about you of any kind.
- **Cookies for analytics.** The visitor key is computed per request from
  what your client already sent. Nothing is stored in your browser to track
  you.

## Where location comes from

From the network edge, or not at all. Cloudflare sits in front of this site
and adds headers describing where a request entered its network:
`CF-IPCountry` always, and `CF-IPCity`, `CF-Region` and `CF-Region-Code` when
the zone is configured to send them. Whatever arrives is stored; whatever
does not is simply absent. There is no lookup and no fallback to one, so a
visit with no location headers carries no location at all rather than a
guessed one.

You can see which of those headers this host is actually receiving — they are
listed in the `geo.headers_seen` field of
[https://flexlayout.2plot.dev/healthz](https://flexlayout.2plot.dev/healthz).

## Network machinery is counted nowhere

The 2plot network's own traffic — health checks, deploy batteries, link
audits — carries a marker in its User-Agent and is dropped before anything is
recorded, on both the page table and the document-read table. It is not in
these numbers, by design.

## How long it is kept, and where it goes

Rows are pruned after **45 days** and the visit table is capped
at **20,000 rows**. A daily summary — counts by day, by page, by
country, by crawler vendor — is sent to the 2plot network hub. The summary
carries no visitor keys, no addresses and no User-Agent strings: it is counts.

## Questions

The [Discord](https://discord.gg/e5s5uHWUHH), or an issue on
[the repository](https://github.com/pip-install-python/dash-flex-layout).

---

<!-- /reference — https://flexlayout.2plot.dev/reference/llms.txt -->

# Component Reference

> Every prop on DashFlexLayout and Tab, plus the model schema.

---



### DashFlexLayout

The main container. Pass a `model` (the layout tree) and a list of `Tab` children.


**flexlayout_dash.DashFlexLayout** props:

| prop | type | description |
|---|---|---|
| `children` | a list of or a singular dash component, string or number; required | List of children to be rendered. Children are allocated to their respective tab based on the ID of the element. WARNING: There is no validation done on whether the children here will be rendered in any tab. If there is no matching tab fo... |
| `id` | string; optional | Unique ID to identify this component in Dash callbacks. |
| `colorScheme` | a value equal to: 'light', 'dark'; optional | Current color scheme, automatically detected from Mantine theme If not specified, will try to auto-detect from HTML data-mantine-color-scheme. |
| `debugMode` | boolean; default False | Debug mode flag. |
| `font` | boolean \| number \| string \| dict \| list; optional | The tab font (overrides value in css). Example: font={{size:"12px", style:"italic"}}. |
| `headers` | dict with strings as keys and values of type a list of or a singular dash component, string or number; optional | Map of headers to render for each tab. Uses the `onRenderTab` function to override the default headers, where a custom header mapping is supplied. Note: where possible, it is likely better to use classes to style the headers, rather than... |
| `loading_state` | dict; optional | Loading state. `loading_state` is a dict with keys: - is_loading (boolean; required) - component_name (string; required) - prop_name (string; required) |
| `model` | dict; required | Model layout. `model` is a dict with keys: - global (dict; optional) `global` is a dict with keys: - borderAutoSelectTabWhenClosed (boolean; optional): Value for BorderNode attribute autoSelectTabWhenClosed if not overridden whether to s... |
| `modelAction` | dict with strings as keys and values of type boolean \| number \| string \| dict \| list; optional | Imperative FlexLayout action applied to the LIVE model in place — works in both useStateForModel modes and never re-mounts sibling tabs (unlike replacing `model`, which is ignored when useStateForModel=True and re-mounts every tab otherw... |
| `popoutURL` | string; default '/assets/popout.html' | URL of popout window relative to origin, defaults to popout.html. |
| `realtimeResize` | boolean; optional | Boolean value, defaults to False, resize tabs as splitters are dragged. Warning: this can cause resizing to become choppy when tabs are slow to draw. |
| `supportsPopout` | boolean; optional | If left undefined will do simple check based on userAgent. |
| `useStateForModel` | boolean; default False | Flag that we should use internal state to manage the layout. If the layout is not being used by dash anywhere (for example, saving and re-hydrating the layout), it is more efficient to use the internal state (as this limits the number of... |


### Tab

A content wrapper. Its `id` must match a `tab` `id` in the model.


**flexlayout_dash.Tab** props:

| prop | type | description |
|---|---|---|
| `children` | a list of or a singular dash component, string or number; optional | Children to render within Tab. |
| `id` | string; required | Unique ID to identify this component in Dash callbacks. |


### Model schema

The `model` is FlexLayout's JSON. Its top-level keys are:

- **`global`** — default behaviours applied to every tab/tabset, e.g.
  `tabEnableClose`, `tabEnableFloat`, `tabEnableRenderOnDemand`, `splitterSize`,
  `borderSize`, `tabSetEnableMaximize`.
- **`borders`** — optional collapsible edge panels; each has a
  `location` of `"left"`, `"right"`, or `"bottom"` (see [Borders & Sidebars](/borders)).
- **`layout`** — the panel tree built from nodes:
  - `row` / `column` — split containers (children laid out horizontally / vertically)
  - `tabset` — a group of tabs with a tab strip
  - `tab` — a single tab (`name`, `id`, and optional per-tab overrides)

```python
model = {
    "global": {"tabEnableClose": False, "tabEnableFloat": True},
    "borders": [
        {"type": "border", "location": "left", "size": 220, "children": [
            {"type": "tab", "name": "Explorer", "id": "explorer"},
        ]},
    ],
    "layout": {
        "type": "row",
        "children": [
            {"type": "tabset", "weight": 50, "children": [
                {"type": "tab", "name": "Panel 1", "id": "panel-1"},
            ]},
            {"type": "tabset", "weight": 50, "children": [
                {"type": "tab", "name": "Panel 2", "id": "panel-2"},
            ]},
        ],
    },
}
```

> Tip: set `"tabEnableRenderOnDemand": False` in `global` to keep every tab mounted —
> important when tab content drives Dash callbacks.


---

*Source: /reference*

---

<!-- /terms — https://flexlayout.2plot.dev/terms/llms.txt -->

# Terms of Use

> Terms of use for flexlayout-dash: what this site is, what it is not, and the terms the documentation and its code examples are offered under.

## What this site is

flexlayout-dash is documentation, published by Pip Install Python LLC. It describes an
open-source Dash component library that wraps FlexLayout-React to provide
dockable, resizable panel layouts. It is reference material: it is not advice,
it is not a service, and nothing on it is a commitment to keep any particular
behaviour working in your project.

## The documentation and the code in it

The prose and the code examples are published so you can read them, run them
and copy them. The source repository states the licence the code is offered
under, and that licence — not this page — is what governs your use of it:

- [https://github.com/pip-install-python/dash-flex-layout](https://github.com/pip-install-python/dash-flex-layout)

Everything here is offered **as is**, without warranty of any kind. Running a
code example against your own data, in your own deployment, is your decision
and your responsibility.

## Links to other sites

This site links to other sites in the 2plot network, to the upstream
FlexLayout-React project, and to third-party projects. Those sites have their
own terms and their own privacy practices, and this page does not speak for
them.

## Changes

These terms change when the site does. The change history for the whole site,
including this page, is the [Changelog](/changelog) and the repository's
commit history — there is no separate archive of previous versions, because
the repository already is one.

## Contact

Questions about these terms: the [Discord](https://discord.gg/e5s5uHWUHH) or an issue on
[the repository](https://github.com/pip-install-python/dash-flex-layout).

---

<!-- /theming — https://flexlayout.2plot.dev/theming/llms.txt -->

# Theming

> Automatic Mantine light/dark integration.

---



### Follows your Mantine theme

`DashFlexLayout` watches the document's `data-mantine-color-scheme` attribute with a
`MutationObserver`, so it restyles itself whenever your app toggles light/dark — no extra
wiring needed. Try the sun/moon switch in the header: the dock below follows it.


```python
# File: docs/theming/example.py

from dash import html
import flexlayout_dash as dfl

# DashFlexLayout watches the document's `data-mantine-color-scheme` attribute,
# so it follows the site's light/dark toggle (top-right) automatically — no
# extra wiring. You can also force it with the `colorScheme` prop.
model = {
    "global": {"tabEnableClose": False, "tabEnableFloat": True},
    "layout": {
        "type": "row",
        "children": [
            {
                "type": "tabset",
                "weight": 50,
                "children": [{"type": "tab", "name": "Light/Dark", "id": "theming-a"}],
            },
            {
                "type": "tabset",
                "weight": 50,
                "children": [{"type": "tab", "name": "Follows Mantine", "id": "theming-b"}],
            },
        ],
    },
}


def _panel(children):
    return html.Div(children, style={"padding": "16px"})


component = dfl.DashFlexLayout(
    id="theming-dock",
    model=model,
    useStateForModel=True,
    style={"height": "340px"},
    children=[
        dfl.Tab(id="theming-a", children=[_panel("Toggle the sun/moon switch in the header — this dock restyles to match.")]),
        dfl.Tab(id="theming-b", children=[_panel("The component reads `data-mantine-color-scheme` from <html> via a MutationObserver.")]),
    ],
)
```


### Forcing a scheme

To pin the scheme regardless of the page, pass the `colorScheme` prop:

```python
dfl.DashFlexLayout(id="dock", model=model, children=tabs, colorScheme="dark")
```

### Custom styling

The component renders under `.dash-dock-container` with a `.dash-dock-light` or
`.dash-dock-dark` modifier, so you can target FlexLayout's classes in your own CSS:

```css
.dash-dock-dark .flexlayout__tabset { background: #1a1b1e; }
.dash-dock-dark .flexlayout__tab    { background: #16213e; color: #eee; }
```


---

*Source: /theming*
