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/.
| Selector | Element | What it styles |
|---|---|---|
.ds-prose | article | The root. The class prop, or articleClass in Angular, is appended here. |
.prose-policy | section | One policy: an h3, an intro p, then the parts below. |
.prose-notes | ul | Notes about the policy, only when there are some. |
.prose-rules | ol | The rules of one policy. |
.prose-rule | li | One rule. A duty, remedy or consequence nested under a rule is also a li.prose-rule. |
.prose-permission, .prose-prohibition, .prose-obligation | li | The kind of the rule, next to .prose-rule. A duty is a .prose-obligation. |
.prose-rule-sentence | p | The sentence of a rule. |
.prose-reference | a | The 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-iri | span, a | A 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-label | p | A lead-in such as "Applies only if:", and the heading of the warnings block. |
.prose-warnings | aside | The "Not rendered" block: parts of the policy the renderer did not turn into sentences. |
.prose-error | p[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-glossary | details[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-from | dd | One line of a glossary entry: usage note, "A kind of", "Implies", deprecation, source profile. |
.prose-violations | section | Validation findings and the verdict, after the policies. Written only with a validator and the findings switched on. |
.prose-violations-status | p[role="status"] | The verdict line. Written only for a strict validation. |
.prose-conforms, .prose-incomplete, .prose-violations-found | p | The verdict, next to .prose-violations-status: conforms, cannot be certified, does not conform. |
.prose-violations-list | ul | The findings. |
.prose-violation | li | One finding. |
.prose-violation-error, .prose-violation-warning, .prose-violation-info | li | The severity of a finding, next to .prose-violation. |
.prose-rule-id | code | The 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.
| Selector | Element | What it styles |
|---|---|---|
.prose-policy > h3 | h3 | The policy heading. |
.prose-policy > p | p | The intro sentence. |
.prose-rule > ul | ul | The 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 dd | dt, dd | A 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.
| Selector | Element | What it styles |
|---|---|---|
.prose-view | article[tabindex="-1"] | The root of OdrlProseView, next to .ds-prose, in both modes. |
.prose-edit | root | Edit mode only. BASE_CSS is scoped to .ds-prose.prose-edit. |
.prose-reveal-hover | root | Edit mode with Reveal::OnHoverOrFocus: the add, remove, reorder and group buttons are dimmed until hover or focus. |
.prose-heading-row | div | Edit mode: wraps the policy heading and its toolbar. |
.prose-heading | h3 | The policy heading. |
.prose-intro | p | The policy intro. |
.prose-conditions | div | A label, a ul.prose-list and, in edit mode, an add bar. Used for limits and conditions. |
.prose-list | ul | A list of conditions or follow-up rules. |
.prose-condition | li | A condition. |
.prose-logical | li | A condition that has children (an and, or, xone group), next to .prose-condition. |
.prose-follow-up | div | A block of duties, remedies or consequences. |
.prose-carried | div | A follow-up block in a position ODRL does not give it, next to .prose-follow-up. |
.prose-term | span[title] | A displayed term. With a profile that explains it, it also has data-iri and aria-describedby. |
.prose-item | span | One entry in a list of actions, parties or assets. |
.prose-inherited | span[title] | A value inherited from the parent policy. |
.prose-text | span | Written only when the host sets classes.text or styles.text. |
.prose-locked-note | span | The note on a part the editor keeps verbatim. |
.prose-locked | code | The JSON of that part. |
.prose-empty | p or span | The text of an empty state. |
.prose-decoration | span | A badge the host returned from the decorate callback. |
.prose-add-bar | div | Edit mode: the row of add buttons and list issues. |
.prose-toolbar | span | Edit mode: the move, remove and group buttons of one item. |
.prose-add | button[data-add] | Add button. data-add holds the list path. |
.prose-remove | button | Remove button. |
.prose-reorder | button | Move up and move down. |
.prose-wrap | button | Group and ungroup. |
.prose-live | div[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-wrap | span | Positioning context for a slot, its message and its listbox. |
.prose-slot | span[contenteditable] | An editable slot. Has data-slot and data-placeholder. |
.prose-slot-empty | slot | The slot has no text. |
.prose-slot-invalid | slot | Error state. A slot with aria-invalid="true" is styled the same. |
.prose-slot-warning | slot | Warning state. |
.prose-slot-whitespace | slot | The text has leading, trailing or non-breaking whitespace. |
.prose-slot-note | small | The "not in the profile" note under a closed slot. |
.prose-slot-message | span | Why typed text was refused, positioned above the slot. Also has .prose-issue and .prose-issue-error. |
.prose-suggestions | ul[role="listbox"] | The open suggestion list. |
.prose-suggestion | li[role="option"] | One option. |
.prose-suggestion-active | option | The active option. |
.prose-suggestion-nested | option | A value narrower than a suggested one. Indented through --prose-depth. |
.prose-suggestion-group | li[role="group"] | A heading and its options, written when some suggestion has a group. |
.prose-suggestion-group-label | span | The group heading. |
.prose-select | select | A closed choice such as an operator. |
.prose-rule-kind | select | The select that sets the kind of a rule: may, must not or must. It is next to .prose-select. |
.prose-issue | span | An issue message the host passed in issues or refusal. |
.prose-issue-error, .prose-issue-warning, .prose-issue-info | span | The 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.
| Selector | Element | What it styles |
|---|---|---|
.prose-problems | section | The panel root. The class prop is appended here. |
.prose-problems-heading | h3 | The heading. It takes focus after a fix removed the focused button. |
.prose-problems-status | p[role="status"] | The verdict, for a strict report only, with one of .prose-conforms, .prose-incomplete, .prose-violations-found. |
.prose-problems-counts | p | The counts of errors, warnings and notes. |
.prose-problem-groups | ol | The groups, one per rule. |
.prose-problem-group | li | One rule: an h4 with the rule id and title, then its findings. |
.prose-problem-list | ol | The findings of one rule. |
.prose-problem | li | One finding. |
.prose-problem-error, .prose-problem-warning, .prose-problem-info | li | The severity, next to .prose-problem. |
.prose-problem-gap | li | The finding is a gap: something could not be checked. |
.prose-problem-message | span | The message. |
.prose-problem-go | button | Go to the target. The host moves focus. |
.prose-problem-fix | button[data-safe] | One per available fix. data-safe="false" marks a fix that is not lossless. |
Internal hooks #
| Selector | Element | Why it is internal |
|---|---|---|
.prose-slot-edit | span | Inner wrapper of a slot. BASE_CSS has no rule for it. |
--prose-depth | custom property | Set 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 #
| Property | Read by | Fallback | Set by |
|---|---|---|---|
--prose-error | BASE_CSS: the invalid underline and the error issue text | #c9190b | The host, on any ancestor or on .ds-prose. |
--prose-warning | BASE_CSS: the warning underline and the warning issue text | #b45309 | The 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);
}