AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Discourse Theme Authoring

skill-discourse-discourse-theme-skills-discourse-theme-authoring · by discourse

Use when creating, developing, or modifying Discourse themes and theme components — covers scaffolding, SCSS architecture, BEM CSS, viewport design, localization, settings, icons, value transformers, theme modifiers, and CSS variables

No reviews yet
0 installs
20 views
0.0% view→install

Install

$ agentstack add skill-discourse-discourse-theme-skills-discourse-theme-authoring

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-discourse-discourse-theme-skills-discourse-theme-authoring)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Discourse Theme Authoring? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Discourse Theme Authoring

Themes and theme components are the primary way to customize Discourse's appearance and behavior without modifying core code. Themes can include SCSS stylesheets, JavaScript/Glimmer components, locale files, settings, and assets.

For detailed reference data, see the sibling files in this directory:

  • css-variables.md — All ~400 CSS custom properties
  • icons.md — Default icon list and icon system details
  • transformers.md — All value and behavior transformers

Scaffolding a New Theme

Always use the discourse_theme CLI to scaffold a new theme or component. Never create theme files manually unless the user explicitly asks.

First, confirm the details with the user:

  1. Theme name (defaults to directory name)
  2. Is this a component? (Y/n — default Y, answer n for a full theme)
  3. Author (defaults to "Discourse")
  4. Description
  5. Start watching? (Y/n — usually n during development)

Then run with piped answers:

printf 'Theme Name\nn\nAuthor Name\nTheme description here\nn\n' | discourse_theme new /path/to/my-theme

It clones from the discourse-theme-skeleton repo, customizes files based on the answers, initializes git, and runs pnpm install.

Generated Structure

my-theme/
├── about.json                  # Theme metadata (required)
├── settings.yml                # Theme settings schema
├── locales/
│   └── en.yml                  # i18n translations
├── javascripts/
│   └── discourse/
│       └── api-initializers/
│           └── my-theme.gjs    # Theme initializer
├── common/
│   └── common.scss             # Common styles entry point
├── stylesheets/                # Additional SCSS files (importable)
├── assets/                     # Static assets (images, fonts)
├── spec/                       # Ruby system tests
├── test/                       # JS acceptance tests
├── .discourse-compatibility    # Version mapping
├── eslint.config.mjs
├── stylelint.config.mjs
├── .prettierrc.cjs
├── .template-lintrc.cjs
└── package.json

about.json

{
  "name": "My Theme",
  "component": false,
  "authors": "Discourse",
  "about_url": null,
  "license_url": null,
  "theme_version": "0.0.1",
  "minimum_discourse_version": null,
  "maximum_discourse_version": null,
  "assets": {},
  "color_schemes": {},
  "modifiers": {},
  "screenshots": []
}

For theme components, set "component": true and remove color_schemes.

Color Schemes

Full themes can define color schemes in about.json. These appear in the admin color scheme picker and set the core color variables:

{
  "color_schemes": {
    "My Light Theme": {
      "primary": "333333",
      "secondary": "ffffff",
      "tertiary": "0088cc",
      "quaternary": "e45735",
      "header_background": "ffffff",
      "header_primary": "333333",
      "highlight": "ffff4d",
      "danger": "e45735",
      "success": "009900",
      "love": "fa6c8d"
    },
    "My Dark Theme": {
      "primary": "dddddd",
      "secondary": "222222",
      "tertiary": "0088cc",
      "quaternary": "e45735",
      "header_background": "111111",
      "header_primary": "dddddd",
      "highlight": "ffff4d",
      "danger": "e45735",
      "success": "009900",
      "love": "fa6c8d"
    }
  }
}

Color values are hex without the # prefix. These 10 base colors generate the full set of color scale variables (--primary-low, --tertiary-hover, etc.).

To restrict the color scheme picker to only this theme's schemes, use:

{
  "modifiers": {
    "only_theme_color_schemes": true
  }
}

SCSS Architecture

Follow this pattern for all themes. No code in common.scss — only imports. Every top-level folder in stylesheets/ has an _index.scss file.

Directory Structure

common/
└── common.scss          # Only imports, no rules
stylesheets/
├── brand/
│   ├── _index.scss      # @import "fonts"; @import "colors";
│   ├── fonts.scss
│   └── colors.scss
├── app/
│   ├── _index.scss      # @import "variables"; @import "header"; ...
│   ├── variables.scss
│   ├── header.scss
│   ├── sidebar.scss
│   ├── buttons.scss
│   └── topic-list.scss
├── blocks/
│   ├── _index.scss      # @import "block-hero"; @import "block-featured-list"; ...
│   ├── block-hero.scss
│   └── block-featured-list.scss
└── layouts/
    ├── _index.scss      # @import "homepage"; @import "sidebar-discovery";
    ├── homepage.scss
    └── sidebar-discovery.scss

common.scss

@use "lib/viewport";
@import "brand";
@import "app";
@import "blocks";
@import "layouts";

Rules:

  • common.scss imports top-level folders only, never individual files
  • Each folder's _index.scss imports all files within that folder
  • Adding a new file only requires updating that folder's _index.scss
  • Only _index.scss files use the underscore prefix — regular SCSS files do not

