CSS hooks (Yew)

This page lists what a stylesheet can select in the output of prose-yew, split into Stable and Internal, and ends with a complete example stylesheet in light and dark.

The repository states no CSS stability policy. The README lists some classes, the Rust doc comments list others, and the snapshots in prose-yew/tests/snapshots/ pin the read-only markup. The split below is a recommendation based on how much each hook is documented, pinned by a test, or used by the two demos. Stable means: select it. Internal means: it carries layout or wiring and may change in a minor release.

The tables are checked against the code. A test collects every prose-* string literal in prose-yew/src/ and fails when one is missing from this page or when this page names one the code does not write.

Stable hooks: read-only renderer #

These classes are written by OdrlProse (Yew), by ProseHandle (the wasm bridge) and by <ds-odrl-prose> (Angular). OdrlProseView in read mode writes the same names. The Rust source is prose-yew/src/lib.rs; the output is pinned by the snapshots in prose-yew/tests/snapshots/.

SelectorElementWhat it styles
.ds-prosearticleThe root. The class prop, or articleClass in Angular, is appended here.
.prose-policysectionOne policy: an h3, an intro p, then the parts below.
.prose-notesulNotes about the policy, only when there are some.
.prose-rulesolThe rules of one policy.
.prose-ruleliOne rule. A duty, remedy or consequence nested under a rule is also a li.prose-rule.
.prose-permission, .prose-prohibition, .prose-obligationliThe kind of the rule, next to .prose-rule. A duty is a .prose-obligation.
.prose-rule-sentencepThe sentence of a rule.
.prose-referenceaThe IRI of a constraint the policy refers to and does not declare, in "the constraint defined at … holds", in a condition or in a rule's sentence (a target's or a party's refinement). A link only when the host lists the IRI in links (Yew), as the site's catalog does for IRIs that answer HTTP 200; otherwise the IRI is plain text. The wasm bridge and Angular pass no links, so they do not write it.
.prose-irispan, aA party, an asset or a value the policy names by an IRI, in an intro, a rule's sentence or a condition. It shows the label the document declares for the IRI (rdfs:label, dct:title, skos:prefLabel or schema:name), else the IRI's last segment, and keeps the full IRI in title. An a with href only when the IRI is http(s) and the host lists it in links (Yew); otherwise a span.
.prose-labelpA lead-in such as "Applies only if:", and the heading of the warnings block.
.prose-warningsasideThe "Not rendered" block: parts of the policy the renderer did not turn into sentences.
.prose-errorp[role="alert"]Replaces the policy when the JSON cannot be read. It sits inside article.ds-prose. The Angular component also writes it when the wasm module cannot be loaded; that one is a child of ds-odrl-prose, outside article.ds-prose, and .ds-prose .prose-error does not match it. Select ds-odrl-prose > .prose-error.
.prose-glossarydetails[open]Terms of the loaded profile that the policy uses, after the policy. At most 30 entries.
.prose-note, .prose-kind-of, .prose-implies, .prose-deprecated, .prose-fromddOne line of a glossary entry: usage note, "A kind of", "Implies", deprecation, source profile.
.prose-violationssectionValidation findings and the verdict, after the policies. Written only with a validator and the findings switched on.
.prose-violations-statusp[role="status"]The verdict line. Written only for a strict validation.
.prose-conforms, .prose-incomplete, .prose-violations-foundpThe verdict, next to .prose-violations-status: conforms, cannot be certified, does not conform.
.prose-violations-listulThe findings.
.prose-violationliOne finding.
.prose-violation-error, .prose-violation-warning, .prose-violation-infoliThe severity of a finding, next to .prose-violation.
.prose-rule-idcodeThe rule id inside a finding, for example prose.datatype-lexical.

Elements without a class #

The read-only renderer leaves some elements bare. Reach them by structure, which ties the stylesheet to the markup.

SelectorElementWhat it styles
.prose-policy > h3h3The policy heading.
.prose-policy > ppThe intro sentence.
.prose-rule > ululThe conditions and refinements of a rule, and the rules nested in a follow-up list. They follow a p.prose-label, which is the only thing telling them apart.
.prose-glossary dt, .prose-glossary dddt, ddA glossary term (a span with the label, then a code with the written form) and its plain definition.

The editor view, OdrlProseView, gives conditions and follow-up lists classes. The read-only renderer does not. This is a gap in the library, not a rule.

