Skip to content

@tabula-css/core

Tabula's contract-as-code layer: the frozen tables, pure functions, error codes, and JSON Schemas that every other Tabula package agrees on. Zero runtime dependencies.

Install

Most projects never install @tabula-css/core directly — it is a transitive dependency of @tabula-css/tokens, @tabula-css/registry, @tabula-css/merge, and @tabula-css/preset, all of which import their invariants from it. Install it directly only if you are writing custom tooling that needs the diagnostic catalog, the frozen tables, or the JSON Schemas without pulling in the rest of the pipeline:

bash
npm install @tabula-css/core

Overview

@tabula-css/core is the single place where the profile's vocabulary and its diagnostics are defined, so the CLI, the registry generator, the linters, and the MCP server cannot drift apart. It has no I/O and no Tailwind dependency: everything it exports is either a frozen data table (the canonical CSS slots, the namespace → utility-family map, the variant table, the banlist), a pure function (rank(), checkLaminarity(), computeSpecificity()), a JSON Schema (for tabula.config.json, a token file, registry.json, manifest.json), or the stable TAB-Exxx/TAB-Wxxx error catalog. Everything downstream — @tabula-css/tokens's validator, @tabula-css/registry's generator, @tabula-css/merge's runtime — reads these tables rather than re-deriving them.

Exports

Diagnostics

The stable diagnostic catalog every TAB-Exxx/TAB-Wxxx code in the system is drawn from — see Concepts § the registry is ground truth and Getting started for the codes in context.

ts
export type Severity = 'error' | 'warning';

export interface Diagnostic {
  readonly code: string;
  readonly severity: Severity;
  readonly message: string;
  readonly subject?: string;
  readonly hint?: string;
}

export type ErrorCode = keyof typeof ERROR_CATALOG; // e.g. 'TAB-E109' | 'TAB-E300' | …
export const ERROR_CATALOG: Readonly<Record<ErrorCode, { severity: Severity; summary: string; teach: string }>>;
export const ERROR_CODES: readonly ErrorCode[];

export function formatMessage(
  template: string,
  params?: Readonly<Record<string, string | number>>,
): string;

export function makeDiagnostic(
  code: ErrorCode,
  params?: Readonly<Record<string, string | number>>,
  opts?: { readonly subject?: string; readonly hint?: string },
): Diagnostic;

ERROR_CATALOG maps every code to its severity, a {name}-templated summary, and a one-line teach string (what tabula explain <code> prints). makeDiagnostic() is the one constructor every package in the system uses to build a Diagnostic: it looks up the code's template, fills in params via formatMessage(), and attaches an optional subject (the token path or class name the diagnostic is about) and hint (one executable next step).

ts
import { makeDiagnostic } from '@tabula-css/core';

makeDiagnostic('TAB-E109', { path: 'color.surface' });
// → { code: 'TAB-E109', severity: 'error',
//     message: "Token 'color.surface' requires a $description ≥ 20 chars, not byte-equal to its value.", … }

The catalog spans structural token-file errors (TAB-E1xx), value semantics (TAB-E15xE16x), variant closure (TAB-E170E172 config, TAB-E230 scan), build invariants (TAB-E21x), CSS governance (TAB-E22x), eject preconditions (TAB-E240/TAB-W402), the merge runtime (TAB-E3xx), generated-artifact integrity (TAB-E601), and CLI usage (TAB-E9xx) — see CSS governance for the E22x family in particular.

Frozen tables

@tabula-css/core freezes one canonical copy of every table the profile's invariants are checked against. Grouped by subject:

