Skip to content
wdj

Design skill

Build consistent wdj screens with typography, spacing, surfaces and interaction patterns adapted for Django, HTMX and Alpine.js.

Use the skill

The ZIP contains wdj-design/SKILL.md and its references. Extract the folder into your project's .agents/skills/ directory. Read the project's instructions and component API before applying it.

Live previews use the recommended Django source. Comparisons show code to avoid. The same source files are included in the download.

Visual rules

1. Content at 14px

Keep body copy, controls and data at the shared text-base size. Larger text is for headings.

Recommended example

API tokens

The production token expires in 30 days.

Code to avoid

<p class="text-lg">The production token expires in 30 days.</p>
Recommended Django source
Django template
{% load wdj %}
<div class="grid gap-1.5">
  <h4 class="text-lg font-semibold">API tokens</h4>
  <p class="text-base text-kumo-subtle">The production token expires in 30 days.</p>
</div>

2. Sentence-case headings

Use ordinary sentence case; preserve the spelling of product names.

Recommended example

Recent requests

Code to avoid

<h4 class="uppercase">RECENT REQUESTS</h4>
<h4 class="capitalize">Recent Requests</h4>
Recommended Django source
Django template
{% load wdj %}
<h4 class="text-lg font-semibold text-kumo-strong">Recent requests</h4>

3. Natural letter spacing

Keep the font's default tracking so the type hierarchy stays consistent.

Recommended example

Worker metrics

Code to avoid

<h4 class="tracking-tight">Worker metrics</h4>
Recommended Django source
Django template
{% load wdj %}
<h4 class="text-lg font-semibold">Worker metrics</h4>

4. Moderate emphasis

Use semibold for headings and medium for emphasis within a sentence.

Recommended example

Account settings

A workspace name is required.

Code to avoid

<h4 class="font-bold">Account settings</h4>
<strong class="font-bold">required</strong>
Recommended Django source
Django template
{% load wdj %}
<div class="grid gap-1.5">
  <h4 class="text-lg font-semibold">Account settings</h4>
  <p class="text-base">A workspace name is <strong class="font-medium">required</strong>.</p>
</div>

6. Optical padding

Account for text line-height with slightly less vertical padding than horizontal padding.

Recommended example

Production

Code to avoid

<div class="p-5"><p>Production</p></div>
Recommended Django source
Django template
{% load wdj %}
<div class="rounded-lg bg-kumo-recessed px-5 py-4 ring-1 ring-kumo-line">
  <p class="text-base font-medium">Production</p>
</div>

7. Immediate hover feedback

Change hover colors immediately. Reserve animation for changes such as opening a panel.

Recommended example

Code to avoid

<button class="transition-colors duration-300 hover:bg-kumo-tint">Open components</button>
Recommended Django source
Django template
{% load wdj %}
{% include "wdj/components/button.html" with label="Open components" href="/components/" only %}

8. Rings on shadowed surfaces

Use a ring with an elevated shadow. Plain dividers and unshadowed panels can use a border.

Recommended example

Workers API

Last deployed four minutes ago.

Code to avoid

<div class="border border-kumo-line shadow-md">Workers API</div>
Recommended Django source
Django template
{% load wdj %}
<div class="rounded-lg bg-kumo-base px-5 py-4 shadow-md ring-1 ring-kumo-line">
  <h4 class="text-lg font-semibold">Workers API</h4>
  <p class="text-base text-kumo-subtle">Last deployed four minutes ago.</p>
</div>

9. Concentric corners

For insets up to 8px, outer radius equals inner radius plus padding: 16px = 8px + 8px here.

Recommended example

A consistent 8px inset, including the corners.

Code to avoid

<div class="rounded-xl p-2">
  <div class="rounded-xl">The corner gap changes.</div>
</div>
Recommended Django source
Django template
{% load wdj %}
<div class="rounded-2xl bg-kumo-tint p-2 ring-1 ring-kumo-line">
  <div class="rounded-lg bg-kumo-base px-5 py-4 ring-1 ring-kumo-line">
    <p class="text-base">A consistent 8px inset, including the corners.</p>
  </div>
</div>

10. Icons on the first text line

Give the icon a line-height wrapper so wrapped text does not pull the icon to its middle.

Recommended example

API token permissions cannot be changed after creation. Create a new token to update its permissions.

Code to avoid

<div class="flex items-center">
  <svg></svg><p>A message that wraps onto several lines.</p>