Stable hooks: editor view #

OdrlProseView writes these in addition to the names above. In read mode the same markup is written without the controls, the slots, the selects, the <style> element, the add bars and the live region. Source: prose-yew/src/edit/view.rs, slot.rs, choice.rs.

SelectorElementWhat it styles
.prose-viewarticle[tabindex="-1"]The root of OdrlProseView, next to .ds-prose, in both modes.
.prose-editrootEdit mode only. BASE_CSS is scoped to .ds-prose.prose-edit.
.prose-reveal-hoverrootEdit mode with Reveal::OnHoverOrFocus: the add, remove, reorder and group buttons are dimmed until hover or focus.
.prose-heading-rowdivEdit mode: wraps the policy heading and its toolbar.
.prose-headingh3The policy heading.
.prose-intropThe policy intro.
.prose-conditionsdivA label, a ul.prose-list and, in edit mode, an add bar. Used for limits and conditions.
.prose-listulA list of conditions or follow-up rules.
.prose-conditionliA condition.
.prose-logicalliA condition that has children (an and, or, xone group), next to .prose-condition.
.prose-follow-updivA block of duties, remedies or consequences.
.prose-carrieddivA follow-up block in a position ODRL does not give it, next to .prose-follow-up.
.prose-termspan[title]A displayed term. With a profile that explains it, it also has data-iri and aria-describedby.
.prose-itemspanOne entry in a list of actions, parties or assets.
.prose-inheritedspan[title]A value inherited from the parent policy.
.prose-textspanWritten only when the host sets classes.text or styles.text.
.prose-locked-notespanThe note on a part the editor keeps verbatim.
.prose-lockedcodeThe JSON of that part.
.prose-emptyp or spanThe text of an empty state.
.prose-decorationspanA badge the host returned from the decorate callback.
.prose-add-bardivEdit mode: the row of add buttons and list issues.
.prose-toolbarspanEdit mode: the move, remove and group buttons of one item.
.prose-addbutton[data-add]Add button. data-add holds the list path.
.prose-removebuttonRemove button.
.prose-reorderbuttonMove up and move down.
.prose-wrapbuttonGroup and ungroup.
.prose-livediv[role="status"], span[role="status"]The announcer, visually hidden by BASE_CSS. The div is the last child of the root. A span is written inside the wrapper of each closed slot. With base_css off, hide both yourself, and select the class, not the tag.
.prose-slot-wrapspanPositioning context for a slot, its message and its listbox.
.prose-slotspan[contenteditable]An editable slot. Has data-slot and data-placeholder.
.prose-slot-emptyslotThe slot has no text.
.prose-slot-invalidslotError state. A slot with aria-invalid="true" is styled the same.
.prose-slot-warningslotWarning state.
.prose-slot-whitespaceslotThe text has leading, trailing or non-breaking whitespace.
.prose-slot-notesmallThe "not in the profile" note under a closed slot.
.prose-slot-messagespanWhy typed text was refused, positioned above the slot. Also has .prose-issue and .prose-issue-error.
.prose-suggestionsul[role="listbox"]The open suggestion list.
.prose-suggestionli[role="option"]One option.
.prose-suggestion-activeoptionThe active option.
.prose-suggestion-nestedoptionA value narrower than a suggested one. Indented through --prose-depth.
.prose-suggestion-groupli[role="group"]A heading and its options, written when some suggestion has a group.
.prose-suggestion-group-labelspanThe group heading.
.prose-selectselectA closed choice such as an operator.
.prose-rule-kindselectThe select that sets the kind of a rule: may, must not or must. It is next to .prose-select.
.prose-issuespanAn issue message the host passed in issues or refusal.
.prose-issue-error, .prose-issue-warning, .prose-issue-infospanThe severity of an issue, next to .prose-issue.

Two more selectors have no class. A closed slot has data-closed="true". A rule also carries prose- plus the name of its list (permission, prohibition, obligation, duty, remedy or consequence), built at run time. Select the first three through the kind classes above, and do not select the other three.

Stable hooks: problems panel #

ProseProblems writes these. The panel is usually placed next to the view and outside .ds-prose, so its rules are not scoped under .ds-prose. Source: prose-yew/src/edit/problems.rs; the markup is pinned by strict_problems.html.