ExportTypePurpose
CANONICAL_SLOT_PROPERTIESreadonly string[]The ordered list of canonical longhand CSS properties; a slot's id is its index.
CANONICAL_SLOTSReadonly<Record<string, string>>slotId → property, matching registry.json's slots map.
SLOT_ALIASESReadonly<Record<string, string>>Block-logical → physical-block canonicalization (padding-block-start → padding-top).
PHYSICAL_INLINE_TWINSReadonly<Record<string, string>>Logical-inline property → its (unregistered) physical-inline twin, for the "never both" unit test.
slotIdOf(property)(string) => SlotId | undefinedThe slot id for a canonical property, or undefined.
SHORTHAND_EXPANSIONSReadonly<Record<string, readonly string[]>>CSS shorthand → longhand-leaf expansion (padding → its four sides), one level deep.
INHERITED_PROPERTIES{ tier1, tier2 }The two-tier inherited-property table: Tier 1 (closure-required: type-*/ink-* only) vs. Tier 2 (ambient, bounded).
INHERITABLE_PROPERTIESreadonly string[]Tier 1 ∪ Tier 2, in stable order — the exact set the base-level ambient baseline must set once at :root.
AMBIENT_BASELINE_DEFAULTSReadonly<Record<string, string>>Frozen literal :root defaults for every Tier-2 property.
TYPE_COMPOSITE_FIELDStuple of 8 stringsThe type composite's field names (fontFamily, fontSize, …).
TYPE_FIELD_TO_PROPERTYReadonly<Record<string, string>>Maps each type field to the CSS property it writes.
inheritanceTier(property)(string) => 0 | 1 | 2A property's inheritance tier, or 0 if it does not inherit.
NAMESPACE_FAMILY_TABLEReadonly<Record<string, NamespaceEntry>>Namespace → the utility families it synthesizes, keyed by namespace (color, spacing, size, radius, …).
ALL_FAMILIESreadonly FamilyDef[]Every family across every namespace, in familyIndex order.
NAMESPACESreadonly string[]The set of legal namespace names.
isKnownNamespace(ns)(string) => booleanWhether a namespace name is legal.
familyInLevel(family, level) / familiesForLevel(entry, level)Filters a family/namespace's entries by profileLevel (base vs strict).
familyBreadth(family)(FamilyDef) => numberNon-composed slot count + written custom-property count — the primary rank() key.
STATIC_UTILITIESReadonly<Record<string, StaticUtility>>The value-free utilities the profile admits (flex, sr-only, cursor-pointer, …), keyed by class name.
STATIC_UTILITY_NAMESreadonly string[]Every static utility's class name.
staticUtilityNamesForLevel(level)(ProfileLevel) => readonly string[]Static utility names registered under a given profile level.
VARIANT_TABLEReadonly<Record<string, VariantDef>>Every named variant (hover, md, group-hover/*, …) with its selector/condition and rank band.
CONDITION_RANK_SCALEnumber (1_000_000)Multiplier applied to a summed condition rank so conditions form outer bands.
BANLISTreadonly BanPattern[]The registry-independent ban floor (space-*, divide-*, *:, in-*, rtl:/ltr:, …).
matchBan(token)(string) => string | undefinedTests a class token against the ban floor; returns the offending ban id.
betterTailwindcssBanlist()() => { restrict: … }The ban floor shaped for eslint-plugin-better-tailwindcss's no-restricted-classes option.
QUARANTINE_ATTRIBUTE, QUARANTINE_KIND, QUARANTINE_FENCE_CLASS, QUARANTINE_SAFE_FAMILIES, QUARANTINE_SELECTOR, QUARANTINE_CSSstrings / readonly string[]The prose-quarantine boundary marker, fence class, safe utility families, and the generated exclusion CSS — see Concepts.
isQuarantineSafe(className)(string) => booleanWhether a class is legal on a quarantine boundary element.

Pure functions

ts
export function expandShorthand(property: string): readonly string[];
export function canonicalizeProperty(property: string): string;
export function isComposedValue(value: string): boolean;

export interface ClassMeta {
  readonly breadth: number;
  readonly familyIndex: number;
  readonly valueIndex: number;
}
export function rank(meta: ClassMeta): number; // throws RankError out of range
export class RankError extends Error {}
export const MAX_BREADTH: number; // 32
export const VALUE_SPAN: number; // 100_000
export const FAMILY_SPAN: number; // 10_000

export interface SlotBearing {
  readonly class: string;
  readonly slots: Iterable<number>;
  readonly atomic?: boolean;
}
export interface LaminarityViolation {
  readonly a: string;
  readonly b: string;
  readonly shared: readonly number[];
  readonly onlyA: readonly number[];
  readonly onlyB: readonly number[];
}
export function checkLaminarity(classes: readonly SlotBearing[]): LaminarityViolation[];

export type Specificity = readonly [number, number, number];
export function computeSpecificity(selector: string): Specificity;
export function compareSpecificity(a: Specificity, b: Specificity): number;
export function formatSpecificity(s: Specificity): string; // "(0,1,0)"
export function hasPseudoElement(selector: string): boolean;

rank() reduces a class's (breadth, familyIndex, valueIndex) triple to the single integer that totally orders the vocabulary (Draft A §5.2: lex(−breadth, familyIndex, valueIndex)) — see Concepts § the merge: a total order, not a cascade simulation. It throws RankError if any component is out of its supported range, since a silent overflow would corrupt the order.

ts
import { rank } from '@tabula-css/core';

rank({ breadth: 4, familyIndex: 1, valueIndex: 0 }); // a p-* class: writes 4 slots

checkLaminarity() is invariant I2 (Draft A §5.3): for classes sharing a condition, their slot sets must nest or be disjoint — never partially overlap. It returns every violating pair (empty means laminar); atomic reset utilities (sr-only) are exempt and skipped.

computeSpecificity() implements CSS Selectors Level 4 specificity without any runtime dependency, understanding :where() (always (0,0,0)), :is()/:not()/:has() (the specificity of their most specific argument), and pseudo-elements. It backs invariant I4 — every utility rule must normalize to (0,1,0) (or (0,1,1) for a pseudo-element).

Config schema & types

The tabula.config.json schema and its matching TypeScript types — see Getting started § 3. Declare your axes.

ts
export type ProfileLevel = 'base' | 'strict';
export const PROFILE_LEVELS: readonly ProfileLevel[];
export const DEFAULT_PROFILE_LEVEL: ProfileLevel; // 'base'

export interface AxisConfig {
  readonly values: readonly string[];
  readonly default: string;
  readonly attribute?: string;
  readonly media?: Readonly<Record<string, string>>;
}
export interface TabulaConfig {
  readonly profileId: string;
  readonly profileLevel?: ProfileLevel;
  readonly axes: Readonly<Record<string, AxisConfig>>;
  readonly baseline?: BaselineConfig;
  readonly groups?: readonly string[];
  readonly variantChainMaxLength?: number;
  readonly dynamicProperties?: Readonly<Record<string, DynamicPropertyConfig>>;
  readonly foreignClasses?: readonly string[];
  readonly textLeafComponents?: readonly string[];
  readonly gamut?: 'srgb' | 'p3' | 'rec2020';
  readonly budgets?: BudgetsConfig;
  readonly scan?: ScanConfig;
  readonly variants?: VariantsConfig; // declared variant products (contract J22)
}

export const configSchema: JsonSchemaObject; // JSON Schema (2020-12) for tabula.config.json

The variants section declares which variant chains produce CSS — the closure fix shipped in v0.2:

ts
export type VariantPreset = 'interaction' | 'all-len1' | 'none';
export const VARIANT_PRESETS: readonly VariantPreset[];

export interface VariantsConfig {
  /** Seeds the product map before `products` are merged over it. Absent ⇒ `interaction`. */
  readonly preset?: VariantPreset;
  /** Canonical chain prefix → the families it may prefix (`"*"` = every non-atomic family). */
  readonly products?: Readonly<Record<string, '*' | readonly string[]>>;
  /** Hard cap on materialized chain candidates. Absent ⇒ DEFAULT_MAX_CHAIN_CANDIDATES. */
  readonly maxChainCandidates?: number;
}

