name: gts-component-conventions
description: Styling and authoring conventions for .gts components and their CSS in the host and boxel-ui packages, plus the content-tag <template>-detection hazards that silently break .gts parsing. Use whenever writing, reviewing, or refactoring a .gts component, a <style scoped> block, or a glimmer template — especially component-styling review passes ("apply these design-review notes", "clean up this component's CSS") and when a .gts file mysteriously fails to type-check or lints with phantom unused-import errors. Triggers on editing component markup/CSS, adding SVG icons, writing conditional class names, choosing colors or units, and any cascading "Cannot find name 'template'" or template-was-dropped symptom.
.gts Component Conventions
Two things live here:
- Design-review guidelines (from Burcu) for component markup and CSS — apply when writing or reviewing any
.gtscomponent or<style scoped>block. - content-tag
<template>hazards — the parser gotchas that silently drop or mangle a template. Read part 2 the moment a.gtsfile fails to type-check or lints with phantom errors after an edit.
Root font-size is the browser default 16px, so 1rem === 16px. There is no html { font-size } override.
Part 1 — Component & CSS guidelines
1. Target DOM elements with data-* attributes, not class names
Class names are a styling concern; JS that reaches into the DOM (closest, querySelector, matches, hasAttribute) should key off a data-* attribute so refactoring CSS classes never breaks behavior. Keep the class for the <style> selector and add a data-* marker for the JS hook.
// Wrong — JS coupled to a styling class
let marker = el.closest('.adorn-context');
// Right — class stays for CSS; data attribute is the JS contract
let marker = el.closest('[data-adorn-context]');
<div class='adorn-context' data-adorn-context ...attributes>
data-test-* attributes (used by the test suite) already follow this — extend the same habit to runtime DOM lookups.
2. Use scalable units (rem) instead of px
Convert dimensional CSS values — width/height, padding, margin, gap, border-radius, font-size, box-shadow spreads, clip-path offsets, positioning insets — to rem (divide px by 16). Prefer the existing --boxel-sp-*, --boxel-font-*, and radius tokens when one fits.
gap: 0.3125rem; /* was 5px */
padding: 0.1875rem 0.4375rem; /* was 3px 7px */
font-size: 0.625rem; /* was 10px */
box-shadow: 0 0 0 0.125rem var(--c); /* was 2px */
Leave as-is: SVG-internal coordinates (viewBox, cx, r, stroke-width, path d), and JS pixel math against getBoundingClientRect() — those aren't CSS layout units. Hairline values like letter-spacing: 0.5px are fine to leave (converting gains nothing).
3. Save hardcoded colors as CSS variables
Never ship a raw hex/rgb in a component. If a color recurs across components, promote it to a shared token in packages/boxel-ui/addon/src/styles/variables.css; if it's truly local, define a component-scoped custom property. Falling back to another variable is fine (var(--token, var(--other))); a hardcoded literal fallback is not.
Reuse an existing semantic token before inventing a new one, and name tokens by role, not by hue. A color named for its appearance (--boxel-teal-ink, --boxel-dark-teal) is a palette primitive; a color named for its job (--boxel-highlight, --boxel-highlight-hover) is what components should reference.
For "readable text/icons on a colored surface," the codebase uses the pervasive <surface> / <surface>-foreground pairing (shadcn/Tailwind-style): --foreground, --muted-foreground, --primary/--primary-foreground, --accent-foreground, --card-foreground, plus component-local --boxel-*-foreground. The idiom is color: var(--primary-foreground, var(--boxel-dark)).
The adorn refactor therefore landed on existing/semantic tokens rather than hue-named ones:
- darker teal for hover/selected →
--boxel-highlight-hover(resolves through--boxel-dark-teal: #00da9f). Don't add a parallel "teal-hover" variable. - dark foreground on a highlight surface →
--boxel-highlight-foreground: #0a2e1c(the companion to--boxel-highlight/--boxel-highlight-hover, following the-foregroundconvention).
/* role-named, not hue-named or hardcoded */
color: var(--boxel-highlight-foreground); /* was #0a2e1c */
background-color: var(--boxel-highlight-hover); /* was #00da9f */
4. Use the cn helper for conditional class names
Don't hand-concatenate classes with inline {{if}}/{{unless}} inside a class string. Use cn from @cardstack/boxel-ui/helpers — positional base classes, named boolean classes.
{{! Wrong }}
<div class='adorn-label {{if @compact "compact"}} {{unless (has-block "dropdown") "no-menu"}}'>
{{! Right }}
<div class={{cn 'adorn-label' compact=@compact no-menu=(unless (has-block 'dropdown') true)}}>
cn emits the same space-separated string, so this is behavior-preserving — existing data-test/class selectors keep matching.
5. SVGs: keep them separate, stroke/fill with currentColor
- Factor SVG artwork into dedicated icon components (the repo convention — e.g.
selection-checkmark-icon.gts) or@cardstack/boxel-icons/@cardstack/boxel-ui/icons, rather than duplicating raw<svg>markup. Ship compressed/optimized SVG. - Make the themeable parts
stroke='currentColor'/fill='currentColor'so a parent can color the icon viacolor:. Parts that are intentionally a fixed brand color (e.g. a dark circle behind a themeable check) reference a token instead of a literal.
{{! themeable check follows the parent's color }}
<path d='…' stroke='currentColor' />
{{! fixed dark disc → token, not a hex literal }}
<circle cx='7' cy='7' r='7' fill='var(--boxel-highlight-foreground)' />
6. Use --boxel-highlight, not --boxel-teal, for the default highlight
--boxel-highlight resolves to --boxel-teal today, but it's the app-wide semantic token for the highlight accent. Referencing it keeps highlight color consistent and re-themeable across the app. (Same idea for --boxel-highlight-hover.)
/* prefer the semantic token, not the raw palette color */
--adorn-accent-light: var(--boxel-highlight); /* not var(--boxel-teal) */
background-color: var(--boxel-highlight); /* not var(--boxel-teal) */
Part 2 — content-tag <template> hazards
content-tag (the preprocessor glint and ember-eslint-parser use to parse .gts) has JavaScript-lexer bugs that make it lose track of <template> tags. When that happens the template is silently dropped or misparsed — and the symptom is not where the bad character is. AGENTS.md (§ ".gts file gotcha") is the canonical list; the known triggers:
1. Backticks inside a regex literal — mistaken for template-literal delimiters.
.replace(/`([^`]+)`/g, '$1') // BROKEN
const INLINE_CODE_RE = new RegExp('`([^`]+)`', 'g'); // FIX
2. !/regex/ (negation before a regex literal) — the / after ! is misread.
lines.some((line) => !/^\s*#/.test(line)); // BROKEN
const HEADING_RE = /^\s*#/; // FIX — extract to a const
lines.some((line) => !HEADING_RE.test(line));
3. A backtick-wrapped bracket token inside the template body — e.g. a <style> CSS comment.
<template>
<style scoped>
/* BROKEN: backtick-wrapped `[data-adorn-context]` here drops the template */
/* FIX: drop the backticks or the brackets in template-region comments */
</style>
</template>
The identical text in a // comment outside <template>…</template> is harmless — content-tag only runs its template-mode lexer between the tags. So keep backtick-wrapped selectors like `[data-foo]` out of comments inside the template; describe the marker in a regular JS comment above the component instead.
Recognizing it
- Outside-template triggers (1 & 2): cascading TypeScript errors beginning with
Cannot find name 'template'at the first<template>in the file. - Inside-template trigger (3): the template body is silently dropped, so type-check may still pass while ESLint reports phantom
no-unused-varson imports/consts that the template references (e.g.hash, a yielded const). ESLint is the canary here — if a refactor that only touched a<template>/<style>block suddenly makes prior imports "unused," suspect a swallowed template before you touch the imports.
Bisecting
Revert to the last-good file and re-apply changes one hunk at a time, running npx eslint <file> between each, until the phantom errors reappear. The offending hunk is almost always a comment or string edit inside the <template> block, not a code change.