CssContract API

The CssContract is the formal agreement between a module and any theme that implements it. A module declares every CSS class it will put on its DOM elements; themes implement CSS rules targeting those classes. This separation lets you swap themes without touching module code.

Type definition

// from @karaoke-cms/contracts

export interface CssContractSlot {
  description: string;
  required: boolean;
}

export type CssContract = Record<string, CssContractSlot>;

A CssContract is a plain object where each key is a CSS class name and the value describes that class:

  • description β€” human-readable explanation for tooling and theme authors
  • required β€” when true, the theme must provide a CSS rule for this class. When false, the class is optional (progressive enhancement)

required: true vs required: false

ValueMeaning
required: trueThe module will not render correctly without this class styled. Themes must implement it.
required: falseThe class adds polish or enhancement but the module functions without it. Themes may skip it in minimal implementations.

A minimal theme only needs to implement required: true slots. A polished theme implements everything.

Where module authors declare it

Module authors place the contract in src/css-contract.ts inside their module package:

// packages/module-widget/src/css-contract.ts
import type { CssContract } from '@karaoke-cms/contracts';

export const cssContract: CssContract = {
  'widget-list':    { description: 'Outer wrapper for the list page',     required: true  },
  'widget-card':    { description: 'Individual item card in the list',     required: true  },
  'widget-detail':  { description: 'Wrapper for the single-item page',    required: true  },
  'widget-badge':   { description: 'Status badge shown on each card',      required: false },
  'widget-meta':    { description: 'Metadata row (date, author, etc.)',    required: false },
};

The contract is then passed to defineModule():

// packages/module-widget/src/index.ts
import { defineModule } from '@karaoke-cms/contracts';
import { cssContract } from './css-contract.js';

export const widget = defineModule({
  id: 'widget',
  cssContract,
  routes: (mount) => [...],
  menuEntries: (mount, id) => [...],
});

Naming convention

All class names must be prefixed with the module id followed by a hyphen:

module id: widget
class names: widget-list, widget-card, widget-detail, widget-badge ...

The /module-contract skill enforces this rule automatically.

Where themes implement it

A theme implements the contract by writing CSS rules that target those class names. There is no registration step β€” the contract is satisfied by the presence of matching CSS rules in the theme’s stylesheet.

/* packages/theme-custom/src/styles/widget.css */

/* required: true β€” must implement */
.widget-list {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-base);
  max-width: 60rem;
  margin: 0 auto;
}

.widget-card {
  background: var(--color-surface);
  border-radius: var(--radius-card);
  padding: 1.5rem;
  border: 1px solid var(--color-border);
}

.widget-detail {
  max-width: 48rem;
  margin: 0 auto;
}

/* required: false β€” optional enhancement */
.widget-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.5rem;
  border-radius: 9999px;
  font-size: 0.75rem;
  background: var(--color-primary);
  color: white;
}

.widget-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

Reading a contract from a module

When writing a theme, import the contract from the module package directly:

// In your theme package
import { cssContract as widgetContract } from '@my-org/module-widget';

// Use it to check which slots are required
const required = Object.entries(widgetContract)
  .filter(([, slot]) => slot.required)
  .map(([name]) => name);
// β†’ ['widget-list', 'widget-card', 'widget-detail']

Do not import from @karaoke-cms/astro β€” always import from the module package or from @karaoke-cms/contracts.

Validation

The /module-contract skill reads cssContract exports and validates:

  • All class names follow the {id}-* prefix convention
  • Every required: true class is implemented in any theme that declares support for the module
  • No undeclared classes are used in module markup

Run it before shipping a new module or theme:

/module-contract

Example: full contract declaration and CSS

// Module: src/css-contract.ts
import type { CssContract } from '@karaoke-cms/contracts';

export const cssContract: CssContract = {
  'newsletter-form':    { description: 'Outer <form> element',         required: true  },
  'newsletter-input':   { description: 'Email <input> field',           required: true  },
  'newsletter-button':  { description: 'Submit button',                  required: true  },
  'newsletter-success': { description: 'Success message after submit',   required: false },
  'newsletter-error':   { description: 'Error message on failed submit', required: false },
};
/* Theme: styles/newsletter.css */

/* required */
.newsletter-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-width: 28rem;
}

.newsletter-input {
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-input);
  font-size: 1rem;
}

.newsletter-button {
  padding: 0.625rem 1.25rem;
  background: var(--color-primary);
  color: white;
  border: none;
  border-radius: var(--radius-button);
  cursor: pointer;
  font-weight: 600;
}

/* optional */
.newsletter-success {
  color: var(--color-success);
  font-weight: 500;
}

.newsletter-error {
  color: var(--color-error);
}