BEM CSS Conventions

Discourse uses a modified BEM variant for CSS. All theme styles must follow these conventions.

Syntax

  • Block: A standalone component. One BEM block per Ember component. Example: .topic-card
  • Element: A part of a block, joined with __. Example: .topic-card__title
  • Modifier: Changes appearance, written as a separate -- prefixed class. Example: --featured

Nesting in SCSS

Use & to visually nest elements under their block. This keeps related styles together and makes them easy to collapse:

.topic-card {
  display: flex;
  gap: var(--space-3);

  &__title {
    font-size: var(--font-up-1);
    font-weight: 700;
  }

  &__excerpt {
    color: var(--primary-medium);
    line-height: var(--line-height-large);
  }

  &__meta {
    display: flex;
    gap: var(--space-2);
  }

  // Modifier applied directly to element
  &__title.--highlighted {
    color: var(--tertiary);
  }

  // Modifier on the block affects children
  &.--compact {
    gap: var(--space-1);
  }

  // Indirect modifier: style children when block has modifier
  .--featured & {
    border-left: 3px solid var(--tertiary);
  }
}

State Prefixes

For conditional states, use is- and has- prefixes:

.sidebar-panel {
  &.is-open { display: block; }
  &.is-collapsed { display: none; }
  &.has-errors { border-color: var(--danger); }
}

HTML Usage


  Title
  ...
  ...

Key Rules

  • One BEM block per reusable component
  • Blocks can contain other blocks
  • Elements belong to their block only — never use .block-a__element inside .block-b
  • Modifiers are standalone classes (--modifier), not chained to the element name
  • Prefer indirect modifiers (on the parent block) over repeating modifiers on every child

Viewport Library

Never use raw CSS media queries. Use the viewport library from Discourse core.

SCSS Mixins

If common.scss already has @use "lib/viewport", all @imported files inherit the namespace — no per-file import needed. Otherwise, add @use "lib/viewport" at the top of the file that uses it.

@include viewport.from(lg) {
  // Applies at lg (1024px) and larger
}

@include viewport.until(sm) {
  // Applies below sm (640px)
}

@include viewport.between(sm, md) {
  // Applies between sm (640px) and md (768px)
}

Breakpoints

| Breakpoint | Size | Pixels (at 16px base) | |------------|--------|-----------------------| | sm | 40rem | 640px | | md | 48rem | 768px | | lg | 64rem | 1024px | | xl | 80rem | 1280px | | 2xl | 96rem | 1536px |

JavaScript (Advanced Cases)

For conditional rendering in components, use the capabilities service:

import Component from "@glimmer/component";
import { service } from "@ember/service";