</div>
Recommended Django source
Django template
{% load wdj %}
<div class="flex items-start gap-2">
  <span class="flex h-lh shrink-0 items-center text-kumo-warning-strong">
    {% wdj_icon "triangle-alert" class="size-3.5" %}
  </span>
  <p class="text-base">API token permissions cannot be changed after creation. Create a new token to update its permissions.</p>
</div>

11. Lighter inline monospace

Use 0.9em for inline code within a normal sentence; keep code blocks at their documented size.

Recommended example

Edit settings.py to enable wdj.

Code to avoid

<p>Edit <code class="font-mono">settings.py</code> to enable wdj.</p>
Recommended Django source
Django template
{% load wdj %}
<p class="text-base">Edit <code class="font-mono text-[0.9em]">settings.py</code> to enable wdj.</p>

12. Visible sticky separators

A sticky toolbar needs a background and a bottom divider to stay distinct from scrolling rows.

Recommended example

API tokens

  • Production token
  • Preview token
  • Staging token
  • Development token

Code to avoid

<div class="sticky top-0">API tokens</div>
Recommended Django source
Django template
{% load wdj %}
<div tabindex="0" role="region" aria-label="Scrollable token preview" class="relative max-h-40 overflow-auto rounded-lg ring-1 ring-kumo-line">
  <div class="sticky top-0 z-10 border-b border-kumo-line bg-kumo-base px-4 py-2.5">
    <p class="text-base font-medium">API tokens</p>
  </div>
  <ul class="divide-y divide-kumo-line text-base">
    <li class="px-4 py-3">Production token</li>
    <li class="px-4 py-3">Preview token</li>
    <li class="px-4 py-3">Staging token</li>
    <li class="px-4 py-3">Development token</li>
  </ul>
</div>

13. Stable collapsing content

Keep the text width stable during a collapse. This responsive example changes opacity, without animating width.

Recommended example

Web analytics

The text keeps its width during the fade.

Code to avoid

<div class="transition-all" :style="expanded ? 'width:256px' : 'width:0'">
  <p class="w-full">The text reflows while the panel closes.</p>
</div>
Recommended Django source
Django template
{% load wdj %}
<div x-data="{ expanded: true }" class="grid min-w-0 gap-3">
  <button type="button" @click="expanded = !expanded" :aria-expanded="expanded" aria-controls="design-collapse-content" class="w-fit rounded-lg bg-kumo-base px-3 py-2 text-base ring-1 ring-kumo-line hover:bg-kumo-tint">Toggle preview</button>
  <div class="w-64 max-w-full">
    <div id="design-collapse-content" x-show="expanded" x-transition.opacity class="rounded-lg bg-kumo-recessed px-4 py-3">
      <h4 class="text-lg font-semibold">Web analytics</h4>
      <p class="text-base text-kumo-subtle">The text keeps its width during the fade.</p>
    </div>
  </div>
</div>

14. One layered surface

Keep a single LayerCard for the composition; use ordinary content inside its primary surface.

Recommended example

Workspace activity

Recent requests

Status
Healthy
Requests
1,284

Code to avoid

{% wdj_component "wdj/components/layer_card.html" title="Outer" %}
  {% wdj_component "wdj/components/layer_card.html" title="Inner" %}...{% endwdj_component %}
{% endwdj_component %}
Recommended Django source
Django template
{% load wdj %}
{% wdj_component "wdj/components/layer_card.html" secondary="Workspace activity" title="Recent requests" %}
  <dl class="grid grid-cols-2 gap-2 text-base">
    <dt class="text-kumo-subtle">Status</dt><dd>Healthy</dd>
    <dt class="text-kumo-subtle">Requests</dt><dd>1,284</dd>
  </dl>
{% endwdj_component %}

15. Keep dialogs mounted

Render the native dialog once and open it with the wdj hook. Dismissal returns focus to its trigger.

Recommended example

Review workspace

The dialog stays in the document while closed.

Code to avoid

{% if show_dialog %}
  {% include "wdj/components/dialog.html" with id="confirm" title="Review workspace" %}
{% endif %}
Recommended Django source
Django template
{% load wdj %}
{% wdj_component "wdj/components/dialog.html" id="design-example-dialog" title="Review workspace" trigger_label="Open dialog example" %}
  <div class="grid gap-4">
    <p class="text-base">The dialog stays in the document while closed.</p>
    <button type="button" data-wdj-dialog-close class="w-fit rounded-lg bg-kumo-base px-3 py-2 text-base ring-1 ring-kumo-line hover:bg-kumo-tint">Close example</button>
  </div>
{% endwdj_component %}

