Contributing

How components and composables document themselves, and what this site reads to build the pages you are looking at.

Documenting components

Component and prop docs live only in source, as JSDoc. The docs site (apps/ground) reads it straight off these files at build time and generates the description and the props table from it — don’t duplicate either anywhere else.

Two things are read, both already conventions Svelte and JSDoc give you for free:

  1. The component description — the <!-- @component --> comment above <script>, which is also what an editor shows on hover in a consuming app.
  2. The props table — the @typedef {Object} <Name>Props block above $props(), one @property per prop. Defaults are not read from the prose: the docs site parses the $props() destructuring itself, so a documented default can never drift from the applied one.

If a component’s page renders with no description or an empty table, that file is missing one of the two blocks below — start there.

1. Component-level docs — @component comment

An HTML comment above <script>, outside it. Supports Markdown.

<!--
@component
Filled button for triggering actions. Use `default` for the primary action on a surface,
`secondary`/`ghost` for progressively lower emphasis, `destructive` for dangerous
actions, and `link` for navigation-style actions.
-->
<script>
  ...
</script>

2. Prop docs — @typedef + @type above $props()

<script>
  /**
   * @typedef {Object} ButtonProps
   * @property {import('svelte').Snippet} children - Button content.
   * @property {'default'|'destructive'|'secondary'|'ghost'|'link'} [variant] - Visual emphasis of the button.
   * @property {boolean} [disabled] - Disables the button (forwarded via `...rest`).
   */

  /** @type {ButtonProps} */
  let { children, variant = 'default', ...rest } = $props();
</script>
  • One @typedef per component, named <ComponentName>Props.
  • One @property per prop, in destructuring order. Optional props get [name].
  • Snippet props: import('svelte').Snippet, or Snippet<[Arg]> for parameterized snippets.
  • Props only reachable via ...rest (native attributes forwarded to the underlying element, e.g. disabled, aria-*) are still documented as @property entries for discoverability, even though they aren’t destructured by name.
  • A shape reused by sibling files in a compound family (e.g. a shared controller object) gets one @typedef in the file that owns it, referenced elsewhere via @import. Don’t duplicate the same typedef across sibling files. The docs site renders those as their own tables, so DataTableColumn and DataTableController are documented once and shown beside the component that takes them.

Reference implementation

See src/components/button/button.svelte for the full pattern applied.

Documenting composables

Same rules, one JSDoc block above the export: prose for what it is and when to reach for it, @param per argument, @returns for what comes back. The shape of a returned controller or an options object gets a @typedef {Object} in the same file, which the docs site renders as a table.

/**
 * @typedef {Object} ThemeController
 * @property {'light'|'dark'} resolved - The scheme actually in effect.
 */

/**
 * Colour-scheme controller for an app shell.
 *
 * @param {'system'|'light'|'dark'} [fallback] - Scheme to use on a first visit only.
 * @returns {ThemeController}
 */
export function createTheme(fallback = 'system') {}

State-holding composables (use-*.svelte.js) are a private class with Svelte 5 runes as public fields ($state, $derived) — no manual getter/setter methods for what a rune field already gives you for free — wrapped in an exported createX() factory (createTheme, createTextScale, createMediaQuery). The class never leaves its own module: consumers and every other file in this package call the factory, never new. This keeps composables consistent with components, which never expose a class either.

DOM actions (use: directives — scrollFade, lightSource, countUp, scrollReveal) are the one exception: Svelte’s action API requires a plain function returning { destroy, update }, so they stay function-shaped by contract, not by choice. flyAndScale is a Svelte transition rather than an action, and is documented as such.

Examples live in the docs site

A component’s examples are apps/ground/src/lib/examples/<component>.examples.svelte: an <Example> per case, with the markup inside a template snippet. The site slices each snippet’s source out of that file at build time, so the code shown on the page is the code that ran — there is nothing to keep in sync.

They are curated rather than exhaustive. Aim for three to six per component, grouping variants into one example instead of giving each its own; a page with twelve near-identical demos is harder to read than one with five that each make a point. Prose on an example says why you would reach for this shape, not what the props do — that is the API table’s job.

Two Svelte-specific traps when writing one:

  • A { inside an attribute value is an expression, not text. Anything with braces or double quotes in it goes in expression form: description={'value=for …'}.
  • The name is the key the source is looked up by, so it has to be unique within the file.

Foundations

The token pages (colours, type scale, spacing) are generated from theme.css by apps/ground/src/lib/server/tokens.js — there is no hand-kept list to update. A new --color-* token shows up as a swatch on its own; give it an entry in that file’s ROLES map so it arrives with a description of what it is for.