Look Conventions
The one canonical way to style a Kanonak ontology, and a decision guide for when to use a look, a transformation, or a view.
The look system turns a class into a rendered page with no hand-written HTML or CSS: you attach a look to a type, and every instance of that type renders through it. Whatever a type does not declare, it inherits from its superclass and ultimately from the universal floor on Resource — so every resource has a sane page even before you style anything. This package is the single source of truth for that system; the conventions below are the whole of it.
Start here — what are you trying to produce?
A readable PAGE for a resource (its HTML page + SVG identity)
-> a LOOK. Declare derivation.look on the CLASS: a ResourceView of bands.
- want the resource's icon / glyph? add look.semanticSvg
- display name or summary is not the default
rdfs.label / rdfs.comment? add look.displayLabel / displaySummary
- want to theme its colors? add derivation.tokens
- want a DIAGRAM (relations, a sequence,
the schema)? add a look.Diagram / look.Sequence /
look.EntityRelationship band — it reads
the graph; author no diagram data
A NON-page artifact (JSON, YAML, generated code) -- or a visual the
band catalogue genuinely cannot express (a bespoke chart or matrix)
-> a TRANSFORMATION. A tx.Transformation bound via derivation.derivations.
A QUERY that selects, filters, or reshapes instances into a result set
-> a VIEW. A view.View, materialized into an EphemeralPackage.
In one line: Look for pages,
Derivations + a
Transformation for non-page artifacts and novel visuals, and a
View for queries. The look is the default for presentation; the other two are for when you are not rendering a resource's own page. Most publishers only ever need looks.
- Layer
Guidance Layer
- Author
Paul Fryer
- Created Date
- May 29, 2026
Conventions
Choosing an Approach
Three tools present or derive data, and they do not overlap. A look (Look) renders a resource as a page — the default, and almost always what you want. A transformation (
Transformation, bound via
Derivations) produces a non-page artifact (JSON, YAML, generated code) or a visual the band catalogue cannot express. A view (
View) is a query that selects and reshapes instances into a result set. Reach for the look first; only drop to a transformation or a view when you are not rendering a resource's own page.
| Text | Rationale | |
|---|---|---|
| # | To render a resource as a readable page, you SHOULD declare a | Declarative bands express the common page (hero + properties + cross-references) in a fraction of the YAML, with built-in cascade semantics for multi-publisher styling. Hand-rolled HTML transformations diverge across publishers and rot. |
| # | Author a | Transformations are the escape hatch for genuinely novel output. Using one where a look would do throws away the cascade and the shared band library. |
| # | Use a | A view is a query; a look is presentation. They share the |
Style Types, Not Instances
You attach a look to a class (or a package, or a publisher), and every instance of that type renders through it. You almost never style a single resource — you style the concept, and all its instances follow. Styling one instance directly is a rare, deliberate override, not the default move.
| Text | Rationale | |
|---|---|---|
| # | A | Styling the type is what makes the system scale: one declaration styles a thousand instances, and the class hierarchy carries it to subtypes. Per-instance looks are duplication waiting to drift. |
| # | When presentation should follow an instance's STATE — a status, a severity, a phase — declare a | State-driven presentation is the one case that looks like it needs per-instance styling and does not. A variant is still a declaration on the TYPE — one rule covers every instance that enters the state, including ones authored later — so it scales the way the rest of the cascade does, while styling each affected instance by hand does not. |
per-instance-styling
Avoid — styling one widget instance. Pull the look up onto the Widget class so every widget renders the same way.
- Value
- my-publisher.org/widgets/widget-0042: derivation.look: type: look.ResourceView bands: [ { type: look.Hero, look.title: rdfs.label } ]
The Resource View
The core pattern: a class declares Look whose value is a
Resource View — an ordered list of bands, each rendering one region of the page from the resource's own properties. The band catalogue covers the common page:
Hero (title + subtitle + type badges),
Property List and
Property Table (property values),
Reference List /
Referenced By (cross-references),
Markdown (prose),
Embedded Views (nested resources rendered through their own class looks), and more. Bands read existing properties — they do not carry content.
| Text | Rationale | |
|---|---|---|
| # | A class's page look MUST be a | An ordered list of declarative bands reading real properties is what lets the renderer produce a page with no per-class code, and keeps labels and values sourced from the graph (DRY). |
styled-class
A complete Author page — a hero from rdfs.label / rdfs.comment, the list of works from catalog.wrote, and an incoming-reference grid — every region derived from existing properties.
- Value
- my-publisher.org/catalog/Author: derivation.look: type: look.ResourceView bands: - { type: look.Hero, look.title: rdfs.label, look.subtitle: rdfs.comment, look.badges: rdfs.type } - { type: look.PropertyList, look.source: catalog.wrote } - { type: look.ReferencedBy }
Path-Carrier Bands
The common bands above name a single property in a slot like Source or
Title Property. The quantitative and series bands —
Stat Row,
Distribution,
Timeline /
Version Diff,
Version Delta,
Time Plot, a detail-row
Reference List, and
Diagram channels — need to reach a value that may live one or more steps INTO the instance. As of look@2.0.0 every such
*Path slot (metricPath, statPath, mapPath, alphaPath/betaPath, lowerPath/upperPath, hueBy, laneBy, labelPath, badgePath, nodeNote, edgeValue) is a Expression — the SAME vocabulary the SVG tiers and data Views use — NOT a
/-separated path string. There are exactly three shapes, chosen by what the step is:
- A property on the evaluated node → a
Property Read whose
readSourceis aVar Ref of
input. - A field inside an embedded value → NEST the read: a
tx.PropertyReadwhosereadSourceis itself thetx.PropertyReadof the embedding property (e.g. readconfidence, thenestimateMeanoff it). - A field across a REFERENCE → a
Traverse (
throughthe reference property, itsstepreading off the traversed-to subject); to show a referenced resource's display name, traverse and readLabel.
The node bound to input depends on the band: instance-level bands (Distribution, StatRow, VersionDelta) evaluate against the PAGE instance; series and diagram bands evaluate against each sub-resource reached through the band's track / source / entries / relation property. Either way input is the node, and every property is a real reference validated at author time.
| Text | Rationale | |
|---|---|---|
| # | Every band | A string path matched by stripped local name is a magic string: it fails silently to undefined at render time and is invisible to validation. A typed expression resolves every step through the object model, so |
stat-and-distribution
A StatRow cell reading estimateMean out of the embedded confidence estimate — a nested PropertyRead, the embedded-field shape.
- Value
- # `confidence` is an embedded Estimate on the page instance; # reach its `estimateMean` with a NESTED PropertyRead. Thesis: derivation.look: type: look.ResourceView bands: - type: look.StatRow look.stats: - look.statLabel: "μ" look.statPath: type: tx.PropertyRead tx.readSource: type: tx.PropertyRead tx.readSource: { type: tx.VarRef, tx.varName: input } tx.readProp: confidence tx.readProp: estimateMean
badge-via-traverse
A ReferenceList badge that follows each evidence item's strength reference and reads the target's rdfs.label — the reference-step shape (Traverse + a read of the traversed-to subject).
- Value
- # `strength` is a REFERENCE; show the referenced resource's label. - type: look.ReferenceList look.entries: [supports] look.badgePath: type: tx.Traverse tx.traverseSource: { type: tx.VarRef, tx.varName: input } tx.through: strength tx.step: type: tx.PropertyRead tx.readSource: { type: tx.VarRef, tx.varName: input } tx.readProp: rdfs.label
string-path
Invalid as of look@2.0.0 — a /-separated string path. The slot is a tx.Expression; author a nested tx.PropertyRead (read confidence, then betaAlpha) instead. A string here is silently dropped (the slot is an object property) and the band renders empty.
- Value
- - type: look.Distribution look.alphaPath: "confidence/betaAlpha" # WRONG: string path look.betaPath: "confidence/betaBeta"
Diagrams Read the Graph
Three bands draw pictures, and they share one discipline: nothing is drawn that the graph does not already state. Nodes are the resources' own identity tiles, labels resolve through the display lenses, and every indicator is read from the ontology — direction from the triple and from Domain /
Range, a reply from
Inverse Of, an undirected edge from
Symmetric Property, cardinality from
Min Count /
Max Count on a targeting shape or from
Functional Property. The look layer only says WHICH relations to show, and how they look where the ontology is silent.
| Band | Source | Draws |
|---|---|---|
the page instance's relations (facets → relation, encode) | a cluster, a hub-and-spoke, or a DAG (edges), with channels mapping a value to thickness / hue | |
a prose property on the page instance (source) + connectors | lifelines and messages, ordered by the prose's [[references]], joined by the graph | |
| the ontology itself | classes, attributes, relationships, cardinalities, inheritance |
A Sequence is the one to reach for when someone asks for a sequence diagram, and it is deliberately NOT a data model of steps. The prose supplies the ORDER and the PARTICIPANTS (every [[reference]], in document order); the graph supplies the EDGES and their DIRECTION (a message is drawn for each declared connector that actually holds between two consecutively-referenced participants, in the direction the triple states — so passive voice cannot mislead); the band's connectors overlay supplies the SELECTION (which object properties count as messages). The three never overlap, and the domain ontology imports nothing about sequences. A [[property]] reference placed between two participant references narrows that pair to that edge. A pair the graph does not join draws nothing — the graph is the filter — and kanonak validate (the SequenceNarrative rule) reports it, so prose↔graph drift is an author-time diagnostic, not a missing arrow.
The EntityRelationship band has no slots at all: on a Package page it draws every class the package declares; on a class page — whose look comes from
Class, since a class's own
Look describes its INSTANCES — it draws that class and the classes its object properties reach.
| Text | Rationale | |
|---|---|---|
| # | A diagram band MUST NOT be given data of its own. Do not model "steps", "messages", "edges" or "cardinality" so that a picture can be drawn: a | A diagram with its own data is a second copy of the model that drifts from the first. Reading the graph makes the picture a proof of the data — and lets |
| # | A | Order can only come from prose (a list has order but no narrative; a set has neither), and a message can only be a triple that holds. Both are checked at validate time — a non-prose source, a missing connector list, a relation that is not a property, and an unknown |
| Text | Rationale | |
|---|---|---|
| # | A | The prose is the diagram's annotation. Rendering both from one property means they cannot disagree, and a reader can follow the numbered messages back into the sentences that name them. |
| # | Prefer the ontology's own structure to a look slot: declare | Structure declared once serves every consumer — the ER diagram, the sequence, |
sequence-from-a-narrative
Three messages — payment → fraud (calls), fraud → payment (responds to, dotted because it is the owl.inverseOf a connector), payment → audit (notifies, open arrowhead) — every one a triple that holds, in the order the prose names the participants. "The fraud service answers the payment service" draws fraud → payment because that is the direction of the respondsTo statement, whatever voice the sentence uses.
- Value
- Service: type: rdfs.Class look.displayLabel: serviceName # lifeline and message labels use the lens calls: { type: owl.ObjectProperty, rdfs.domain: Service, rdfs.range: Service, rdfs.label: calls } respondsTo: { type: owl.ObjectProperty, rdfs.domain: Service, rdfs.range: Service, rdfs.label: responds to, owl.inverseOf: calls } # → the dotted reply, derived notifies: { type: owl.ObjectProperty, rdfs.domain: Service, rdfs.range: Service, rdfs.label: notifies } narrative: { type: owl.DatatypeProperty, rdfs.domain: Flow, rdfs.range: prose.Markdown, rdfs.label: Narrative } Flow: type: rdfs.Class derivation.look: type: look.ResourceView look.bands: - type: look.Markdown # the prose … look.source: narrative - type: look.Sequence # … and the diagram, from ONE property look.source: narrative look.nodeTier: chip look.connectors: - { look.relation: calls } - { look.relation: respondsTo } - { look.relation: notifies, look.lineStyle: open } # async: the one thing OWL cannot say payment-service: { type: Service, serviceName: Payment Service, calls: fraud-service, notifies: audit-service } fraud-service: { type: Service, serviceName: Fraud Service, respondsTo: payment-service } audit-service: { type: Service, serviceName: Audit Service } checkout-flow: type: Flow narrative: | The [[payment-service]] asks the [[fraud-service]] to screen the order, and the [[fraud-service]] answers the [[payment-service]] with a verdict. On success the [[payment-service]] tells the [[audit-service]] to record it.
entity-relationship-on-package-and-class
No slots to author: the package page draws every class the package declares, each class page draws its neighbourhood, and the 1..1 on serviceName is read from the SHACL shape that kanonak validate --conformance also enforces.
- Value
- my-package: type: Package derivation.look: type: look.PackageView look.bands: - type: look.EntityRelationship # the whole package's schema rdfs.Class: # augment the imported class: every class PAGE derivation.look: type: look.ResourceView look.bands: - type: look.EntityRelationship # this class and the classes it reaches service-shape: # cardinality comes from here, not from the band type: sh.NodeShape sh.targetClass: Service sh.property: - { type: sh.PropertyShape, sh.path: serviceName, sh.minCount: 1, sh.maxCount: 1 }
steps-modeled-for-the-picture
Avoid — a Step class invented so a sequence can be drawn. It duplicates relations the services already have (calls, respondsTo) and will drift from them. Write the narrative as prose with [[references]] and let a look.Sequence read the existing object properties.
- Value
- Step: type: rdfs.Class stepFrom: { type: owl.ObjectProperty, rdfs.domain: Step, rdfs.range: Service } stepTo: { type: owl.ObjectProperty, rdfs.domain: Step, rdfs.range: Service } stepOrder: { type: owl.DatatypeProperty, rdfs.domain: Step, rdfs.range: xsd.integer } checkout-flow: type: Flow steps: - { type: Step, stepFrom: payment-service, stepTo: fraud-service, stepOrder: 1 } - { type: Step, stepFrom: fraud-service, stepTo: payment-service, stepOrder: 2 }
cardinality-on-the-band
Avoid — the band has no cardinality slot by design. Declare a sh.PropertyShape with sh.minCount / sh.maxCount (or make the property owl.FunctionalProperty); the diagram and the conformance validator then read the same constraint.
- Value
- - type: look.EntityRelationship look.cardinality: # no such slot, and there must not be one - { look.relation: serviceName, look.min: 1, look.max: 1 }
Visual Identity
A resource's icon is declared with Semantic SVG: one responsive SVG with four visibility tiers — chip, icon, card, full — so the same concept reads as a favicon, a sidebar tile, a grid card, and a hero figure without per-context variants. Each tier is a
Expression (typically a
Concat over SVG string literals interleaved with
Property Read /
Display Label), so every substitution resolves through the object model and is validated at author time. The floor on
Resource supplies a first-letter glyph, so a class needs this only to override the default.
| Text | Rationale | |
|---|---|---|
| # | A custom | A typed expression fails at |
Display Lenses
Cards, nav links, hero titles, and SVG glyphs all read a resource's display name and summary through two lenses: Display Label and
Display Summary. The defaults on
Resource are
Label and
Comment. A class whose name or summary lives in a domain-specific property (say
teamName) declares the lens once, and every renderer reads the right value — no need to duplicate the value into rdfs.label.
| Text | Rationale | |
|---|---|---|
| # | When a class's display name or one-line summary is NOT in | The lens keeps one value in one place and teaches every renderer to find it; duplicating into rdfs.label invites the two copies to drift apart. |
team-name-lens
The Team class tells every renderer that a team's display name comes from teamName and its summary from teamCharter.
- Value
- my-publisher.org/org/Team: look.displayLabel: org.teamName look.displaySummary: org.teamCharter
The Cascade and the Floor
Looks cascade. A resource's look, semanticSvg, display lenses, and tokens each resolve by walking outward from the resource — the closest declaration wins — and bottoming out at the universal floor on Resource (published as
kanonak.org/universal-look). So you only declare what differs from your superclass, and a class that declares nothing still renders a sensible page. Class-hierarchy overrides merge per facet; a per-instance override replaces.
Tokens resolve through five levels, closest first:
| Level | Declared on | Use it for |
|---|---|---|
| instance | the resource itself | a deliberate one-off |
| variant | a matching | presentation that follows state |
| class | any class in its type chain | the normal place to style |
| publisher | your | the site-wide palette |
| floor | the defaults you inherit |
Set the palette at the publisher level and refine per class — a class override beats the site palette, and the site palette beats the floor. Two things worth knowing before you theme: the stock floor is a DARK theme, so a light palette must set every colour token or land half-applied; and every rendered stylesheet carries a provenance header naming which level supplied each token, which is the fastest way to answer "why is this colour not what I set?".
| Text | Rationale | |
|---|---|---|
| # | Every resource MUST render even when its class declares no look: the universal floor on | A guaranteed floor is what makes the system safe to adopt incrementally — you style what matters and inherit the rest, rather than authoring a full page for every class up front. |
| Text | Rationale | |
|---|---|---|
| # | A site-wide palette SHOULD be declared as | The publisher level exists so one declaration themes a site while leaving each class free to refine it. Repeating a palette per class is duplication waiting to drift. Overriding the floor does work — it is where |