Screen recipes

1. Page header with navigation and actions

Keep breadcrumbs, title/description and tabs/actions as separate groups. The action row stacks on mobile.

Recommended example

Workspace settings

Configure access and preferences.

Recommended Django source
Django template
{% load wdj %}
{% wdj_component "wdj/components/page_header.html" title="Workspace settings" description="Configure access and preferences." heading_level=2 crumbs=design_crumbs tabs=design_navigation %}
  {% include "wdj/components/button.html" with label="View all resources" href="/resources/" only %}
{% endwdj_component %}

2. Preferences form with local feedback

Use one control size throughout a row, explicit labels and native required validation. This example updates local state.

Recommended example

Workspace preferences

Update this local preview.

Recommended Django source
Django template
{% load wdj %}
<form id="design-preferences" x-data="{ saved: false }" @submit.prevent="saved = true" @input="saved = false">
  {% wdj_component "wdj/components/settings_section.html" title="Workspace preferences" description="Update this local preview." %}
    {% include "wdj/components/input.html" with id="design-workspace" name="workspace" label="Workspace name" value="Demo workspace" required=True size="base" only %}
    <div class="flex flex-wrap items-center gap-3">
      {% include "wdj/components/button.html" with label="Save preview" type="submit" variant="primary" size="base" only %}
      <p role="status" class="text-base text-kumo-subtle" x-text="saved ? 'Preview updated locally.' : 'Changes stay in this preview.'"></p>
    </div>
  {% endwdj_component %}
</form>

3. Tabs, range and accordion with separate owners

Alpine owns local panels; the range and exclusive disclosures use native elements with wdj initialization.

Recommended example

Project configuration.
Preferences change locally.
Access settings

Only one section is expanded.

Billing settings

Disclosure state is native.

Recommended Django source
Django template
{% load wdj %}
<div class="grid min-w-0 gap-6">
  {% include "wdj/components/tabs.html" with id="design-local-tabs" items=design_tabs mode="local" label="Design example panels" only %}
  <form class="grid gap-3">
    {% include "wdj/components/slider.html" with id="design-budget" name="budget" label="Budget range" range=True min=10 max=90 step=5 only %}
    <div>{% include "wdj/components/button.html" with label="Reset example range" type="reset" only %}</div>
  </form>
  <div class="grid gap-2">
    {% wdj_component "wdj/components/collapsible.html" title="Access settings" name="design-accordion" %}<p class="text-base">Only one section is expanded.</p>{% endwdj_component %}
    {% wdj_component "wdj/components/collapsible.html" title="Billing settings" name="design-accordion" %}<p class="text-base">Disclosure state is native.</p>{% endwdj_component %}
  </div>
</div>

4. Server search with a real fallback

The shared GET endpoint owns search results. HTMX replaces only the result region; the form still has a normal GET action.

Recommended example

Search results appear here.
Recommended Django source
Django template
{% load wdj %}
{% wdj_component "wdj/components/filter_bar.html" label="Find a component" action="/components/" attrs=design_search_attrs %}
  <div class="min-w-0 flex-1">{% include "wdj/components/input.html" with id="design-search" name="q" label="Component name" placeholder="Search the catalog" only %}</div>
  {% include "wdj/components/button.html" with label="Search examples" type="submit" only %}
{% endwdj_component %}
<div id="design-search-results" aria-live="polite" class="mt-4 min-w-0 text-base text-kumo-subtle">Search results appear here.</div>

5. Grouped navigation in a bounded preview

Keep contained navigation in its own height-constrained frame. The page shell owns the mobile overlay; this example stays inline.

Recommended example

Related pages stay together in named groups. This caption moves below the navigation on narrow screens.

Recommended Django source
Django template
{% load wdj %}
<div class="flex flex-col overflow-hidden rounded-lg ring-1 ring-kumo-line sm:h-80 sm:flex-row">
  <div class="h-80 shrink-0 sm:h-full">
    {% include "wdj/components/sidebar.html" with groups=design_groups aria_label="Design example navigation" brand_label="Workspace" brand_initials="W" contained=True persist_key="design-skill-sidebar" only %}
  </div>
  <p class="min-w-0 flex-1 px-5 py-4 text-base text-kumo-subtle">Related pages stay together in named groups. This caption moves below the navigation on narrow screens.</p>
