AGENTS.md@skills/dmc-best-practices · git:20260308.4723f7b · 2026-03-08 · sha256 9565c15f5588f76d

AGENTS.md@skills/dmc-best-practices git:20260308.4723f7bA

Immutable. This exact content is served forever at /api/v1/blob/9565c15f5588f76d.

# DMC Best Practices - Complete Reference

> This document is optimized for AI agents and LLMs. It contains the complete, compiled reference for Dash Mantine Components best practices.

## Table of Contents

1. [Architecture (CRITICAL)](#1-architecture-critical)
   - [Wrap Layout in MantineProvider](#wrap-layout-in-mantineprovider)
   - [Define Callbacks Before app.run()](#define-callbacks-before-apprun)
   - [Prevent Circular Callbacks](#prevent-circular-callbacks)
   - [Never Modify Global Variables](#never-modify-global-variables-in-callbacks)

2. [Callbacks (CRITICAL/HIGH)](#2-callbacks-criticalhigh)
   - [Use State for Non-Triggering Values](#use-state-for-non-triggering-values)
   - [Return JSON-Serializable Values Only](#return-json-serializable-values-only)
   - [PreventUpdate vs no_update](#preventupdate-vs-no_update)
   - [Debounce Text Inputs](#debounce-text-inputs)
   - [Use ctx.triggered_id Correctly](#use-ctxtriggered_id-correctly)
   - [Use prevent_initial_call Appropriately](#use-prevent_initial_call-appropriately)

3. [Styling (HIGH/MEDIUM)](#3-styling-highmedium)
   - [Use Static Selectors Only](#use-static-selectors-only)
   - [Limit Style Props to 3-4 Per Component](#limit-style-props-to-3-4-per-component)
   - [Use CSS Media Queries for Responsive](#use-css-media-queries-for-responsive-design)
   - [Prefer CSS Variables Over Hardcoded](#prefer-css-variables-over-hardcoded-values)
   - [Use classNames Over styles Prop](#use-classnames-over-styles-prop)

4. [Data Management (HIGH)](#4-data-management-high)
   - [Use dcc.Store for Client-Side Data](#use-dccstore-for-client-side-data)
   - [Use Server-Side Caching for Large Data](#use-server-side-caching-for-large-data)
   - [Isolate Cache by Session ID](#isolate-cache-by-session-id)
   - [Use Signaling Pattern for Expensive Ops](#use-signaling-pattern-for-expensive-operations)

5. [Performance (MEDIUM-HIGH)](#5-performance-medium-high)
   - [Use Clientside Callbacks for Frequent Updates](#use-clientside-callbacks-for-frequent-updates)
   - [Memoize Expensive Functions](#memoize-expensive-functions)
   - [Use WebGL for Large Scatter Plots](#use-webgl-for-large-scatter-plots)

6. [Forms & Validation (MEDIUM)](#6-forms--validation-medium)
   - [Validate Early, Fail Fast](#validate-early-fail-fast)
   - [Return User-Friendly Error Messages](#return-user-friendly-error-messages)

7. [Theming (MEDIUM)](#7-theming-medium)
   - [Custom Colors Need 10 Shades](#custom-colors-need-10-shades)
   - [Test Both Light and Dark Modes](#test-both-light-and-dark-modes)
   - [Set Component Defaults in Theme](#set-component-defaults-in-theme)

8. [DMC v2.x Migrations (MEDIUM)](#8-dmc-v2x-migrations-medium)
   - [DMC v2.x Breaking Changes](#dmc-v2x-breaking-changes)
   - [Use NotificationContainer Not Provider](#use-notificationcontainer-not-notificationprovider)

9. [Accessibility (MEDIUM)](#9-accessibility-medium)
   - [Always Provide Labels for Inputs](#always-provide-labels-for-form-inputs)

---

## 1. Architecture (CRITICAL)

### Wrap Layout in MantineProvider

**Impact:** CRITICAL - App fails to render without it

All Dash Mantine Components require a MantineProvider wrapper at the root of your layout.

**Incorrect:**
```python
app.layout = dmc.Container([
    dmc.Title("My App"),
    dmc.Button("Click me"),
])
# Error: MantineProvider is required
```

**Correct:**
```python
app.layout = dmc.MantineProvider([
    dmc.Container([
        dmc.Title("My App"),
        dmc.Button("Click me"),
    ])
])
```

---

### Define Callbacks Before app.run()

**Impact:** CRITICAL - Callbacks silently ignored if defined after run()

All callbacks must be registered before calling `app.run()`.

**Incorrect:**
```python
if __name__ == "__main__":
    app.run(debug=True)

# Callback defined AFTER app.run() - NEVER REGISTERED
@callback(Output("output", "children"), Input("btn", "n_clicks"))
def update(n):
    return f"Clicked {n} times"
```

**Correct:**
```python
@callback(Output("output", "children"), Input("btn", "n_clicks"))
def update(n):
    return f"Clicked {n or 0} times"

if __name__ == "__main__":
    app.run(debug=True)
```

---

### Prevent Circular Callbacks

**Impact:** CRITICAL - Causes infinite loops, crashes browser/server

When a callback output feeds back as an input, it creates an infinite loop.

**Incorrect:**
```python
@callback(
    Output("counter", "children"),
    Input("counter", "children"),  # Same as output - CIRCULAR
    Input("btn", "n_clicks"),
)
def update(current, n):
    return int(current or 0) + 1
```

**Correct:**
```python
@callback(
    Output("counter", "children"),
    Input("btn", "n_clicks"),
    State("counter", "children"),  # State reads without triggering
)
def update(n, current):
    if not n:
        return "0"
    return str(int(current or 0) + 1)
```

---

### Never Modify Global Variables in Callbacks

**Impact:** CRITICAL - Breaks multi-worker deployments, causes data leaks

Each worker has its own copy of global state, causing inconsistent behavior.

**Incorrect:**
```python
click_count = 0  # Global variable

@callback(Output("output", "children"), Input("btn", "n_clicks"))
def update(n):
    global click_count
    click_count += 1  # DANGEROUS
    return f"Total: {click_count}"
```

**Correct:**
```python
# Use dcc.Store instead
dcc.Store(id="click-store", data={"count": 0})

@callback(
    Output("click-store", "data"),
    Output("output", "children"),
    Input("btn", "n_clicks"),
    State("click-store", "data"),
)
def update(n, store):
    if not n:
        return store, f"Total: {store['count']}"
    new_count = store["count"] + 1
    return {"count": new_count}, f"Total: {new_count}"
```

---

## 2. Callbacks (CRITICAL/HIGH)

### Use State for Non-Triggering Values

**Impact:** CRITICAL - Using Input when State needed causes unwanted executions

`Input` triggers the callback when its value changes. `State` reads without triggering.

**Incorrect:**
```python
@callback(
    Output("result", "children"),
    Input("name", "value"),      # Triggers on every keystroke
    Input("email", "value"),     # Also triggers
    Input("submit", "n_clicks"),
)
def submit_form(name, email, n):
    return process_form(name, email)  # Runs hundreds of times
```

**Correct:**
```python
@callback(
    Output("result", "children"),
    Input("submit", "n_clicks"),   # Only trigger
    State("name", "value"),        # Read without triggering
    State("email", "value"),
    prevent_initial_call=True,
)
def submit_form(n, name, email):
    return process_form(name, email)  # Runs only on submit
```

---

### Return JSON-Serializable Values Only

**Impact:** CRITICAL - Non-serializable returns crash callbacks silently

Callbacks must return: dict, list, str, int, float, bool, None.

**Incorrect:**
```python
from datetime import datetime

@callback(Output("timestamp", "children"), Input("btn", "n_clicks"))
def update(n):
    return datetime.now()  # NOT serializable
```

**Correct:**
```python
@callback(Output("timestamp", "children"), Input("btn", "n_clicks"))
def update(n):
    return datetime.now().isoformat()  # String is serializable
```

**Conversions:**
- datetime → `.isoformat()`
- Decimal → `float()` or `str()`
- DataFrame → `.to_dict("records")`
- numpy array → `.tolist()`

---

### PreventUpdate vs no_update

**Impact:** HIGH - Wrong choice causes unnecessary updates or blocks needed ones

`PreventUpdate` blocks ALL outputs. `no_update` selectively skips specific outputs.

**Incorrect:**
```python
from dash.exceptions import PreventUpdate

@callback(Output("status", "children"), Output("data", "children"), Input("btn", "n_clicks"))
def update(n):
    if n % 2 == 0:
        raise PreventUpdate  # Blocks BOTH outputs
```

**Correct:**
```python
from dash import no_update

@callback(Output("status", "children"), Output("data", "children"), Input("btn", "n_clicks"))
def update(n):
    if n % 2 == 0:
        return no_update, fetch_data()  # Skip status, update data
    return "Updated", fetch_data()
```

---

### Debounce Text Inputs

**Impact:** HIGH - Without debounce, callbacks fire on every keystroke

**Incorrect:**
```python
dmc.TextInput(id="search")

@callback(Output("results", "children"), Input("search", "value"))
def search(query):
    return api_search(query)  # 5 API calls for typing "hello"
```

**Correct:**
```python
dmc.TextInput(id="search", debounce=300)  # Wait 300ms

@callback(Output("results", "children"), Input("search", "value"))
def search(query):
    return api_search(query)  # 1 call after typing stops
```

---

### Use ctx.triggered_id Correctly

**Impact:** MEDIUM-HIGH - Determine which input fired the callback

**Correct:**
```python
from dash import ctx

@callback(
    Output("counter", "children"),
    Input("btn-add", "n_clicks"),
    Input("btn-subtract", "n_clicks"),
    prevent_initial_call=True,
)
def update(add_clicks, sub_clicks):
    triggered = ctx.triggered_id
    if triggered == "btn-add":
        return f"Added! Total: {add_clicks}"
    elif triggered == "btn-subtract":
        return f"Subtracted! Total: {sub_clicks}"
```

---

### Use prevent_initial_call Appropriately

**Impact:** MEDIUM - Skip unnecessary callback on page load

**Correct for button-triggered actions:**
```python
@callback(
    Output("result", "children"),
    Input("submit-btn", "n_clicks"),
    prevent_initial_call=True,  # Skip on page load
)
def submit(n):
    return f"Submitted {n} times"
```

**Don't use for initial data loading:**
```python
@callback(Output("chart", "figure"), Input("date-picker", "value"))
def update_chart(date):
    # Should run on load for initial chart
    return create_chart(date or default_date)
```

---

## 3. Styling (HIGH/MEDIUM)

### Use Static Selectors Only

**Impact:** CRITICAL - Dynamic class selectors break on library updates

**Incorrect:**
```css
.m_77c9d27d { background-color: red; }  /* Breaks on update */
```

**Correct:**
```css
.mantine-Button-root { background-color: red; }  /* Stable */
.mantine-Button-root[data-disabled="true"] { opacity: 0.5; }
```

---

### Limit Style Props to 3-4 Per Component

**Impact:** HIGH - Excessive style props reduce readability

**Incorrect:**
```python
dmc.Card(p="xl", m="md", w=400, h=300, bg="gray.1", c="dark.9", radius="lg", shadow="md")
```

**Correct:**
```python
dmc.Card(p="xl", radius="lg", className="feature-card")
```
```css
.feature-card { width: 400px; height: 300px; /* ... */ }
```

---

### Use CSS Media Queries for Responsive Design

**Impact:** HIGH - More performant than responsive style props

**Incorrect:**
```python
dmc.SimpleGrid(cols={"base": 1, "sm": 2, "md": 3, "lg": 4})
```

**Correct:**
```python
dmc.SimpleGrid(className="responsive-grid")
```
```css
.responsive-grid { grid-template-columns: 1fr; }
@media (min-width: 48em) { .responsive-grid { grid-template-columns: repeat(2, 1fr); } }
```

---

### Prefer CSS Variables Over Hardcoded Values

**Impact:** MEDIUM - Maintains consistency and enables theme changes

**Incorrect:**
```css
.my-card { background-color: #f8f9fa; padding: 16px; }
```

**Correct:**
```css
.my-card {
    background-color: var(--mantine-color-gray-0);
    padding: var(--mantine-spacing-md);
}
```

---

### Use classNames Over styles Prop

**Impact:** MEDIUM - Better maintainability and CSS specificity

**Incorrect:**
```python
dmc.Button("Submit", styles={"root": {"minWidth": "200px"}})
```

**Correct:**
```python
dmc.Button("Submit", classNames={"root": "submit-button"})
```
```css
.submit-button { min-width: 200px; }
.submit-button:hover { background-color: var(--mantine-color-green-7); }
```

---

## 4. Data Management (HIGH)

### Use dcc.Store for Client-Side Data

**Impact:** HIGH - Proper data sharing between callbacks

**Correct:**
```python
dcc.Store(id="shared-store", data={"value": None})

@callback(Output("shared-store", "data"), Input("btn", "n_clicks"), State("shared-store", "data"))
def update(n, store):
    store["value"] = n
    return store

@callback(Output("display", "children"), Input("shared-store", "modified_timestamp"), State("shared-store", "data"))
def display(ts, store):
    return f"Value: {store.get('value')}"
```

---

### Use Server-Side Caching for Large Data

**Impact:** HIGH - Handles datasets too large for client-side

**Correct:**
```python
from flask_caching import Cache

cache = Cache(app.server, config={"CACHE_TYPE": "filesystem", "CACHE_DIR": ".cache"})

@cache.memoize()
def get_large_dataframe():
    return pd.read_csv("large_file.csv")

@callback(Output("chart", "figure"), Input("filter", "value"))
def update(filter_val):
    df = get_large_dataframe()  # Cached
    return create_figure(df[df["category"] == filter_val])
```

---

### Isolate Cache by Session ID

**Impact:** HIGH - Required for multi-user deployments

**Correct:**
```python
def get_session_id():
    if "session_id" not in session:
        session["session_id"] = str(uuid.uuid4())
    return session["session_id"]

def get_user_data(session_id, filters):
    cache_key = f"data_{session_id}_{hash(tuple(filters))}"
    # Each user's data isolated
```

---

### Use Signaling Pattern for Expensive Operations

**Impact:** MEDIUM-HIGH - Compute once, retrieve cached results

**Correct:**
```python
dcc.Store(id="data-signal")

@callback(Output("data-signal", "data"), Input("filters", "value"))
def compute(filters):
    data = expensive_query(filters)
    cache.set(f"data_{hash(tuple(filters))}", data)
    return {"cache_key": f"data_{hash(tuple(filters))}"}

@callback(Output("chart", "figure"), Input("data-signal", "data"))
def update_chart(signal):
    data = cache.get(signal["cache_key"])
    return create_chart(data)
```

---

## 5. Performance (MEDIUM-HIGH)

### Use Clientside Callbacks for Frequent Updates

**Impact:** HIGH - Eliminates server round-trips

**Correct:**
```python
clientside_callback(
    """
    function(n_clicks) {
        if (!n_clicks) return window.dash_clientside.no_update;
        const current = document.documentElement.getAttribute('data-mantine-color-scheme');
        return current === 'light' ? 'dark' : 'light';
    }
    """,
    Output("mantine-provider", "forceColorScheme"),
    Input("theme-toggle", "n_clicks"),
)
```

---

### Memoize Expensive Functions

**Impact:** MEDIUM-HIGH - Cache repeated computations

**Correct:**
```python
from functools import lru_cache

@lru_cache(maxsize=128)
def process_data(category, year):
    df = pd.read_csv("large_file.csv")
    return df[(df["category"] == category) & (df["year"] == year)].to_dict()
```

---

### Use WebGL for Large Scatter Plots

**Impact:** MEDIUM - Required for 100k+ data points

**Correct:**
```python
import plotly.express as px

fig = px.scatter(df, x="x", y="y", render_mode="webgl")
# Or use go.Scattergl instead of go.Scatter
```

---

## 6. Forms & Validation (MEDIUM)

### Validate Early, Fail Fast

**Impact:** MEDIUM - Collect all errors, show clear feedback

**Correct:**
```python
@callback(
    Output("result", "children"),
    Output("email", "error"),
    Output("password", "error"),
    Input("submit", "n_clicks"),
    State("email", "value"),
    State("password", "value"),
    prevent_initial_call=True,
)
def submit(n, email, password):
    errors = []
    email_error = password_error = ""

    if not email or "@" not in email:
        email_error = "Valid email required"
        errors.append(email_error)
    if not password or len(password) < 8:
        password_error = "Password must be 8+ characters"
        errors.append(password_error)

    if errors:
        return dmc.Alert(children=errors, color="red"), email_error, password_error

    return dmc.Alert("Success!", color="green"), "", ""
```

---

### Return User-Friendly Error Messages

**Impact:** MEDIUM - Log technical details, show helpful messages

**Correct:**
```python
import logging
logger = logging.getLogger(__name__)

@callback(Output("result", "children"), Input("btn", "n_clicks"), prevent_initial_call=True)
def process(n):
    try:
        return dmc.Alert(f"Success: {process_data()}", color="green")
    except ConnectionError as e:
        logger.error(f"API failed: {e}", exc_info=True)
        return dmc.Alert("Unable to connect. Try again later.", color="red")
```

---

## 7. Theming (MEDIUM)

### Custom Colors Need 10 Shades

**Impact:** HIGH - Incomplete palettes cause runtime errors

**Correct:**
```python
theme = {
    "colors": {
        "brand": [
            "#E3F2FD", "#BBDEFB", "#90CAF9", "#64B5F6", "#42A5F5",
            "#2196F3", "#1E88E5", "#1976D2", "#1565C0", "#0D47A1",
        ],  # Exactly 10 shades (0-9)
    },
    "primaryColor": "brand",
}
```

---

### Test Both Light and Dark Modes

**Impact:** MEDIUM - Ensures consistent appearance

**Correct:**
```python
dmc.MantineProvider(id="mantine-provider", defaultColorScheme="light", children=[...])

# CSS for both modes:
# [data-mantine-color-scheme="dark"] .my-component { ... }
```

---

### Set Component Defaults in Theme

**Impact:** MEDIUM - Centralize styling for consistency

**Correct:**
```python
theme = {
    "components": {
        "Button": {"defaultProps": {"size": "md", "radius": "md"}},
        "TextInput": {"defaultProps": {"size": "md", "radius": "sm"}},
    },
}
# Now all Buttons get size="md" by default
```

---

## 8. DMC v2.x Migrations (MEDIUM)

### DMC v2.x Breaking Changes

**Key changes from v1.x to v2.x:**

| Change | v1.x | v2.x |
|--------|------|------|
| DateTimePicker | `timeInputProps={}` | `timePickerProps={}` |
| Carousel | `loop=True` | `emblaOptions={"loop": True}` |
| Image | `flex: 0` default | Add `flex=0` explicitly |
| DatesProvider | `timezone` supported | `timezone` removed |
| Popover.hideDetached | `False` | `True` |

---

### Use NotificationContainer Not NotificationProvider

**Impact:** MEDIUM - NotificationProvider is deprecated

**Incorrect:**
```python
dmc.NotificationProvider(position="top-right")  # DEPRECATED
```

**Correct:**
```python
dmc.NotificationContainer(position="top-right")  # v2.x
```

---

## 9. Accessibility (MEDIUM)

### Always Provide Labels for Form Inputs

**Impact:** MEDIUM - Required for screen readers

**Incorrect:**
```python
dmc.TextInput(id="email", placeholder="Enter email")  # No label
```

**Correct:**
```python
dmc.TextInput(
    id="email",
    label="Email Address",
    placeholder="Enter your email",
    description="We'll never share your email",
    required=True,
)
```

---

## Quick Reference: Top 10 Rules

1. **Wrap layout in MantineProvider** - All DMC components require it
2. **Never modify global variables** in callbacks - Breaks multi-worker deployments
3. **Use State not Input** for values that shouldn't trigger callbacks
4. **Define callbacks before app.run()** - Registration must happen first
5. **Prevent circular callbacks** - Outputs feeding inputs cause infinite loops
6. **Return JSON-serializable values** - Only dict, list, str, number, bool, None
7. **Use static CSS selectors** - Never target `.m_*` dynamic classes
8. **Custom colors need 10 shades** - Exactly 10 (0=lightest, 9=darkest)
9. **Use dcc.Store for client data** - Not global variables
10. **Debounce text inputs** - Limit callback firing on rapid changes