---
title: "Authoring a Pack"
slug: theming-authoring-a-pack
section: theming
order: 40
level: advanced
description: "Define your own theme pack — minimum viable preset, design system, and registration. Includes the full ThemeTokens / ThemePreset / DesignSystem / ThemePack reference."
---

# Authoring a Pack

A theme pack is a Python module that exports three objects: a `PRESET`
(colors, light + dark), a `DESIGN_SYSTEM` (typography / layout /
animation / interaction), and a `PACK` (the cohesive bundle). Register
all three and your pack is selectable from settings, the theme panel,
and the dev panel.

## Minimum viable example

```python
# my_app/themes/sunset.py
from djust.theming import (
    AnimationStyle, ColorScale, DesignSystem, IconStyle, InteractionStyle,
    LayoutStyle, SurfaceStyle, ThemePack, ThemePreset, ThemeTokens,
    TypographyStyle, PATTERN_MINIMAL, ILLUST_LINE,
)

# ─── colors ──────────────────────────────────────────────────────────
DARK = ThemeTokens(
    background=ColorScale(20, 80, 8),         # warm near-black
    foreground=ColorScale(30, 30, 95),
    card=ColorScale(20, 80, 11),
    card_foreground=ColorScale(30, 30, 95),
    popover=ColorScale(20, 80, 11),
    popover_foreground=ColorScale(30, 30, 95),
    primary=ColorScale(15, 90, 60),           # sunset orange
    primary_foreground=ColorScale(20, 80, 8),
    secondary=ColorScale(20, 80, 14),
    secondary_foreground=ColorScale(30, 30, 95),
    muted=ColorScale(20, 50, 15),
    muted_foreground=ColorScale(20, 30, 60),
    accent=ColorScale(15, 90, 60),
    accent_foreground=ColorScale(20, 80, 8),
    destructive=ColorScale(355, 75, 55),
    destructive_foreground=ColorScale(0, 0, 100),
    success=ColorScale(160, 60, 45),
    success_foreground=ColorScale(0, 0, 100),
    warning=ColorScale(40, 90, 55),
    warning_foreground=ColorScale(0, 0, 8),
    info=ColorScale(210, 60, 55),
    info_foreground=ColorScale(0, 0, 100),
    link=ColorScale(15, 90, 60),
    link_hover=ColorScale(15, 90, 70),
    code=ColorScale(20, 80, 14),
    code_foreground=ColorScale(30, 30, 95),
    selection=ColorScale(15, 80, 55),
    selection_foreground=ColorScale(20, 80, 8),
    brand=ColorScale(15, 90, 60),
    brand_foreground=ColorScale(20, 80, 8),
    border=ColorScale(0, 0, 18),
    input=ColorScale(0, 0, 18),
    ring=ColorScale(15, 90, 60),
    surface_1=ColorScale(20, 80, 8),
    surface_2=ColorScale(20, 80, 11),
    surface_3=ColorScale(20, 80, 14),
)

LIGHT = ThemeTokens(
    background=ColorScale(30, 100, 98),       # warm cream paper
    foreground=ColorScale(20, 80, 8),
    # …same shape, light values
)

PRESET = ThemePreset(
    name='sunset',
    display_name='Sunset',
    description='Warm dusk gradients on a near-black base',
    light=LIGHT,
    dark=DARK,
    default_mode='dark',
    radius=0.5,
)

# ─── design system (typography / layout / etc.) ──────────────────────
DESIGN_SYSTEM = DesignSystem(
    name='sunset',
    display_name='Sunset',
    description='Wide-tracking display headings, monospace labels',
    category='editorial',
    typography=TypographyStyle(
        name='sunset',
        font_family_sans="'Inter', system-ui, sans-serif",
        font_family_mono="'JetBrains Mono', monospace",
        scale_ratio=1.25,
    ),
    layout=LayoutStyle(name='sunset', density='comfortable', spacing_unit=4),
    surface=SurfaceStyle(name='sunset', shadow_intensity='subtle', border_radius='medium'),
    icons=IconStyle(name='sunset', style='outlined', stroke_width='1.5'),
    animation=AnimationStyle(name='sunset', transition_style='smooth', duration_normal='0.25s'),
    interaction=InteractionStyle(name='sunset', button_hover='color', focus_style='ring'),
)

# ─── pack (the cohesive bundle) ──────────────────────────────────────
PACK = ThemePack(
    name='sunset',
    display_name='Sunset',
    description='Warm dusk preset on an editorial design system',
    category='editorial',
    design_theme='sunset',
    color_preset='sunset',
    icon_style=DESIGN_SYSTEM.icons,
    animation_style=DESIGN_SYSTEM.animation,
    pattern_style=PATTERN_MINIMAL,
    interaction_style=DESIGN_SYSTEM.interaction,
    illustration_style=ILLUST_LINE,
)
```

