phoenix-duskmoon-design
Phoenix 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.
More from gsmlg-dev/code-agent
elixir-architect
Use when designing or architecting Elixir/Phoenix applications, creating comprehensive project documentation, planning OTP supervision trees, defining domain models with Ash Framework, structuring multi-app projects with path-based dependencies, or preparing handoff documentation for Director/Implementor AI collaboration
17flutter-reducing-app-size
Measures and optimizes the size of Flutter application bundles for deployment. Use when minimizing download size or meeting app store package constraints.
17flutter-animating-apps
Implements animated effects, transitions, and motion in a Flutter app. Use when adding visual feedback, shared element transitions, or physics-based animations.
17flutter-managing-state
Manages application and ephemeral state in a Flutter app. Use when sharing data between widgets or handling complex UI state transitions.
17flutter-handling-concurrency
Executes long-running tasks in background isolates to keep the UI responsive. Use when performing heavy computations or parsing large datasets.
16flutter-caching-data
Implements caching strategies for Flutter apps to improve performance and offline support. Use when retaining app data locally to reduce network requests or speed up startup.
16