SelectorElementWhat it styles
.prose-problemssectionThe panel root. The class prop is appended here.
.prose-problems-headingh3The heading. It takes focus after a fix removed the focused button.
.prose-problems-statusp[role="status"]The verdict, for a strict report only, with one of .prose-conforms, .prose-incomplete, .prose-violations-found.
.prose-problems-countspThe counts of errors, warnings and notes.
.prose-problem-groupsolThe groups, one per rule.
.prose-problem-groupliOne rule: an h4 with the rule id and title, then its findings.
.prose-problem-listolThe findings of one rule.
.prose-problemliOne finding.
.prose-problem-error, .prose-problem-warning, .prose-problem-infoliThe severity, next to .prose-problem.
.prose-problem-gapliThe finding is a gap: something could not be checked.
.prose-problem-messagespanThe message.
.prose-problem-gobuttonGo to the target. The host moves focus.
.prose-problem-fixbutton[data-safe]One per available fix. data-safe="false" marks a fix that is not lossless.

Internal hooks #

SelectorElementWhy it is internal
.prose-slot-editspanInner wrapper of a slot. BASE_CSS has no rule for it.
--prose-depthcustom propertySet inline by the library on each nested suggestion. Do not set it.

Also internal: the element ids (prose-sugg-<slot>, prose-note-<slot>, prose-message-<slot>, prose-issue-<key>-<i>, prose-def-<slot>), the text of every aria-label and title, tabindex="-1" on containers, the order of siblings, the comment markers in server-rendered output, and the text of BASE_CSS itself. The contract is that the editor ships a default stylesheet you can turn off.

Data attributes and roles #

data-node marks a policy, rule or condition container and holds its path. data-slot marks a slot or select and holds its path. data-add marks an add button and holds the path of the list it adds to; data-item="group" marks the button that adds a group. data-closed="true" marks a closed slot, which takes only the values the profile lists. data-placeholder holds the text an empty slot shows. data-iri holds the full IRI of a profile term in read mode. data-safe on a fix button says whether the fix is lossless. The paths are the strings of NodePath, SlotPath and ListPath from prose-core, and ProseProblems::onjump hands them to a host that moves focus. A slot is a combobox when suggestions are on and a textbox when they are off.

Custom properties #

PropertyRead byFallbackSet by
--prose-errorBASE_CSS: the invalid underline and the error issue text#c9190bThe host, on any ancestor or on .ds-prose.
--prose-warningBASE_CSS: the warning underline and the warning issue text#b45309The host.

The read-only renderer reads no custom properties. --prose-error does not colour .prose-error, .prose-violation-error or .prose-problem-error; style those classes yourself, as the example does. No other custom property exists in the Rust, TypeScript or CSS sources of the repository.

Example stylesheet: rendered policy #

prose.css is a complete starting point for the rendered policy, the glossary and the validation findings. It defines its own tokens (--ds-*) in light, in dark by system preference and in dark by data-theme="dark" on <html>, and sets --prose-error and --prose-warning from them. Rename the tokens to yours and keep the selectors. The same file is served at examples/prose.css, and the home page of this site loads it. A test renders the real output with it in Chrome and checks the colours.

/* ds-prose-rs: example stylesheet for the rendered policy (light and dark).
   The library ships no CSS for the read-only renderer. This file is a complete
   starting point: copy it, rename the --ds-* tokens to yours, keep the selectors.
   Editor overrides live in prose-editor.css and read the tokens defined here. */

/* Tokens: light first, then dark by system preference, then an explicit
   data-theme on <html> that wins over the system in both directions. */