## Registration

Register all three during app startup:

```python
# my_app/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = 'my_app'

    def ready(self):
        from djust.theming import register_theme_pack, register_preset, register_design_system
        from .themes.sunset import PRESET, DESIGN_SYSTEM, PACK

        register_preset('sunset', PRESET)
        register_design_system('sunset', DESIGN_SYSTEM)
        register_theme_pack('sunset', PACK)
```

Then point your settings at it:

```python
LIVEVIEW_CONFIG = {
    'theme': {'pack': 'sunset', 'default_mode': 'dark'},
}
```

## Mixing and matching

You don't need a custom design system to ship a custom pack. You can
build on a shipped one:

```python
from djust.theming import get_registry

# Pair your custom preset with the shipped 'editorial' design system.
editorial_ds = get_registry().get_design_system('editorial')

PACK = ThemePack(
    name='sunset-editorial',
    display_name='Sunset Editorial',
    description='Sunset colors with the editorial design system',
    category='editorial',
    design_theme='editorial',
    color_preset='sunset',
    icon_style=editorial_ds.icons,
    animation_style=editorial_ds.animation,
    pattern_style=PATTERN_MINIMAL,
    interaction_style=editorial_ds.interaction,
    illustration_style=ILLUST_LINE,
)
```

This is how the shipped packs are structured — many of them share design
systems and only differ in their `PRESET`.

## Field reference

### `ThemePreset`

The full set is in
[`_types.py`](https://github.com/djust-org/djust/blob/main/python/djust/theming/_types.py),
but the load-bearing fields:

| Field | Required | What it does |
|---|---|---|
| `name` | yes | Unique identifier used in settings and the API. |
| `display_name` | yes | Human-readable name shown in the theme panel. |
| `light` / `dark` | yes | `ThemeTokens` for each mode. Both required. |
| `default_mode` | no | `'light'` or `'dark'`. Determines which set goes into `:root`. Default: `'light'`. |
| `radius` | no | Border-radius multiplier in `rem`. Default: `0.5`. |
| `extra_css_vars` | no | Project-specific knobs. See [`extra_css_vars`](/theming/extra-css-vars/). |
| `extra_css_vars_light` / `_dark` | no | Mode-specific overrides. |
| `surface` | no | `SurfaceTreatment` for glass / gradient / noise effects. |

### `ThemeTokens`

22 color slots — see [Tokens](/theming/tokens/) for the full table and
what each one paints. Every slot is a `ColorScale(hue, saturation,
lightness)` with HSL values 0-360 / 0-100% / 0-100%.

### `DesignSystem`

| Field | What it does |
|---|---|
| `typography` | Font families, scale ratio, weight scale. |
| `layout` | Density (compact / comfortable / spacious), spacing unit. |
| `surface` | Shadow intensity, border-radius preset, surface treatment. |
| `icons` | Style (outlined / filled / rounded), stroke width, corner rounding. |
| `animation` | Entrance / hover / loading effects, durations, easing. |
| `interaction` | Button-hover, link-hover, card-hover, focus-style behaviors. |

### `ThemePack`

The cohesive bundle — references a preset name and a design-system name,
plus pattern + illustration choices and copies of icon / animation /
interaction styles for cross-pack querying.

## Testing your pack

Local dev:

1. Add the pack module + register it in `apps.py:ready()`.
2. Restart the dev server.
3. Open `/_djust/panel/` (DEBUG=True) → Theming tab → swap to your pack.
4. Click through every page that matters; look for color leaks (see
   [Troubleshooting](/theming/troubleshooting/)).
5. Toggle light / dark mode; verify both sets work.

## See also

- **[Tokens](/theming/tokens/)** — full reference for every color slot.
- **[Structure vs. style](/theming/structure-vs-style/)** — what does NOT
  belong in a pack.
- **[Source: djust.theming.themes](https://github.com/djust-org/djust/tree/main/python/djust/theming/themes)**
  — every shipped pack as a reference.