A declared variant product widens the registered set (base vocabulary ∪ products) so a variant-prefixed class emits CSS while the emitted stylesheet stays a pure function of the profile — see Concepts § Variants. Structural shape is checked by configSchema; the value-semantics gate (the chain grammar, family names, the media→breakpoint requirement TAB-E170, the invalid-product check TAB-E171, and the maxChainCandidates budget TAB-E172) lives in @tabula-css/tokens's validateConfig.

DynamicPropertyConfig.syntax may never be the literal '*' — a --d-* dynamic custom property (written through dyn()) with a universal @property syntax would validate nothing anywhere in the system, so both this schema and @tabula-css/tokens' validateConfig reject it (TAB-E127).

Other defaults and caps exported alongside the schema: DEFAULT_VARIANT_CHAIN_MAX_LENGTH (2, lowered from 3 in v0.2 to match the length the emitter actually produces), DEFAULT_VARIANT_PRESET ('interaction'), DEFAULT_MAX_CHAIN_CANDIDATES (20,000 — the variants.maxChainCandidates budget), DEFAULT_GAMUT ('srgb'), DEFAULT_MAX_ESCAPES (25), DEFAULT_MAX_SUPPRESSIONS (0), DEFAULT_SCAN_SOURCES/DEFAULT_SCAN_IGNORE (glob defaults for tabula scan), and MAX_AXIS_CROSS_PRODUCT (16).