:root {
  color-scheme: light dark;
  --ds-fg: #1f2328;
  --ds-bg: #ffffff;
  --ds-muted: #57606a;
  --ds-border: #d0d7de;
  --ds-accent: #0969da;
  --ds-allow: #15803d;
  --ds-deny: #b91c1c;
  --ds-duty: #1d4ed8;
  --ds-warn: #b45309;
  --ds-focus: #0969da;
  --ds-surface: #f6f8fa;
  --ds-selected-bg: #0969da;
  --ds-selected-fg: #ffffff;
  /* The two custom properties the library reads (editor only). */
  --prose-error: var(--ds-deny);
  --prose-warning: var(--ds-warn);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ds-fg: #e6edf3;
    --ds-bg: #0d1117;
    --ds-muted: #9198a1;
    --ds-border: #3d444d;
    --ds-accent: #4493f8;
    --ds-allow: #3fb950;
    --ds-deny: #f85149;
    --ds-duty: #58a6ff;
    --ds-warn: #d29922;
    --ds-focus: #4493f8;
    --ds-surface: #151b23;
    --ds-selected-bg: #1f6feb;
    --ds-selected-fg: #ffffff;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --ds-fg: #e6edf3;
  --ds-bg: #0d1117;
  --ds-muted: #9198a1;
  --ds-border: #3d444d;
  --ds-accent: #4493f8;
  --ds-allow: #3fb950;
  --ds-deny: #f85149;
  --ds-duty: #58a6ff;
  --ds-warn: #d29922;
  --ds-focus: #4493f8;
  --ds-surface: #151b23;
  --ds-selected-bg: #1f6feb;
  --ds-selected-fg: #ffffff;
}
:root[data-theme="light"] {
  color-scheme: light;
}

/* Angular only: an unknown element is display: inline. */
ds-odrl-prose {
  display: block;
}

/* The policy */
.ds-prose {
  color: var(--ds-fg);
  line-height: 1.5;
}
.ds-prose .prose-policy {
  margin: 0 0 2rem;
}
.ds-prose .prose-policy > h3,
.ds-prose .prose-heading {
  margin: 0 0 0.25rem;
  font-size: 1.1rem;
}
.ds-prose .prose-rules {
  padding-left: 1.25rem;
}
.ds-prose .prose-rule {
  margin: 0 0 1rem;
}
.ds-prose .prose-rule-sentence {
  margin: 0 0 0.25rem;
  padding-left: 0.5rem;
  font-weight: 600;
  border-left: 4px solid var(--ds-border);
}
.ds-prose .prose-permission > .prose-rule-sentence {
  border-left-color: var(--ds-allow);
}
.ds-prose .prose-prohibition > .prose-rule-sentence {
  border-left-color: var(--ds-deny);
}
.ds-prose .prose-obligation > .prose-rule-sentence {
  border-left-color: var(--ds-duty);
}
.ds-prose .prose-label {
  margin: 0.4rem 0 0.1rem;
  font-size: 0.9rem;
  color: var(--ds-muted);
}
.ds-prose .prose-notes {
  font-size: 0.85rem;
  color: var(--ds-muted);
}
.ds-prose .prose-term {
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.ds-prose .prose-inherited,
.ds-prose .prose-empty {
  color: var(--ds-muted);
  font-style: italic;
}
.ds-prose .prose-warnings {
  font-size: 0.85rem;
  padding: 0.4rem 0.8rem;
  border: 1px solid var(--ds-warn);
  border-radius: 6px;
}
.ds-prose .prose-error,
ds-odrl-prose > .prose-error {
  color: var(--ds-deny);
}

/* Glossary of profile terms */
.ds-prose .prose-glossary {
  margin-top: 0.75rem;
  font-size: 0.9rem;
}
.ds-prose .prose-glossary dt {
  margin-top: 0.4rem;
  font-weight: 600;
}
.ds-prose .prose-glossary dt code {
  font-weight: 400;
  color: var(--ds-muted);
}
.ds-prose .prose-glossary dd {
  margin-left: 1rem;
}
.ds-prose .prose-note,
.ds-prose .prose-kind-of,
.ds-prose .prose-implies,
.ds-prose .prose-from {
  color: var(--ds-muted);
}
.ds-prose .prose-deprecated {
  color: var(--ds-warn);
}

/* Validation findings and the verdict */
.ds-prose .prose-violations-status {
  display: inline-block;
  margin: 0 0 0.5rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid currentColor;
  border-radius: 6px;
  font-weight: 600;
}
.ds-prose .prose-conforms {
  color: var(--ds-allow);
}
.ds-prose .prose-incomplete {
  color: var(--ds-warn);
}
.ds-prose .prose-violations-found {
  color: var(--ds-deny);
}
.ds-prose .prose-violations-list {
  font-size: 0.9rem;
}
.ds-prose .prose-violation-error {
  color: var(--ds-deny);
}
.ds-prose .prose-violation-warning {
  color: var(--ds-warn);
}
.ds-prose .prose-violation-info {
  color: var(--ds-muted);
}
.ds-prose .prose-rule-id {
  color: var(--ds-muted);
}

Example stylesheet: editor and problems panel #

prose-editor.css recolours the editor and the problems panel with the same tokens. Load it after prose.css. It keeps base_css on. See Styling for why every selector starts with :root.

/* ds-prose-rs: example overrides for the editor (Yew only), light and dark.
   Load it after prose.css, which defines the --ds-* tokens used here.
   Keep EditConfig::base_css on: this file only recolours.
   BASE_CSS is a <style> inside the article, after every stylesheet in <head>,
   and all of its selectors start with .ds-prose.prose-edit. The :root prefix
   below adds one point of specificity so these rules win. */

:root .ds-prose.prose-edit .prose-slot {
  border-bottom-color: var(--ds-muted);
  background: color-mix(in srgb, var(--ds-accent) 8%, transparent);
  border-radius: 3px;
}
:root .ds-prose.prose-edit .prose-slot:focus,
:root .ds-prose.prose-edit .prose-select:focus,
:root .ds-prose.prose-edit button:focus-visible {
  outline-color: var(--ds-focus);
}
:root .ds-prose.prose-edit .prose-slot-invalid,
:root .ds-prose.prose-edit .prose-slot[aria-invalid="true"] {
  border-bottom-color: var(--prose-error);
}
:root .ds-prose.prose-edit .prose-slot-warning {
  border-bottom-color: var(--prose-warning);
}
:root .ds-prose.prose-edit .prose-select,
:root .ds-prose.prose-edit .prose-add,
:root .ds-prose.prose-edit .prose-remove,
:root .ds-prose.prose-edit .prose-reorder,
:root .ds-prose.prose-edit .prose-wrap {
  color: var(--ds-fg);
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  border-radius: 4px;
}
:root .ds-prose.prose-edit .prose-locked {
  border-color: var(--ds-border);
  background: var(--ds-surface);
}
:root .ds-prose.prose-edit .prose-slot-note {
  color: var(--ds-warn);
}
:root .ds-prose.prose-edit .prose-slot-message {
  background: var(--ds-bg);
  color: var(--prose-error);
  border-color: var(--prose-error);
}

/* Suggestion listbox */
:root .ds-prose.prose-edit .prose-suggestions {
  background: var(--ds-bg);
  color: var(--ds-fg);
  border-color: var(--ds-border);
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.2);
}
:root .ds-prose.prose-edit .prose-suggestion-active,
:root .ds-prose.prose-edit .prose-suggestion:hover {
  background: var(--ds-selected-bg);
  color: var(--ds-selected-fg);
}
:root .ds-prose.prose-edit .prose-suggestion-group-label {
  color: var(--ds-muted);
}

