@canonical/design-system
v0.2.5
Published
An OWL ontology for modeling UI design systems as structured, queryable knowledge graphs. Built to bridge the gap between design specifications and implementation by establishing a shared semantic vocabulary for components, patterns, layouts, and their re
Maintainers
Keywords
Readme
Design System Ontology
An OWL ontology for modeling UI design systems as structured, queryable knowledge graphs. Built to bridge the gap between design specifications and implementation by establishing a shared semantic vocabulary for components, patterns, layouts, and their relationships.
Core Philosophy: A design system is more than a component library - it's a formal language. By modeling UI elements ontologically, we enable machine-readable specifications, automated consistency checking, and intelligent tooling that understands design intent.
Quick Start
1. Core Concepts
The ontology organizes UI elements into a hierarchy:
UIElement (root)
└── UIBlock (visual/abstract entity for composing UIs)
├── Component - Implementable UI piece (Button, Badge, Card...)
├── Pattern - Reusable solution to UX problems
├── Layout - Opinionated space division for navigation
├── Subcomponent - Part of a parent component
└── Group - Repeating series of one sibling block (Cards, Tiles...)Components are organized by Tiers (scope/applicability) and can be customized through ModifierFamilies (variant axes). The diagram is an overview, not the class list — run pragma ontology lookup ds for the full hierarchy (Modifier and ModifierFamily sit under UIElement too).
2. Defining a Component
@prefix ds: <https://ds.canonical.com/> .
ds:global.component.button a ds:Component ;
ds:name "Button" ;
ds:summary "Buttons trigger actions within an interface" ;
ds:tier ds:global ;
ds:hasModifierFamily ds:global.modifier_family.importance ;
ds:usage """### When to use
For primary actions that transform or submit data
### When not to use
For navigation - use links instead""" .3. Tiered Organization
Components belong to tiers that define their scope — global blocks are universal,
the apps* tiers scope application UI (shared or per-app), and further tiers cover
their own surfaces. The tier set is live graph data; query it, never copy it:
pragma tier list # every tier that exists today, with display names4. Modifier System
Modifiers provide systematic variation through ModifierFamilies:
ds:global.modifier_family.importance a ds:ModifierFamily ;
ds:name "Importance" ;
ds:hasModifier ds:global.modifier.primary,
ds:global.modifier.secondary,
ds:global.modifier.tertiary .
ds:global.component.button
ds:hasModifierFamily ds:global.modifier_family.importance .The families and their values are live graph data:
pragma modifier list # every family with its values
pragma modifier lookup <Family> # one family in full — take the name from the list outputHow-To Guides
How to Add a New Component
- Determine the appropriate tier based on scope
- Draft the spec as a standalone file under
specs/(seespecs/README.md) — never hand-editdata/, it is regenerated destructively from Coda; a human enters the finished spec into Coda - Define required properties: name, summary, tier
- Link to applicable modifier families
- Add usage guidelines (
ds:usage, with### When to use/### When not to usesub-sections)
@prefix ds: <https://ds.canonical.com/> .
ds:apps.component.file_tree a ds:Component ;
ds:name "FileTree" ;
ds:summary "Hierarchical file browser for navigating directory structures" ;
ds:tier ds:apps ;
ds:usage "### When to use\nWhen users need to navigate nested file hierarchies" ;
ds:figmaLink <https://figma.com/file/...> .How to Define a Layout
Layouts define how space is divided for a domain of information:
ds:apps.layout.sidebar a ds:Layout ;
ds:name "Sidebar Layout" ;
ds:domain "Application navigation" ;
ds:grid "1fr 4fr" ;
ds:gridAreas "sidebar main" ;
ds:targetDevices "desktop, tablet" .How to Create a Pattern
Patterns are reusable solutions to UX problems:
ds:global.pattern.empty_state a ds:Pattern ;
ds:name "Empty State" ;
ds:summary "Guidance shown when a container has no content" ;
ds:tier ds:global ;
ds:usage "### When to use\nWhen a list, table, or container is empty" ;
ds:guidelines "Include illustration, message, and action" .How to Model Component Composition
Use subcomponents for parts that belong to a parent — the live Card.Header, for example:
ds:global.subcomponent.card-header a ds:Subcomponent ;
ds:name "Card.Header" ;
ds:parentComponent ds:global.component.card .Only parts a user instantiates in their own code (<Card.Header>) get a
subcomponent entry; a part the user cannot write as <Parent.Part> belongs in the
parent's anatomy as an anonymous role, not here (Button's icon is a role, not a
subcomponent).
How to Query the Design System
Using the pragma CLI:
# every component, pattern, layout and subcomponent, with its type and tier
pragma block list
# Every triple on a specific block
pragma graph inspect ds:global.component.button
# Arbitrary SPARQL — e.g. components drawing from a modifier family (a local
# name with more than one dot does not parse in a query body; use the full IRI)
pragma graph query "SELECT ?c WHERE { ?c ds:hasModifierFamily <https://ds.canonical.com/global.modifier_family.criticality> }"Reference
Classes
| Class | Description |
|-------|-------------|
| UIElement | Root class for all design system entities |
| UIBlock | Visual/abstract entity for composing UIs |
| Component | Implementable UI piece |
| Pattern | Reusable UX solution |
| Layout | Space division for navigation |
| Subcomponent | Part of a component |
| Modifier | Variant option |
| ModifierFamily | Grouping of related modifiers |
| Tier | Scope/applicability level |
| Property | Configurable component property |
| ImplementationObject | Platform-specific implementation |
| ImplementationLibrary | Implementation library |
Run pragma ontology lookup ds for the full class list with today's instance
counts (it also carries classes this overview omits).
UIBlock Properties
| Property | Range | Description |
|----------|-------|-------------|
| name | string | Display name (from Entity) |
| summary | string | What this block is/does (from Entity) |
| tier | Tier | Scope classification |
| usage | string | Usage guidance (### When to use / ### When not to use sections) |
| guidelines | string | Design guidelines |
| figmaLink | anyURI | Link to Figma designs |
| anatomyDsl | string | Anatomy in the Anatomy DSL (YAML) |
| anatomyClassic | string | Anatomy as links/prose |
| hasVariant | UIBlock | Variant blocks |
| hasModifierFamily | ModifierFamily | Applicable variant axes |
| hasProperty | Property | Configurable properties |
Run pragma ontology lookup ds --class UIBlock for the full declared set —
documentationStage, changeLog, and the variant/inheritance links are there too
(name and summary are inherited from Entity; --class Entity shows them).
Layout-Specific Properties
| Property | Range | Description |
|----------|-------|-------------|
| domain | string | Information domain |
| grid | string | CSS grid definition |
| gridAreas | string | Named grid areas |
| targetDevices | string | Supported devices |
Component Relationships
| Property | Domain | Range | Description |
|----------|--------|-------|-------------|
| hasSubcomponent | Component | Subcomponent | Composition |
| parentComponent | Subcomponent | Component | Inverse of above |
| hasModifierFamily | UIBlock | ModifierFamily | Variant axes |
| hasModifier | ModifierFamily | Modifier | Variant options |
Implementation Bridge
| Property | Domain | Range | Description |
|----------|--------|-------|-------------|
| implementsBlock | ImplementationObject | UIBlock | Links code to spec |
| library | ImplementationObject | ImplementationLibrary | Source library |
| libraryTier | ImplementationLibrary | Tier | Library's tier |
Explanation
Why an Ontology?
After a decade using Vanilla Framework (CSS library), Canonical observed that visual consistency worked well but led to challenges:
- Inconsistent terminology - Same component, different names across teams
- Implicit relationships - Component composition undocumented
- Lost design rationale - Why decisions were made
- Fragmented specifications - Figma, docs, code out of sync
An ontology addresses these by:
- Establishing shared vocabulary - One name, one meaning
- Explicit relationships - Queryable component graph
- Structured metadata - Guidelines, rationale, links preserved
- Machine-readable specs - Enables tooling and validation
Design Principles
1. Separation of Concept and Implementation
The ontology separates the what (UIBlock) from the how (ImplementationObject). A Button concept can have multiple implementations across React, Web Components, or Flutter while maintaining semantic identity.
2. Tiered Scoping
Not all components belong everywhere. Tiers make scope explicit - global components are universal, while apps-tier components may not make sense on marketing sites.
3. Systematic Variation
ModifierFamilies provide principled variation. Instead of ad-hoc props like isImportant, isPrimary, variant="critical", the ontology models Importance and Criticality as distinct axes that components can opt into.
4. Documentation as Data
Usage guidelines, when-to-use patterns, and design rationale are first-class properties - queryable, versionable, and programmatically accessible.
Architecture
design-system/
├── definitions/
│ └── ontology.ttl # TBox: Classes, properties
├── data/ # Instance data — regenerated from Coda, never hand-edited
│ ├── global/ # Global tier instances
│ │ ├── component/ # Button, Badge, Card...
│ │ ├── subcomponent/ # Card.Header, Accordion.Item...
│ │ ├── group/ # Cards, Tiles...
│ │ ├── pattern/ # Empty state, Loading...
│ │ ├── layout/ # Grid layouts
│ │ ├── modifier/ # Primary, Secondary...
│ │ ├── modifier_family/ # Importance, Criticality...
│ │ └── ... # Other block classes
│ ├── apps/ # Apps tier
│ ├── sites/ # Sites tier
│ └── ... # Other tiers
├── specs/ # Drafted block specs — not read by build or sync
├── skills/ # Agent skills (installed via pragma sources update)
├── src/ # Build, sync, and collector source (TypeScript)
├── examples/ # Example files (sync target projection)
└── README.md # This fileNamespaces
@prefix ds: <https://ds.canonical.com/> . # Unified namespace for ontology and instancesURI Convention
Instances follow the pattern: {tier}.{class}.{name}
ds:global.component.button
ds:apps.layout.application_layout
ds:global.modifier_family.importanceDependencies
Runtime dependencies are declared in package.json (today ajv, jsonld, and n3 —
schema validation and RDF processing); read them there, never from this file.
Data Pipeline
The design system data is extracted from Coda and transformed to RDF:
bun install
# Create .env with Coda API key
echo "CODA_API_KEY=your-token" > .env
# Extract and transform
bun run ds:list # List available tables
bun run ds:extract # Extract to JSON
bun run ds:transform # Transform to JSON-LD/TurtleImplementation Collector
The collect-implementations script scans codebases for @implements annotations and generates RDF linking implementations to their design system specifications.
Setup
- Create a
design-system.jsonconfig file in your project root:
{
"name": "my-component-library",
"platform": "react",
"description": "React implementation of the design system",
"link": "https://github.com/org/my-library",
"documentation": "https://docs.example.com/components",
"tier": "ds:global",
"prefix": {
"short": "ds",
"namespace": "https://ds.canonical.com/"
},
"pattern": "src/**/*.tsx",
"outputDir": "data"
}Configuration Options
| Field | Required | Description |
|-------|----------|-------------|
| name | Yes | Library identifier (e.g., "pragma-react") |
| platform | Yes | Framework/platform (react, vue, angular, etc.) |
| description | No | Human-readable library description |
| link | Yes | Main repository or package URL |
| documentation | No | Documentation URL if different from link |
| tier | No | Design system tier reference (e.g., "ds:global") |
| prefix.short | Yes | Namespace prefix used in annotations (e.g., "ds") |
| prefix.namespace | Yes | Full namespace URI |
| pattern | Yes | Glob pattern for files to scan |
| outputDir | No | Output directory for .ttl files (default: "data") |
Annotating Components
Add @implements annotations as comments in your component files:
// @implements ds:global.component.button
export function Button({ children, ...props }) {
return <button {...props}>{children}</button>;
}Annotation formats:
- Basic:
// @implements ds:global.component.button - With version:
// @implements ds:[email protected] - Draft status:
// @implements ds:global.component.button [draft]
Running the Collector
From within your project directory (where design-system.json is located):
# Using bun directly
bun /path/to/design-system/src/collect-implementations.ts
# Or if installed globally/linked
collect-implementationsOutput
The collector generates two Turtle files in the configured output directory. With the
config and annotation from the sections above, it emits (verbatim, after each file's
@prefix header):
implementationLibrary.ttl- Defines the implementation library under<namespace>implementation.library.<slug>:ds:implementation.library.my-component-library a ds:ImplementationLibrary; ds:libraryName "my-component-library"; ds:platform "react"; ds:link "https://github.com/org/my-library"; ds:summary "React implementation of the design system"; ds:documentation "https://docs.example.com/components"; ds:libraryTier ds:global.implementationObjects.ttl- Attaches each annotated file to the library as a blank-nodeds:ImplementationObject(ds:headLinkcarries the file's relative path):ds:implementation.library.my-component-library ds:hasImplementation [ a ds:ImplementationObject; ds:implementsBlock ds:global.component.button; ds:headLink "src/components/Button.tsx" ].
Example Workflow
# 1. Navigate to your component library
cd my-react-library
# 2. Add annotations to components
echo '// @implements ds:global.component.button' >> src/Button.tsx
# 3. Run the collector
bun ~/code/cn/design-system/src/collect-implementations.ts
# Output:
# Scanning src/**/*.tsx for @implements annotations...
# Found 1 valid implementation(s):
# - ds:global.component.button
# Written: data/implementationLibrary.ttl
# Written: data/implementationObjects.ttl
# Done!CI: Automated Coda Sync
Two GitHub Actions workflows keep the design system data in sync with Coda:
- Scheduled — runs daily at 06:00 UTC
- Manual — trigger from the Actions tab via "Run workflow"
Both run bun run build (extract + transform) and commit any changes to data/.
Fail-closed guards
data/ is fully regenerated on every sync, so a broken extract (expired API
token, renamed grid, partial API response) could silently destroy committed
spec data. Three independent layers prevent that:
- Expected-table manifest — the transform derives the set of tables it
expects from
source.json(outputs, references, and@inlineembeds) and hard-fails if any is missing from the extract (src/transform/expectedTables.ts). - Delta guards — the new dataset is staged in a temp directory and
compared against the committed
data/before anything is deleted. The transform aborts when a non-empty table yields zero subjects, the subject count drops >10%, any tier file disappears, or the triple count drops >5% (src/transform/deltaGuards.ts,src/transform/collectDataMetrics.ts). - Workflow deletion threshold — after regeneration, the sync workflow
checks the staged git diff and refuses to commit when deletions exceed the
thresholds in
src/scripts/evaluateDataDeletion.ts. On any guard failure the run fails without committing and an issue is opened/annotated.
For an intentional large deletion (e.g. a planned cleanup in Coda), run the
manual sync workflow with the allow_shrink input enabled — it sets the
SYNC_ALLOW_SHRINK=1 escape hatch, which downgrades the guards to warnings
for that run. Locally: SYNC_ALLOW_SHRINK=1 bun run build.
Setting up the Coda API token
- Go to https://coda.io/account
- Scroll to API settings
- Click Generate API token
- Name it
ci-access-token - Add a restriction:
- Type: Doc or table
- Access: Read only
- Doc ID: the full
_d-prefixed value from the Coda URL — for example, givenhttps://coda.io/d/Design-System-Database_dNyzE_TLZDh/, the ID is_dNyzE_TLZDh. Verify by navigating tohttps://coda.io/d/_dNyzE_TLZDh
- Copy the token, then in your GitHub repo go to Settings > Secrets and variables > Actions and create a secret named
CODA_API_KEYwith the token value
Version
The current version lives in package.json — read it there, never from this file.