Token, registry, and manifest schemas

ts
export const TOKEN_TYPES: readonly string[]; // 'color' | 'dimension' | 'number' | … | 'type' | 'shadow'
export const tokensSchema: JsonSchemaObject; // JSON Schema for a flat-DTCG token file
export interface Token {
  readonly $type: string;
  readonly $value: TokenValue;
  readonly $description: string;
  readonly $deprecated?: Deprecation;
  readonly $extensions?: TokenExtensions;
}
export type TokensFile = Readonly<Record<string, TokenNamespace>>;
export function isLiteralEscape(meta: ExceptionMeta): boolean;

export const registrySchema: JsonSchemaObject; // JSON Schema for registry.json (schema version 3)
export interface RegistryFile { /* registrySchemaVersion (3), classes, variants, exceptions, variantProducts, chainCount, chainExceptions, … */ }
export interface RegistryClass { /* family, slots, rank, declarations, … */ }

export const manifestSchema: JsonSchemaObject; // JSON Schema for manifest.json
export const MANIFEST_FORMAT_VERSION: number; // 1
export interface ManifestFile { /* formatVersion, sourceHash, inputsHash, artifacts, … */ }

tokensSchema covers the structural parts of the token-file contract (depth-2 nesting, a closed $type enum, required $description, kebab token names); value semantics a JSON Schema cannot express (axis totality, contrast, dimension domains) are custom passes in @tabula-css/tokens. registrySchema and manifestSchema are consumed by @tabula-css/registry's reader to validate registry.json and manifest.json before anything trusts their contents. The registry schema is at version 3 (v0.2): it adds the declared-variant-product fields variantProducts (chain prefix → sorted family list) and chainCount, plus the optional chainExceptions (exact single-class chain grants). A registry written at an earlier schema version is rejected by the reader with TAB-E302.

Every JSON Schema is typed through the minimal JsonSchema/JsonSchemaObject shape core also exports (a JSON Schema draft 2020-12 subset expressive enough for these four schemas, with an unknown-typed index signature rather than any).

See also

  • @tabula-css/tokens — the validator and resolver that consume core's tables and schemas.
  • @tabula-css/registry — the reader that validates registry.json/manifest.json against core's schemas.
  • @tabula-css/merge — the runtime that throws core's TAB-E3xx diagnostics.
  • Concepts — locality, the registry, the merge algorithm, and the banned-mechanism catalog these tables encode.

Released under the MIT License.