/* Issues shown at their slot */
:root .ds-prose.prose-edit .prose-issue-error {
  color: var(--prose-error);
}
:root .ds-prose.prose-edit .prose-issue-warning {
  color: var(--prose-warning);
}
:root .ds-prose.prose-edit .prose-issue-info {
  color: var(--ds-muted);
}

/* Problems panel. It is usually placed next to the view, outside .ds-prose,
   so these rules are not scoped under .ds-prose. */
.prose-problems {
  color: var(--ds-fg);
  font-size: 0.9rem;
}
.prose-problems-counts {
  color: var(--ds-muted);
}
.prose-problem-groups,
.prose-problem-list {
  padding-left: 1.25rem;
}
.prose-problem {
  margin: 0 0 0.4rem;
}
.prose-problem-error .prose-problem-message {
  color: var(--ds-deny);
}
.prose-problem-warning .prose-problem-message {
  color: var(--ds-warn);
}
.prose-problem-info .prose-problem-message {
  color: var(--ds-muted);
}
.prose-problem-gap .prose-problem-message {
  font-style: italic;
}
.prose-problem-go,
.prose-problem-fix {
  margin-left: 0.25rem;
  padding: 0.1rem 0.45rem;
  color: var(--ds-fg);
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  border-radius: 4px;
}
.prose-problem-fix[data-safe="false"] {
  border-color: var(--ds-warn);
}
.prose-problems-status.prose-conforms {
  color: var(--ds-allow);
}
.prose-problems-status.prose-incomplete {
  color: var(--ds-warn);
}
.prose-problems-status.prose-violations-found {
  color: var(--ds-deny);
}