This attempts to define some useful rules for LLMs to write better HTML and CSS for Discourse... the basics covered include: **Naming (BEM)** - `block__element` + standalone `.--modifier` (two-dash); `is-`/`has-` state classes - Name by meaning, not appearance (no position/color/size in names) - Don't interpolate user input into class names — use data attributes - Prefer a state class over `:has()` **Color & theming** - Never hardcode color; use the custom-property palette; no separate dark-mode block - `--token-color-*` for standard UI, palette for bespoke; `--d-*` design vars - Don't rely on color alone; WCAG AA contrast; forced-colors awareness **CSS authoring** - Native CSS over compile-time SASS; keep `z()`, `lib/viewport`, `&` nesting - Low specificity; avoid `!important` (comment when unavoidable) - `em`/`rem` + flexible sizing + `overflow-wrap`; `gap` over margins - Local custom properties for reuse/`calc()`; mobile-first, intrinsic layout - RTL via logical properties - Shared mixins (`ellipsis`, `line-clamp`, `d-animation`) - Style with restraint — minimal styling, leave aesthetics to themes **Accessibility** - Semantic/landmark markup; heading levels by outline - Icon-only labels; `.sr-only` (not `display:none`); live regions via the `a11y` service - `:focus-visible`; `prefers-reduced-motion`; animate `transform`/`opacity` - Design to work without hover **Templates** - Escape by default (XSS); `dIcon` with valid sprite icons; translatable strings - `<DButton>` variants; FormKit for forms; `...attributes` on root; `dConcatClass` - `<PluginOutlet>` (don't add speculatively); avoid div-itis & empty containers **Stylesheet placement** - `common/` responsive stylesheets (`desktop/`/`mobile/` deprecated); plugin/theme registration - Lint with `bin/lint --fix` Also fixed an issue with two docs having the same index and cleaned up some references to align better.
4.2 KiB
Vendored
Layout & responsive reference
Companion to the Where stylesheets live section of SKILL.md. The rules live there:
write one responsive stylesheet (not desktop/mobile copies), prefer intrinsic layout, and use
lib/viewport for breakpoints. This file is the detail.
The authoritative philosophy is
docs/developer-guides/docs/03-code-internals/27-designing-for-devices.md:
design mobile-first, then enhance for larger viewports and richer input — read it for the
full picture.
Prefer intrinsic layout over breakpoints
Reach for a breakpoint only when a layout genuinely needs to restructure. For sizing and wrapping, prefer intrinsic, self-adjusting CSS that responds to available space on its own — it adapts at every width, not just at the breakpoints you happened to pick, and it's far less code to maintain.
// GOOD — one rule, fills and wraps columns to fit any container width
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(14em, 1fr));
gap: 1em;
}
// AVOID — redefining the column count at each breakpoint
.card-grid {
grid-template-columns: 1fr;
@include viewport.from(sm) { grid-template-columns: repeat(2, 1fr); }
@include viewport.from(lg) { grid-template-columns: repeat(3, 1fr); }
@include viewport.from(xl) { grid-template-columns: repeat(4, 1fr); }
}
Other intrinsic tools to favor before media queries: flex-wrap with flex basis/grow,
min()/max()/clamp() for fluid sizing, min-content/max-content/fit-content, and
auto-fit/auto-fill + minmax(). Use lib/viewport breakpoints for the cases intrinsic
layout can't express — e.g. moving a sidebar from beside the content to below it, or swapping
flex-direction.
Responsive breakpoints — lib/viewport
Make a component responsive with the standardized lib/viewport module rather than ad-hoc
media queries or the legacy breakpoint() mixin. @use it at the top of the file, then use
the from / until / between mixins:
@use "lib/viewport";
.my-component {
flex-direction: column; // mobile-first default
@include viewport.from(md) {
flex-direction: row; // wider viewports
}
&__sidebar {
@include viewport.until(lg) {
display: none;
}
}
}
Standard breakpoints
(app/assets/stylesheets/lib/viewport.scss):
| Name | Width |
|---|---|
sm |
40rem |
md |
48rem |
lg |
64rem |
xl |
80rem |
2xl |
96rem |
viewport.from($bp)→width >= $bp(min-width);viewport.until($bp)→width < $bp(max-width);viewport.between($from, $until)→ a bounded range.- Prefer a mobile-first default with
from()to scale up. Always use a named breakpoint — never a rawpx/remmedia query — so breakpoints stay consistent sitewide.
For viewport-conditional rendering in a component (rather than CSS), use the capabilities
service — this.capabilities.viewport.lg, etc. — but SCSS is the recommended way to handle
layout differences.
Touch & hover
Some devices have only a touchscreen, some only a pointer, some both — and touch users cannot
hover. So design interfaces to work entirely without hover, and add hover only as an
enhancement. When you do add hover styling, scope it to non-touch devices via the
html.discourse-no-touch class (Discourse adds .discourse-touch / .discourse-no-touch to
<html> based on (any-pointer: coarse)):
html.discourse-no-touch .my-component__reveal-on-hover {
opacity: 0;
&:hover { opacity: 1; }
}
In components, the same info is on the capabilities service (this.capabilities.touch).
Legacy device modes (deprecated)
Discourse historically shipped separate mobile/desktop stylesheets and layouts switched by user-agent. All of these are deprecated and being removed — don't use them in new code:
- the
desktop/andmobile/stylesheet directories, - the
.mobile-view/.desktop-viewHTML classes, - the
site.mobileViewboolean in JS.
Replace them with the viewport breakpoints and capabilities service above. ("Mobile mode"
will become an alias for "viewport width < sm" for backwards compatibility.)