About phoenix-duskmoon-design
A design skill for building Phoenix pages with the DuskMoon design system. It gives developers conventions for themes, components, color, typography, layout, and motion.
Key features
- 01Theme setup and switching
- 02Component color conventions
- 03Token-based color rules
- 04Typography and layout guidance
- 05Surface depth and motion rules
Use cases
- 01Planning a Phoenix page’s visual direction
- 02Applying consistent DuskMoon component colors
- 03Designing forms, navigation, and data views
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ gsmlg-dev/ code-agent --skill phoenix-duskmoon-designAfter installing, type/phoenix-duskmoon-designin Claude Code to confirm it loaded.
Details
From the project READMEPhoenix DuskMoon Design Conventions
Design Thinking
Before building any page or view, commit to an intentional aesthetic direction within the DuskMoon design system:
- Purpose: What does this page do? Dashboard, form, marketing, admin, content?
- Density: Dense data UI vs generous editorial spacing — pick one per view
- Emphasis: Identify the single most important action or content block — that gets primary color; everything else recedes
- Depth: How many surface layers does this view need? Flat (1-2 levels) vs layered (3-4 levels)
DuskMoon provides the tokens — your job is to use them with intention, not uniformly.
Theme Setup
CSS Entry (Required)
@import "tailwindcss";
@plugin "@duskmoon-dev/core/plugin";
@import "phoenix_duskmoon/components";
HTML Root (Required)
<html lang="en" data-theme="sunshine">
Themes: sunshine (light), moonlight (dark).
Body Base
<body class="bg-surface text-on-surface">
Every page starts on surface with on-surface text. No exceptions.
Theme Switcher
Always include <.dm_theme_switcher /> in the appbar. Requires ThemeSwitcher hook.
Component Default Colors
Page Shell
| Component | Color | Class / Pattern |
|---|---|---|
| Appbar | primary | appbar appbar-primary appbar-sticky |
| Page background | surface | bg-surface on <body> |
| Sidebar | secondary | bg-secondary text-secondary-content |
| Footer | surface-container-high | bg-surface-container-high |
| Bottom nav | primary | color="primary" |
| Drawer | secondary | bg-secondary text-secondary-content |
The appbar is the brand anchor — always primary. The sidebar and drawer use secondary to create a strong navigation identity distinct from the content area. The page body is always surface. Footer uses higher surface elevation for subtle separation.
<.dm_appbar title={@page_title} sticky>
<:logo>...</:logo>
<:menu to={~p"/dashboard"} active={@active == :dashboard}>Dashboard</:menu>
<:user_profile><.dm_theme_switcher /></:user_profile>
</.dm_appbar>
<div class="flex">
<aside class="bg-secondary text-secondary-content w-64 min-h-screen p-4">
<.dm_left_menu active={@current_path}>
<:menu>...</:menu>
</.dm_left_menu>
</aside>
<main class="flex-1 p-6">
<%= @inner_content %>
</main>
</div>
<.dm_page_footer label="© 2026 MyApp" />
Actions
| Component | Default | When to Change |
|---|---|---|
| Primary button | variant="primary" | Main page action — one per view |
| Secondary button | variant="secondary" | Alternative actions |
| Tertiary button | variant="tertiary" | Accent features, special highlights |
| Destructive button | variant="error" | Delete, remove, irreversible |
| Ghost button | variant="ghost" | Toolbar, low-emphasis, cancel |
| Link button | variant="link" | Inline text actions |
| Outline button | variant="outline" | Medium emphasis, toggleable |
<div class="flex gap-2">
<.dm_btn variant="ghost">Cancel</.dm_btn>
<.dm_btn variant="primary">Save Changes</.dm_btn>
</div>
Content
| Component | Color | Notes |
|---|---|---|
| Card | surface-container | Default CSS — no class needed |
| Card (elevated) | surface-container-high | Add shadow="md" |
| Modal / Dialog | surface-container-highest | Highest elevation |
| Tooltip | primary | color="primary" default |
| Popover | surface-container-high | |
| Accordion | surface-container | |
| Bottom sheet | surface-container-high | |
| Snackbar | inverse-surface or type-based | Use type for semantic |
Data Display
| Component | Color | Notes |
|---|---|---|
| Badge | variant="primary" | Use secondary/tertiary for category differentiation |
| Chip | color="primary" | Semantic colors for status: success, error |
| Progress | color="primary" | success for completion, error for limits |
| Avatar | color="primary" | Initials placeholder color |
| Stat | no color | color="primary" for key metrics only |
| Timeline | semantic per item | completed = green, active = primary |
| Table | no color | zebra + hover for readability |
Forms
| Component | Color | Notes |
|---|---|---|
| Input | no color | Uses outline border. Set color only for emphasis |
| Checkbox | color="primary" | |
| Radio | color="primary" | |
| Switch | color="primary" | |
| Select | no color | |
| Slider | color="primary" | |
| Rating | color="primary" | warning for star ratings |
Feedback
| Component | Color | Notes |
|---|---|---|
| Alert | type-based | variant="info", "success", "warning", "error" |
| Flash | kind-based | :info, :error |
| Toast | type-based | Always set type |
| Loading spinner | variant="primary" |
Navigation
| Component | Color | Notes |
|---|---|---|
| Appbar | primary | Always |
| Navbar | surface-container-high | navbar-surface-container-high |
| Tabs | no color | Active = primary underline |
| Breadcrumb | on-surface-variant | Muted text |
| Steps | color="primary" | Completed = filled, upcoming = outline |
| Left menu | secondary | Active item = primary-content highlight |
Color Rules
Rule 1: No Hardcoded Colors
Never use hex, rgb, hsl, named colors, or Tailwind palette classes (bg-blue-500, text-slate-600). All colors from design tokens only.
<%# FORBIDDEN %>
<div style="background: #3b82f6;">
<div class="bg-blue-500 text-white">
<%# CORRECT %>
<div class="bg-primary text-primary-content">
Rule 2: Pair Background + Text
Every background token has a text counterpart. Always use them together.
| Background | Text |
|---|---|
bg-primary | text-primary-content |
bg-secondary | text-secondary-content |
bg-tertiary | text-tertiary-content |
bg-surface | text-on-surface |
bg-surface-container-* | text-on-surface |
bg-primary-container | text-on-primary-container |
bg-error | text-error-content |
bg-error-container | text-on-error-container |
bg-inverse-surface | text-inverse-on-surface |
Rule 3: Surface Elevation — Low to High
Nest surfaces from low to high. Never place lower elevation inside higher.
surface (page)
├─ secondary (sidebar, drawer)
└─ surface-container (card)
└─ surface-container-high (navbar, popover, elevated card)
└─ surface-container-highest (dialog, tooltip)
Rule 4: Semantic Colors for States Only
success, warning, error, info communicate state — never for decoration or branding.
<%# CORRECT %>
<.dm_alert variant="error">Validation failed</.dm_alert>
<.dm_badge variant="success">Active</.dm_badge>
<%# WRONG %>
<.dm_btn variant="success">Submit</.dm_btn> <%# Use primary %>
Rule 5: Primary = Brand, Secondary = Support, Tertiary = Accent
| Role | Purpose | Examples |
|---|---|---|
| Primary | Brand identity, main actions, key focus | Appbar, main CTA, active nav |
| Secondary | Supporting actions, alternative emphasis | Secondary CTA, toggles |
| Tertiary | Special highlights, decorative accents | Feature badges, accent cards |
Rule 6: One Primary Action Per View
One variant="primary" button per visible section. Supporting actions use secondary, ghost, or outline.
Rule 7: Icons Inherit Color
dm_mdi and dm_bsi default to currentcolor. Do not set explicit color= unless the icon must differ from surrounding text.
Rule 8: Container Colors for Soft Emphasis
Use primary-container / on-primary-container for tinted backgrounds with lower contrast. Good for featured sections, highlight cards, selected states.
<div class="bg-primary-container text-on-primary-container p-4 rounded-lg">
Featured content with softer emphasis
</div>
Typography
Font Strategy
DuskMoon does not prescribe a typeface — projects choose their own. Rules:
- Pick a distinctive display font for headings — avoid Inter, Roboto, Arial, system-ui. Load from Google Fonts or self-host.
- Pair with a clean body font that complements the display choice.
- Use a monospace font for code blocks — JetBrains Mono, Fira Code, or similar.
- Define fonts as CSS variables, not inline.
/* Project CSS — after DuskMoon imports */
:root {
--font-display: 'Instrument Serif', serif;
--font-body: 'DM Sans', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
body { font-family: var(--font-body); }
h1, h2, h3 { font-family: var(--font-display); }
code, pre { font-family: var(--font-mono); }
Type Scale
| Element | Class | Usage |
|---|---|---|
| Page title | text-3xl font-bold | One per page |
| Section heading | text-2xl font-semibold | |
| Card title | text-lg font-semibold | |
| Body text | text-base | Default |
| Secondary text | text-sm text-on-surface-variant | Muted supporting info |
| Caption | text-xs text-on-surface-variant | Timestamps, metadata |
Text Color Hierarchy
| Priority | Token | Usage |
|---|---|---|
| Primary text | text-on-surface | Headings, body text, labels |
| Secondary text | text-on-surface-variant | Descriptions, hints, timestamps |
| Disabled text | text-on-surface-variant with opacity-50 | Disabled states |
| Link text | text-primary | Clickable inline text |
Layout Composition
Spatial Principles
- Content pages: Generous whitespace —
p-6orp-8on main,gap-6between cards - Data views: Dense spacing —
p-4on main,gap-3between items - Consistent gaps: Pick one scale per view (
gap-4orgap-6, not mixed) - Max width: Content areas should constrain width —
max-w-7xl mx-autofor wide,max-w-3xlfor text-heavy
Card Grid
<div class="grid grid-cols-auto-fit-80 gap-6">
<.dm_card :for={item <- @items}>
<:title><%= item.name %></:title>
<%= item.description %>
</.dm_card>
</div>
Sidebar + Content
<div class="flex min-h-screen">
<aside class="bg-secondary text-secondary-content w-64 border-r border-outline-variant p-4 shrink-0">
<.dm_left_menu active={@current_path} />
</aside>
<main class="flex-1 bg-surface p-6 overflow-auto">
<%= @inner_content %>
</main>
</div>
Stacked Sections with Depth
<section class="bg-surface-container-low p-8 rounded-2xl">
<h2 class="text-2xl font-semibold mb-6">Featured</h2>
<div class="grid grid-cols-3 gap-4">
<.dm_card shadow="md">...</.dm_card>
<.dm_card shadow="md">...</.dm_card>
<.dm_card shadow="md">...</.dm_card>
</div>
</section>
Backgrounds & Depth
Surface Layering
Don't use flat pages. Build depth with surface tokens:
<body class="bg-surface text-on-surface">
<header class="bg-primary text-primary-content">...</header>
<div class="bg-surface-container-low -mt-8 pt-12 pb-8 rounded-t-3xl">
<div class="max-w-6xl mx-auto px-6">
<.dm_card shadow="lg" class="bg-surface-container">
...
</.dm_card>
</div>
</div>
</body>
Hero Sections
Use container color gradients for hero areas:
.hero {
background: linear-gradient(
135deg,
var(--color-primary-container) 0%,
var(--color-tertiary-container) 100%
);
}
Scrim & Overlays
.overlay {
background-color: hsl(var(--color-scrim) / 0.5);
backdrop-filter: blur(4px);
}
Borders & Dividers
outline-variant→ subtle dividers, decorative bordersoutline→ interactive borders (inputs, focused states)
<hr class="border-outline-variant" />
<div class="border border-outline rounded-lg">Interactive element</div>
Motion & Transitions
Theme Transition
* {
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
Card Hover
.interactive-card {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.interactive-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
Reduced Motion
Always respect user preference:
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}
Motion Principles
- One transition property per interaction — don't animate everything
- Page-level animations on route change (LiveView transitions)
- Subtle hover states on cards and buttons —
translateY(-2px)max - Loading states use
dm_loading_spinneror skeleton components — no custom spinners
Anti-Patterns
- Tailwind color scale — No
bg-blue-500,text-gray-600. Breaks theme switching. - Hardcoded colors — No hex, rgb, hsl, or named colors.
- Opacity faking surfaces — No
bg-white/80. Usesurface-container-*tokens. - Inline color styles — No
style="color: ...". - Raw HTML over components — Use
dm_*components when available. - Uniform primary everywhere — One primary action per view. Supporting actions recede.
- Semantic colors as decoration —
success/error/warning/infoare for states only. - Generic fonts — No Inter, Roboto, Arial as conscious choices. Pick distinctive typefaces.
- Missing text pairing — Every
bg-*needs itstext-*-contentortext-on-*. - Flat layouts — Use surface elevation for depth. Pages should never feel like a single flat plane.
- Timid color usage — A dominant primary with sharp accents outperforms evenly-distributed, wishy-washy palettes. Commit to the hierarchy.
- Cookie-cutter pages — Each view type (dashboard, form, content, marketing) should have a distinct spatial composition. Don't copy-paste the same layout everywhere.