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 authorsrequiredβ whentrue, the theme must provide a CSS rule for this class. Whenfalse, the class is optional (progressive enhancement)
required: true vs required: false
| Value | Meaning |
|---|---|
required: true | The module will not render correctly without this class styled. Themes must implement it. |
required: false | The 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: trueclass 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);
}