class MyComponent extends Component {
  @service capabilities;

  
    {{#if this.capabilities.viewport.lg}}
      Only shown on lg and larger
    {{/if}}
  
}

Touch Detection

html.discourse-touch {
  // Touch devices (phones, tablets, touch laptops)
}

html.discourse-no-touch {
  // Non-touch devices
}

Localized Strings

Locale File Structure

locales/en.yml:

en:
  theme_metadata:
    description: "My theme description"
    settings:
      my_setting: "Description of my_setting"
  homepage:
    welcome: "Welcome to our community"
    topic_count:
      one: "%{count} topic"
      other: "%{count} topics"

Using Theme Strings in Templates

themePrefix is automatically injected in .gjs files — no import needed. It resolves the key under the theme's namespace (theme_translations.{themeId}.{key}).

import { i18n } from "discourse-i18n";

  {{! Theme string via themePrefix }}
  {{i18n (themePrefix "homepage.welcome")}}

  {{! Pluralized theme string }}
  {{i18n (themePrefix "homepage.topic_count") count=@topicCount}}

  {{! Passing theme string key to a component }}
  

Using Core Strings

Reference core i18n strings directly without themePrefix:

import { i18n } from "discourse-i18n";

  {{! Core string — no themePrefix }}
  {{i18n "topic.create"}}

Theme Settings

Settings are defined in settings.yml at the theme root. Type is auto-detected from the default value unless explicitly set.

Setting Types

Integer
max_items:
  type: integer
  default: 5
  min: 1
  max: 20
Float
opacity:
  type: float
  default: 0.8
  min: 0.0
  max: 1.0
String
heading_text:
  default: "Welcome"

custom_css_class:
  default: ""
  min: 0
  max: 50

bio_text:
  default: ""
  textarea: true
Bool
show_banner: true

show_sidebar:
  default: false
Enum
layout_style:
  type: enum
  default: grid
  choices:
    - grid
    - list
    - cards
List

Pipe-separated values. The list_type controls the admin UI widget.

| list_type | UI Widget | Description | |-------------|-----------|-------------| | (omitted) | Text field | Plain text input, pipe-separated | | compact | Dropdown with choices | Select from predefined choices | | simple | Tag-style input | Add/remove free-form text items | | category | Category selector | Pick categories from the site | | tag | Tag chooser | Pick tags from the site | | group | Group selector | Pick groups from the site | | emoji | Emoji picker | Pick emojis |

# Plain list (default)
featured_tags:
  type: list
  default: "announcements|support|feedback"

# Compact list with predefined choices
layout_options:
  type: list
  list_type: compact
  default: "grid|list"

# Category picker
homepage_categories:
  type: list
  list_type: category
  default: ""

# Tag picker
featured_tags:
  type: list
  list_type: tag
  default: ""

# Simple (tag-style free-form input)
contact_fields:
  type: list
  list_type: simple
  default: "email|phone"
Upload

Stores a file upload. The value is a CDN URL in JavaScript.

hero_image:
  type: upload
  default: ""
Objects

Structured data with schema validation. Stored as JSON. Max 0.5 MB.

navigation_links:
  type: objects
  default:
    - label: "Home"
      url: "/"
      icon: "house"
    - label: "About"
      url: "/about"
      icon: "circle-info"
  schema:
    name: link
    properties:
      label:
        type: string
        required: true
        validations:
          min_length: 1
          max_length: 100
      url:
        type: string
        required: true
        validations:
          url: true
      icon:
        type: string

Object property types: string, integer, float, boolean, datetime, upload, enum, categories, groups, tags, objects (nested).

Validations by type:

  • string: min_length, max_length, url (boolean)
  • integer/float: min, max
  • categories/groups/tags: min, max (item count)

Accessing Settings in JavaScript

The settings object is automatically injected in theme .gjs files:

// In an api-initializer:
const heroImage = settings.hero_image;
const links = settings.navigation_links;

Setting Descriptions in Locales

en:
  theme_metadata:
    settings:
      show_banner: "Toggle the hero banner on the homepage"
      navigation_links: "Configure the navigation links"

Theme Modifiers

Modifiers are declared in the modifiers key of about.json. They affect server-side behavior.

Boolean Modifiers

| Modifier | Default | Description | |----------|---------|-------------| | serialize_topic_excerpts | false | Include excerpts when serializing topic lists | | custom_homepage | null | Enable custom homepage for this theme | | serialize_topic_op_likes_data | null | Include OP likes data in topic serialization | | serialize_topic_is_hot | null | Include "is hot" status in topic serialization | | only_theme_color_schemes | null | Restrict color scheme picker to this theme's schemes |

String Array Modifiers

| Modifier | Description | |----------|-------------| | csp_extensions | Additional Content Security Policy directives | | svg_icons | Icon names to include in the icon subset | | serialize_post_user_badges | Badge names to serialize alongside post data |

Complex Modifiers

| Modifier | Description | |----------|-------------| | topic_thumbnail_sizes | Additional thumbnail resolutions (format: ["800x600"]) |

Example

{
  "modifiers": {
    "serialize_topic_excerpts": true,
    "custom_homepage": true,
    "svg_icons": ["star", "rocket", "chart-line", "far-comments"],
    "topic_thumbnail_sizes": ["400x300"]
  }
}

Setting-Dependent Modifiers

Pull modifier values from a theme setting:

{
  "modifiers": {
    "serialize_topic_excerpts": {
      "type": "setting",
      "value": "enable_excerpts"
    }
  }
}

Font Awesome Icons

Discourse uses Font Awesome 6 Free (solid, regular, brands) plus custom Discourse icons. See icons.md for the full default icon list.

Icon Name Conventions

| Style | Prefix | Example | |-------|--------|---------| | Solid | (none) | heart, star, house | | Regular | far- | far-heart, far-star | | Brands | fab- | fab-github, fab-discord | | Discourse custom | discourse- | discourse-sparkles |

Adding Non-Default Icons

For any icon not in the default set, add it to svg_icons in about.json:

{
  "modifiers": {
    "svg_icons": ["chart-line", "wand-magic-sparkles", "far-face-grin"]
  }
}

Custom SVG Icons

Upload a custom SVG sprite file as a theme asset with variable name icons-sprite. Icons are registered using their `` IDs.

Icon Replacement

Replace icons globally via the plugin API:

api.replaceIcon("heart", "thumbs-up");

Value and Behavior Transformers

Transformers let themes modify values and behavior used by core components. See transformers.md for the full list.

Value Transformers

Register in an api-initializer. The callback receives the current value and context:

api.registerValueTransformer("transformer-name", ({ value, context }) => {
  return transformedValue;
});

Commonly used value transformers for theming:

| Transformer | Use Case | |-------------|----------| | home-logo-href | Change where the logo links | | home-logo-image-url | Swap the logo image | | topic-list-columns | Add/remove/reorder topic list columns | | topic-list-item-class | Add CSS classes to topic list rows | | post-menu-buttons | Customize post action buttons | | navigation-items | Modify top navigation tabs | | create-topic-label | Change the "New Topic" button text | | category-display-name | Customize category name rendering |

Behavior Transformers

Wrap core logic with chainable callbacks. Call next() to continue the chain; omit it to override:

api.registerBehaviorTransformer("topic-list-item-click", ({ next, context

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [discourse](https://github.com/discourse)
- **Source:** [discourse/discourse-theme-skills](https://github.com/discourse/discourse-theme-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.