</div>
Context data for recipes
design_crumbs = [{"label": "Home", "href": "/"}, {"label": "Settings"}]
design_navigation = [
    {"label": "Overview", "href": "/resources/", "active": True},
    {"label": "Components", "href": "/components/"},
]
design_tabs = [
    {"key": "overview", "label": "Overview", "content": "Project configuration."},
    {"key": "settings", "label": "Settings", "content": "Preferences change locally."},
]
design_search_attrs = {
    "hx-get": "/components/list/",
    "hx-target": "#design-search-results",
    "hx-swap": "innerHTML",
}
design_groups = [{"label": "Workspace", "items": [
    {"label": "Resources", "href": "/resources/", "icon": "database"},
    {"label": "Components", "href": "/components/", "icon": "boxes"},
    {"label": "Interaction guide", "href": "/guide/", "icon": "book-open"},
]}]

Review a screen

Check both themes, readable labels, matching control heights, keyboard focus, disabled/error states, repeated HTMX updates and narrow/landscape layouts. A save or delete in a real application requires server validation and permissions.

Components

Components overview
AutocompleteFree-form input with local or HTMX-filtered suggestions.
BadgeSolid, tinted, outline and dot status labels.
BannerInline message bar with variants and optional action.
BreadcrumbsTrail navigation in two sizes.
ButtonVariants, sizes, shapes, icons and loading state.
ButtonGroupJoins a small set of tightly-coupled buttons into a single control (split buttons).
CheckboxSingle checkbox and indeterminate state.
ClipboardTextRead-only text field with a one-click copy-to-clipboard button.
Code / CodeHighlightedEscaped Pygments grammar highlighting, multiline tokens, line numbers, highlighted lines and localized copying.
CollapsibleComposable disclosure component for showing and hiding content.
Combobox / AutocompleteKumo-styled input/button triggers, search inside the popup, grouped local choices and debounced HTMX search.
CommandPaletteAccessible spotlight search overlay with Cmd+K keyboard shortcut.
DatePickerKumo-styled date and datetime popups; inline single, multiple and range calendars with bounds/presets.
DateRangePickerStyled popup or inline range calendar with bounds, presets and separately named form values.
DialogNative <dialog> modal with focus management.
DrawerEdge-anchored modal panel. LayerDialog has its own layered component.
DropdownMenuNative details menu with delegated keyboard navigation and dismissal.
EmptyEmpty-state placeholder with icon and action.
FieldLabel + description + error wrapper. Built into each control.
FlowDirected diagrams with branches, SVG connectors, custom anchors and responsive local scrolling.
GridResponsive CSS grid layout container with preset column configurations.
InlineCopyTextCompact borderless copy control for IDs and short values.
InputText field with label, description, leading/trailing icons.
InputAreaMulti-line textarea with auto-resize option.
InputGroupInput field with seamless prefix, suffix, and addon attachments.
LabelForm field label, optional marker and tooltip.
LayerCardCompound surface card with distinct primary and secondary layers.
LayerDialogLayered native modal with actions, forms, nested dialogs and pending states.
LinkInline, current and plain text links.
LoaderAnimated spinner in three sizes.
MenuBarCompact view selector using native radio semantics.
MeterProgress bar showing a measured value within a known range.
PaginationA page navigation component for navigating through paginated content.
PopoverAnchored popup with interactive content.
RadioRadio group, vertical and horizontal.
SelectKumo-styled Alpine dropdown with groups, rich options and multiple selection; native form values and fallback.
SensitiveInputPassword/secret input with reveal toggle and copy button.
SeparatorHorizontal / vertical rules with optional label.
SidebarRecursive navigation groups, account/zone views, search, loading, resize and full-screen mobile breadcrumbs.
SkeletonShimmer placeholder.
SliderInset single/range selection with vertical grips, value badges, endpoint labels and native keyboard/form behavior.
SurfaceBordered surface panel with header slot.
SwitchOn/off toggle in three sizes.
TableServer-driven table: sort, filter, select and paginate the full dataset.
Table of contentsPage section navigation tree with active link indicator.
TabsSegmented and underline tabs with a sliding indicator, responsive overflow and local Alpine or URL/HTMX state.
TagInputMulti-value badge input for tags and recipients.
TextHeading, body, secondary, success, error and mono text.
ToastyExpandable notification stack with variants, paused expiry and a live region.
ToolbarCompact grouped controls with shared borders, composed filters and keyboard arrow navigation.
TooltipHover and focus tooltip with grouped delays, Escape dismissal and viewport bounds.
↑ ↓ Navigate ↵ Select