It looks like you're offline.
Open Library logo

Actions

Button <ol-button>

Variants

Secondary is the default — a bare <ol-button> renders as secondary.

Secondary Primary Destructive Ghost
<ol-button>
    Secondary
</ol-button>
<ol-button variant="primary">
    Primary
</ol-button>
<ol-button variant="destructive">
    Destructive
</ol-button>
<ol-button variant="ghost">
    Ghost
</ol-button>

Links

Set href and the component renders an <a> with the same styling — for button-shaped navigation like Read, Borrow, or Find in a library. disabled and loading drop the href and set aria-disabled.

Read Borrow Find in a library Unavailable
<ol-button variant="primary" href="/search?q=hobbit">
    Read
</ol-button>
<ol-button href="/search?q=hobbit">
    Borrow
</ol-button>
<ol-button href="/search?q=hobbit" target="_blank" rel="noopener">
    Find in a library
</ol-button>
<ol-button href="/search?q=hobbit" disabled>
    Unavailable
</ol-button>

Icon-only shapes

shape="icon" makes a square whose side matches the size's control height; shape="circle" rounds it. Always give these an aria-label.

<ol-button shape="icon" aria-label="Search">
  <svg ...></svg>
</ol-button>
<ol-button shape="circle" size="small" aria-label="Save">
  <svg ...></svg>
</ol-button>

Floating

elevation="floating" swaps in a heavier drop shadow for a control that sits over content — a save button on cover art, a scroll-to-top button — instead of on the page surface.

Back to top
<ol-button shape="circle" elevation="floating" aria-label="Save">
    <svg width="16"
         height="16"
         viewBox="0 0 24 24"
         fill="none"
         stroke="currentColor"
         stroke-width="2"
         stroke-linecap="round"
         stroke-linejoin="round"
         aria-hidden="true">
        <path d="M12 5v14M5 12h14" />
    </svg>
</ol-button>
<ol-button elevation="floating">
    Back to top
</ol-button>

Sizes

Heights come from the shared --control-height-* tokens, so each size lines up with the same-size segmented control and input.

Small Medium Large
<ol-button size="small">
    Small
</ol-button>
<ol-button>
    Medium
</ol-button>
<ol-button size="large">
    Large
</ol-button>

With icon

Put an inline SVG in the icon-start or icon-end slot. The button sizes it to match (14/16/18px by size) and adds the gap, so the SVG needs no width, height, or margin of its own.

Preview Search Inside Next
<ol-button size="small">
  <svg slot="icon-start" ...></svg>
  Preview
</ol-button>

<ol-button>
  Next
  <svg slot="icon-end" ...></svg>
</ol-button>

Disabled

Secondary Primary Destructive
<ol-button disabled>
    Secondary
</ol-button>
<ol-button variant="primary" disabled>
    Primary
</ol-button>
<ol-button variant="destructive" disabled>
    Destructive
</ol-button>

Loading

Shows a spinner and blocks interaction. The label stays in the DOM so the button's width doesn't shift.

Save Cancel Delete
<ol-button variant="primary" loading>
    Save
</ol-button>
<ol-button loading>
    Cancel
</ol-button>
<ol-button variant="destructive" loading>
    Delete
</ol-button>

Loading, driven from JS

Click for a 1.5s fake request. Setting .loading disables the button for the duration.

Save
button.loading = true;
await save();
button.loading = false;

Full width

Continue
<ol-button full-width>
    Continue
</ol-button>

Form submission

The element is form-associated and keeps a hidden native submit button in its light DOM, so it behaves like a native button: type="submit" submits the enclosing form (native validation runs, Enter in a text field submits, event.submitter is set, name/value are submitted, formaction and friends work), type="reset" resets it, and preventDefault() on a click cancels either. A disabled fieldset disables it too.

Submit Reset

Disable fieldset

Last submitted: (submits: 0)

<form id="demo-button-form">
    <fieldset id="demo-button-form-fieldset" class="ds-demo-row">
        <label for="demo-button-form-input">Your name:</label>
        <input id="demo-button-form-input" name="name" required>
        <label for="demo-button-form-input-2">Nickname:</label>
        <input id="demo-button-form-input-2" name="nickname">
        <ol-button type="submit" name="action" value="save">
            Submit
        </ol-button>
        <ol-button type="reset" variant="secondary">
            Reset
        </ol-button>
    </fieldset>
</form>
<p>
    <ol-toggle id="demo-button-form-disable" variant="button">
        Disable fieldset
    </ol-toggle>
</p>
<p>Last submitted: <strong id="demo-button-form-output">—</strong> (submits: <span id="demo-button-form-count">0</span>)</p>
<script>
    (function () {
        const form = document.getElementById('demo-button-form');
        const out = document.getElementById('demo-button-form-output');
        const count = document.getElementById('demo-button-form-count');
        const fieldset = document.getElementById('demo-button-form-fieldset');
        let submits = 0;
        form.addEventListener('submit', function (e) {
            e.preventDefault();
            count.textContent = ++submits;
            // Pass the submitter so its name/value are included, as they would be on a real submission.
            const data = new FormData(form, e.submitter);
            const via = e.submitter ? e.submitter.closest('ol-button').textContent.trim() : 'no submitter';
            out.textContent = (data.get('name') || '—') + ' [' + data.get('action') + ', via ' + via + ']';
        });
        form.addEventListener('reset', function () { out.textContent = '—'; });
        document.getElementById('demo-button-form-disable').addEventListener('ol-toggle-change', function (e) {
            fieldset.disabled = e.detail.checked;
        });
    })();
</script>

Styling from outside

Two parts are exposed: control (the inner button or link) and label (the span around the slotted content). Use them for layout tweaks a consumer legitimately owns — like clamping a long label — never to restyle the button itself.

A very long label that gets clamped
.filter-trigger ol-button::part(label) {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

Events. None custom — use the native click, which bubbles from the inner <button>.

Rendering. Shadow DOM. The component renders and paints a real <button> (or <a>) in its shadow root, so it works inside any other component's shadow root too. Before upgrade the host tag is styled by components/ol-button.css (ol-button:not(:defined), loaded render-blocking site-wide) so server-rendered buttons look right on first paint. Style from outside via ::part(control) and ::part(label), never by reaching for the inner element.

API reference
Properties
PropertyAttributeTypeDefaultDescription
variant variant "primary" | "secondary" | "destructive" | "ghost" 'secondary' Default: "secondary". Ghost is transparent with no border or lift; it fills on hover.
size size "small" | "medium" | "large" 'medium' Default: "medium"
type type "button" | "submit" | "reset" 'button' Default: "button"
loading loading Boolean false Shows a spinner and disables interaction.
disabled disabled Boolean false Disables interaction.
fullWidth full-width Boolean false Button expands to fill its container.
shape shape "icon" | "circle" Icon-only: width equals the size's height, no horizontal padding. "circle" additionally rounds it. Give it an aria-label.
elevation elevation "floating" Heavier drop shadow for a control that sits over content (e.g. a save button on cover art) rather than on the page.
name name string Submitted with `value` when this button submits the form.
value value String See `name`.
href href String Renders an <a> instead of a <button>.
target target String Link target (only with href).
rel rel String Link rel (only with href).
download download String Link download attribute (only with href).
isDisabled Whether the control is disabled from either source: its own `disabled` property or an ancestor `<fieldset disabled>`. Use this — not `disabled` — to gate interaction and to set `?disabled` on inner controls.
formAssociatedValue Override point. The value(s) to submit with the form.
Slots
SlotDescription
(default) Default slot carries the button label.
icon-start Leading icon (an inline SVG). Sized to the button's size (14/16/18px) and separated from the label by a 4px gap only when filled.
icon-end Trailing icon, same treatment. Not for the disclosure chevron, which is automatic on popover triggers.
CSS parts
PartDescription
control The inner <button> or <a>.
label The span wrapping the slotted label.

Toggle <ol-toggle>

Avoid: For picking one of several options use Segmented Control.

Default

The switch, a label, and an optional greyed sublabel.

<ol-toggle label="Readable Only" sublabel="4.6M">
</ol-toggle>
<ol-toggle label="Readable Only" sublabel="4.6M" checked>
</ol-toggle>

Button variant

A bordered, raised container styled like ol-button variant="secondary". When checked it fills with a soft blue tint, matching a selected row in ol-select-popover.

<ol-toggle variant="button" label="Readable Only" sublabel="4.6M">
</ol-toggle>
<ol-toggle variant="button" label="Readable Only" sublabel="4.6M" checked>
</ol-toggle>

Custom label content

Put content in the default slot instead of using label / sublabel. Supply accessible-label so the switch still has an accessible name.

Dark mode — easier on the eyes
<ol-toggle accessible-label="Dark mode">
    <strong>Dark mode</strong> — easier on the eyes
</ol-toggle>

Disabled

<ol-toggle label="Readable Only" sublabel="4.6M" disabled>
</ol-toggle>
<ol-toggle variant="button" label="Readable Only" sublabel="4.6M" checked disabled>
</ol-toggle>

Change event

Toggling fires ol-toggle-change with the new state in event.detail.checked.

Checked: false

<ol-toggle id="demo-toggle" variant="button" label="Readable Only" sublabel="4.6M">
</ol-toggle>
<p>Checked: <strong id="demo-toggle-state">false</strong></p>
<script>
    document.getElementById('demo-toggle').addEventListener('ol-toggle-change', (e) => {
        document.getElementById('demo-toggle-state').textContent = e.detail.checked;
    });
</script>
API reference
Properties
PropertyAttributeTypeDefaultDescription
formAssociatedValue Override point. The value(s) to submit with the form.
checked checked Boolean false On/off state. Default false.
disabled disabled Boolean false Disables interaction. Default false.
variant variant "button" Omit for the default (plain) toggle, or "button" for a bordered, raised container styled like ol-button[variant="secondary"] (subtle drop shadow, inset specular edge on hover) that fills with a soft blue tint when checked.
label label String Primary label text.
sublabel sublabel String Secondary greyed text shown after the label.
accessibleLabel accessible-label String Override aria-label on the switch. Needed when supplying label content via the default slot.
value value String 'on' Value submitted when checked. Default "on".
name name string Form field name. When set and the toggle is checked, it submits with the enclosing `<form>` (see FormAssociatedMixin).
isDisabled Whether the control is disabled from either source: its own `disabled` property or an ancestor `<fieldset disabled>`. Use this — not `disabled` — to gate interaction and to set `?disabled` on inner controls.
Events
EventTypeDescription
ol-toggle-change CustomEvent Fired on toggle. detail: { checked: Boolean }
Slots
SlotDescription
(default) Custom label content. Overrides the label/sublabel properties when provided (they render as the slot's fallback content).

Segmented Control <ol-segmented-control>

Avoid: More than about four options belong in a Select Popover.

Options are <ol-segment> children with a value; their text is the label. The component reads them once and renders accessible radios — no per-option wiring. Exactly one option is always selected.

Default

Grid List Compact
<ol-segmented-control value="list" accessible-label="View">
    <ol-segment value="grid">
        Grid
    </ol-segment>
    <ol-segment value="list">
        List
    </ol-segment>
    <ol-segment value="compact">
        Compact
    </ol-segment>
</ol-segmented-control>

Sizes

Heights track the shared --control-height-* tokens, so each size lines up with the same-size button.

Grid List Grid List Grid List
<ol-segmented-control size="small" accessible-label="View (small)">
    <ol-segment value="grid">
        Grid
    </ol-segment>
    <ol-segment value="list">
        List
    </ol-segment>
</ol-segmented-control>
<ol-segmented-control accessible-label="View (medium)">
    <ol-segment value="grid">
        Grid
    </ol-segment>
    <ol-segment value="list">
        List
    </ol-segment>
</ol-segmented-control>
<ol-segmented-control size="large" accessible-label="View (large)">
    <ol-segment value="grid">
        Grid
    </ol-segment>
    <ol-segment value="list">
        List
    </ol-segment>
</ol-segmented-control>

Icons

A segment's content can be markup instead of text — use <ol-icon>, not the icon macro, since the control re-renders each segment inside its shadow root. Give icon-only segments a label attribute so the radio is named. Icons inherit the segment's color, so they track selected and hover states for free.

<ol-segmented-control value="grid" accessible-label="View">
  <ol-segment value="grid" label="Grid">
    <ol-icon name="layout-grid"></ol-icon>
  </ol-segment>
  <ol-segment value="list" label="List">
    <ol-icon name="list"></ol-icon>
  </ol-segment>
</ol-segmented-control>

Disabled

Disable the whole control, or a single option with disabled on its <ol-segment>.

Grid List Day Week Year
<ol-segmented-control disabled accessible-label="Disabled control">
    <ol-segment value="grid">
        Grid
    </ol-segment>
    <ol-segment value="list">
        List
    </ol-segment>
</ol-segmented-control>
<ol-segmented-control accessible-label="Range">
    <ol-segment value="day">
        Day
    </ol-segment>
    <ol-segment value="week">
        Week
    </ol-segment>
    <ol-segment value="year" disabled>
        Year
    </ol-segment>
</ol-segmented-control>

Full width

Options share the container width equally.

Ebook Audiobook Print
<ol-segmented-control full-width accessible-label="Format">
    <ol-segment value="ebook">
        Ebook
    </ol-segment>
    <ol-segment value="audiobook">
        Audiobook
    </ol-segment>
    <ol-segment value="print">
        Print
    </ol-segment>
</ol-segmented-control>

Change event

Selecting fires ol-segmented-control-change with the new value in event.detail.value. Click, or focus the control and use the arrow / Home / End keys.

Grid List Compact

Selected value: grid

<ol-segmented-control id="demo-segmented" accessible-label="View">
    <ol-segment value="grid">
        Grid
    </ol-segment>
    <ol-segment value="list">
        List
    </ol-segment>
    <ol-segment value="compact">
        Compact
    </ol-segment>
</ol-segmented-control>
<p>Selected value: <strong id="demo-segmented-output">grid</strong></p>
<script>
    document.getElementById('demo-segmented').addEventListener('ol-segmented-control-change', (e) => {
        document.getElementById('demo-segmented-output').textContent = e.detail.value;
    });
</script>

Keyboard & accessibility. Renders as a radiogroup of radios. Tab moves into the selected option; arrow keys and Home / End move the selection between enabled options, wrapping at the ends. Disabled options are skipped. Always provide an accessible-label.

API reference
Properties
PropertyAttributeTypeDefaultDescription
formAssociatedValue Override point. The value(s) to submit with the form.
value value String The selected option's value. Reflected; defaults to the first enabled option.
size size "small" | "medium" | "large" 'medium' Default: "medium"
disabled disabled Boolean false Disables the whole control.
fullWidth full-width Boolean false Stretch to fill the container; options share the width equally.
accessibleLabel accessible-label String aria-label for the radio group. Default: none.
name name string Form field name. When set, the selected `value` submits with the enclosing `<form>` (see FormAssociatedMixin).
isDisabled Whether the control is disabled from either source: its own `disabled` property or an ancestor `<fieldset disabled>`. Use this — not `disabled` — to gate interaction and to set `?disabled` on inner controls.
Events
EventTypeDescription
ol-segmented-control-change CustomEvent Fired on user selection. detail: { value: String }
Slots
SlotDescription
(default) One or more <ol-segment value="…">Label</ol-segment> option elements. A segment's content may be text or markup (e.g. an icon); add a `label` attribute on icon-only segments for the accessible name.

Chip <ol-chip>

Default

Medium is the default size.

Fiction Science History
<ol-chip>
    Fiction
</ol-chip>
<ol-chip>
    Science
</ol-chip>
<ol-chip>
    History
</ol-chip>

Selected

A selected chip shows a checkmark and switches to the active color scheme.

Fiction History Science
<ol-chip>
    Fiction
</ol-chip>
<ol-chip selected>
    History
</ol-chip>
<ol-chip>
    Science
</ol-chip>

Small size

Poetry Drama Essays
<ol-chip size="small">
    Poetry
</ol-chip>
<ol-chip size="small" selected>
    Drama
</ol-chip>
<ol-chip size="small">
    Essays
</ol-chip>

With count

Fiction History
<ol-chip count="1,024">
    Fiction
</ol-chip>
<ol-chip count="76" selected>
    History
</ol-chip>

As a link

With href the chip renders as an anchor instead of a button.

Fiction History
<ol-chip href="/subjects/fiction" count="1,024">
    Fiction
</ol-chip>
<ol-chip href="/subjects/history" count="76" selected>
    History
</ol-chip>

Domain variants

Colors a chip by the kind of thing it represents. The chip maps the variant to a soft-tint palette internally — see the chip tokens under Foundations → Colors.

English Science Fiction Memoir Ursula K. Le Guin San Francisco Read now
<ol-chip variant="language">
    English
</ol-chip>
<ol-chip variant="subject">
    Science Fiction
</ol-chip>
<ol-chip variant="genre">
    Memoir
</ol-chip>
<ol-chip variant="author">
    Ursula K. Le Guin
</ol-chip>
<ol-chip variant="place">
    San Francisco
</ol-chip>
<ol-chip variant="neutral">
    Read now
</ol-chip>

Removable filter pills

variant plus selected makes a removable, category-colored filter pill. The selected state adds a close icon; listen for ol-chip-select to remove the filter.

Read now English French
<ol-chip variant="neutral" size="small" selected>
    Read now
</ol-chip>
<ol-chip variant="language" size="small" selected>
    English
</ol-chip>
<ol-chip variant="language" size="small" selected>
    French
</ol-chip>

Interactive toggle

Clicking fires ol-chip-select. The chip does not toggle itself — flip .selected in your handler.

Toggle me Toggle me too
chip.addEventListener('ol-chip-select', (e) => {
  e.target.selected = !e.target.selected;
});
API reference
Properties
PropertyAttributeTypeDefaultDescription
selected selected Boolean false Whether the chip is in a selected state
size size "small" | "medium" 'medium' Default: "medium"
variant variant "language" | "subject" | "genre" | "author" | "place" | "neutral" Domain category that tints the chip. Omit for the default (white / solid-blue-when-selected) chip. The chip maps the variant to a soft-tint palette internally (see colors.css); a variant chip keeps its tint when `selected` and just gains a close icon.
href href String When set, the chip renders as a link
count count String Optional count displayed to the right of the label
accessibleLabel accessible-label String Override aria-label on the inner interactive element
Events
EventTypeDescription
ol-chip-select CustomEvent Fired on click. detail: { selected: Boolean }
Slots
SlotDescription
(default) The chip's label content

Chip Group <ol-chip-group>

Default

Chips are spaced with an 8px gap and wrap when they exceed the container width.

Fiction Science History Poetry Drama
<ol-chip-group>
    <ol-chip>
        Fiction
    </ol-chip>
    <ol-chip>
        Science
    </ol-chip>
    <ol-chip>
        History
    </ol-chip>
    <ol-chip>
        Poetry
    </ol-chip>
    <ol-chip>
        Drama
    </ol-chip>
</ol-chip-group>

Gap

small is 4px, large is 12px.

Fiction Science History Fiction Science History
<ol-chip-group gap="small">
    <ol-chip size="small">
        Fiction
    </ol-chip>
    <ol-chip size="small">
        Science
    </ol-chip>
    <ol-chip size="small">
        History
    </ol-chip>
</ol-chip-group>
<ol-chip-group gap="large">
    <ol-chip>
        Fiction
    </ol-chip>
    <ol-chip>
        Science
    </ol-chip>
    <ol-chip>
        History
    </ol-chip>
</ol-chip-group>

Wrapping

Narrow the container and chips flow onto the next line.

Fiction Science History Poetry Drama Essays Biography
<ol-chip-group>
    <ol-chip>
        Fiction
    </ol-chip>
    <ol-chip>
        Science
    </ol-chip>
    <ol-chip>
        History
    </ol-chip>
    <ol-chip>
        Poetry
    </ol-chip>
    <ol-chip>
        Drama
    </ol-chip>
    <ol-chip>
        Essays
    </ol-chip>
    <ol-chip>
        Biography
    </ol-chip>
</ol-chip-group>

Mixed chip states

Any combination of sizes, states, and link chips works.

Fiction History Science Poetry
<ol-chip-group>
    <ol-chip href="/subjects/fiction" count="1,024">
        Fiction
    </ol-chip>
    <ol-chip href="/subjects/history" count="76" selected>
        History
    </ol-chip>
    <ol-chip href="/subjects/science" count="512">
        Science
    </ol-chip>
    <ol-chip href="/subjects/poetry" count="33">
        Poetry
    </ol-chip>
</ol-chip-group>
API reference
Properties
PropertyAttributeTypeDefaultDescription
gap gap "small" | "medium" | "large" 'medium' "small" (4px), "medium" (8px, default), or "large" (12px)
Slots
SlotDescription
(default) One or more <ol-chip> elements

Pagination <ol-pagination>

Event-based

Clicking a page fires update:page with the page number in event.detail. The component does not move itself — set current-page in your handler.

Selected page: 1

<ol-pagination id="demo-pagination" total-pages="10" current-page="1">
</ol-pagination>
<p>Selected page: <strong id="demo-selected-page">1</strong></p>
<script>
    document.getElementById('demo-pagination').addEventListener('update:page', (e) => {
        document.getElementById('demo-selected-page').textContent = e.detail;
        e.target.setAttribute('current-page', e.detail);
    });
</script>

Sizes

Use size to match the surrounding controls. Heights track the shared --control-height-* tokens, so each size lines up with the same-size button. Medium is the default.

<ol-pagination size="small" total-pages="10" current-page="3" label-pagination="Pagination sizes, small">
</ol-pagination>
<ol-pagination total-pages="10" current-page="3" label-pagination="Pagination sizes, medium">
</ol-pagination>

URL-based navigation

With base-url the component renders anchors instead, for SEO-friendly links.

<ol-pagination total-pages="50" current-page="5" base-url="/search?q=example">
</ol-pagination>

Edges and ellipsis

Arrows drop out at the first and last page; long ranges collapse to an ellipsis.

<ol-pagination total-pages="20" current-page="1">
</ol-pagination>
<ol-pagination total-pages="20" current-page="10">
</ol-pagination>
<ol-pagination total-pages="20" current-page="20">
</ol-pagination>
<ol-pagination total-pages="3" current-page="2">
</ol-pagination>
<ol-pagination total-pages="5" current-page="3">
</ol-pagination>
<ol-pagination total-pages="100" current-page="50">
</ol-pagination>

Arrows mode

When the total isn't known, show only previous/next. has-next-page controls whether the forward arrow is live.

<ol-pagination mode="arrows" current-page="3" has-next-page>
</ol-pagination>
<ol-pagination mode="arrows" current-page="3">
</ol-pagination>
<ol-pagination mode="arrows" current-page="1" has-next-page>
</ol-pagination>
API reference
Properties
PropertyAttributeTypeDefaultDescription
mode mode "full" | "arrows" 'full' Display mode: "full" (default) shows page numbers with arrows, "arrows" shows only previous/next arrows (useful when total is unknown)
size size "small" | "medium" 'medium' Default: "medium". Heights track the shared control-height tokens, so a same-size button lines up.
totalPages total-pages Number 1 Total number of pages (required for "full" mode)
currentPage current-page Number 1 Currently selected page (1-indexed)
hasNextPage has-next-page Boolean false Whether a next page exists. Only used in "arrows" mode. In "full" mode, this is derived from totalPages.
baseUrl base-url String '' Optional base URL for generating page links. When omitted, falls back to the current page URL, preserving all existing query parameters (similar to changequery). Always renders anchor tags for SEO-friendly navigation.
labelPreviousPage label-previous-page String 'Go to previous page' Aria label for "previous page" button (default: "Go to previous page")
labelNextPage label-next-page String 'Go to next page' Aria label for "next page" button (default: "Go to next page")
labelGoToPage label-go-to-page String 'Go to page {page}' Aria label template for page buttons, use {page} as placeholder (default: "Go to page {page}")
labelCurrentPage label-current-page String 'Page {page}, current page' Aria label template for current page, use {page} as placeholder (default: "Page {page}, current page")
labelPagination label-pagination String 'Pagination' Aria label for the navigation landmark (default: "Pagination")
Events
EventTypeDescription
ol-pagination-change Fired when a page is selected. detail: { page: Number }

Overlays

Tooltip <ol-tooltip>

Avoid: Never put essential information or interactive content in a tooltip.

Default

Hover or focus the trigger. Placement is top with a 150ms show delay.

<ol-tooltip content="Edit this item">
    <button class="demo-btn">Edit</button>
</ol-tooltip>
<ol-tooltip content="Delete permanently">
    <button class="demo-btn">Delete</button>
</ol-tooltip>
<ol-tooltip content="Share with others">
    <button class="demo-btn">Share</button>
</ol-tooltip>

Placement

Flips automatically near viewport edges. Add arrow to show the directional arrow, hidden by default.

<ol-tooltip content="I'm on top" placement="top" arrow>
    <button class="demo-btn">top</button>
</ol-tooltip>
<ol-tooltip content="I'm on the bottom" placement="bottom" arrow>
    <button class="demo-btn">bottom</button>
</ol-tooltip>
<ol-tooltip content="I'm on the left" placement="left" arrow>
    <button class="demo-btn">left</button>
</ol-tooltip>
<ol-tooltip content="I'm on the right" placement="right" arrow>
    <button class="demo-btn">right</button>
</ol-tooltip>

Rich content via slot

Use slot="content" for HTML inside the tooltip.

Keyboard shortcut: Ctrl+K
<ol-tooltip>
    <button class="demo-btn">Info</button>
    <span slot="content">Keyboard shortcut: <strong>Ctrl+K</strong></span>
</ol-tooltip>

Book hovercard

Slotted content lives in the light DOM, so page CSS styles it. Inherited properties like color cascade in from the tooltip; structural styles come from the consuming page's stylesheet.

Statistical methods for planners
Statistical methods for planners (1980)
Prescott's Microbiology
Prescott's Microbiology (2013)
Encyclopaedia of hand-weaving
Encyclopaedia of hand-weaving (1959)
<ol-tooltip placement="bottom" arrow>
  <img class="book-tip__trigger"
        width="120"
        height="180"
        src="…"
        alt="Cover">
  <div slot="content" class="book-tip">…</div>
</ol-tooltip>
API reference
Properties
PropertyAttributeTypeDefaultDescription
content content String '' Text content of the tooltip
placement placement String 'top' Preferred placement relative to the trigger. Format: "{side}" or "{side}-{align}" where side is "top", "bottom", "left", or "right" and align is "start", "center", or "end". Default: "top"
showDelay show-delay Number 150 Milliseconds to wait before showing (default: 150). Set to 0 for an instant tooltip.
hideDelay hide-delay Number 0 Milliseconds to wait before hiding (default: 0)
offset offset Number 4 Gap in px between trigger and tooltip (default: 4)
arrow arrow Boolean false Shows the directional arrow (hidden by default)
disabled disabled Boolean false Prevents the tooltip from showing
Events
EventTypeDescription
ol-tooltip-show CustomEvent Fired when the tooltip opens
ol-tooltip-hide CustomEvent Fired when the tooltip closes
Slots
SlotDescription
(default) The trigger element (button, icon, link, etc.)
content Optional rich HTML tooltip content (overrides the content attribute)

Popover <ol-popover>

Menu popover

Popovers animate from the trigger using transform-origin. Click to see the scale + fade entrance.

Options
  • Edit
  • Duplicate
  • Archive
  • Move
  • Share
  • Delete
<ol-popover>
  <ol-button slot="trigger">Options</ol-button>
  <ul>...</ul>
</ol-popover>

Placement

transform-origin follows placement, so the animation always expands from the trigger.

bottom-start
  • Edit
  • Duplicate
  • Archive
  • Move
  • Share
  • Delete
top-center
  • Edit
  • Duplicate
  • Archive
  • Move
  • Share
  • Delete
<ol-popover placement="bottom-start" aria-label="Actions">
        <ol-button slot="trigger">
            bottom-start
        </ol-button>
            <ul class="ds-demo-menu" style="min-width: 160px;">
    <li>Edit</li>
    <li>Duplicate</li>
    <li class="ds-demo-menu__divided">Archive</li>
    <li>Move</li>
    <li>Share</li>
    <li class="ds-demo-menu__divided">Delete</li>
</ul>

    </ol-popover>
    <ol-popover placement="top-center" aria-label="Actions">
        <ol-button slot="trigger">
            top-center
        </ol-button>
            <ul class="ds-demo-menu" style="min-width: 160px;">
    <li>Edit</li>
    <li>Duplicate</li>
    <li class="ds-demo-menu__divided">Archive</li>
    <li>Move</li>
    <li>Share</li>
    <li class="ds-demo-menu__divided">Delete</li>
</ul>

    </ol-popover>
API reference
Properties
PropertyAttributeTypeDefaultDescription
open open Boolean false Whether the popover is currently open
placement placement String 'bottom-center' Preferred placement relative to the trigger. Format: "{side}-{align}" where side is "top" or "bottom" and align is "start", "center", or "end". Default: "bottom-center"
offset offset Number 4 Gap in px between trigger and popover (default: 4)
autoClose auto-close Boolean true Whether outside clicks close the popover. Escape always closes for accessibility. Default: true
Events
EventTypeDescription
ol-popover-open CustomEvent Fired when the popover opens. detail: { placement: String }
ol-popover-close CustomEvent Cancelable. Fired when the popover requests to close. Call `preventDefault()` to keep it open. Note: the swipe-dismiss close fires after the gesture completes and is not cancelable. detail: { reason: 'escape' | 'outside-click' | 'swipe' | 'trigger' | 'tab' }
Slots
SlotDescription
trigger The trigger element (button, icon, etc.)
(default) Default slot for popover content

Select Popover <ol-select-popover>

Default

Above 8 items a filter input renders automatically. Selected items move to a SELECTED group on next open.

Language

Selected: (none)

<ol-select-popover
  label="Language"
  placeholder="Filter languages…"
  unselected-heading="LANGUAGES"
  items='[{"value":"en","label":"English"}, ...]'>
  <ol-button slot="trigger">Language</ol-button>
</ol-select-popover>

Small list, no search

At 8 items or fewer the filter input is hidden. Override with search-threshold — 0 always shows it, a large value always hides it.

Layout
<ol-select-popover id="demo-select-layout" label="Layout" unselected-heading="LAYOUTS" aria-label="Layout filter">
    <ol-button slot="trigger">
        Layout
    </ol-button>
</ol-select-popover>
<script>
    document.getElementById('demo-select-layout').items = [
        { value: 'grid', label: 'Grid' }, { value: 'list', label: 'List' }, { value: 'compact', label: 'Compact' }
    ];
</script>

Custom trigger

Any element with slot="trigger" works; the component handles open/close and ARIA wiring.

Pick a genre
<ol-select-popover id="demo-select-custom" label="Genre" unselected-heading="GENRES" aria-label="Genre filter">
    <a slot="trigger"
       href="#"
       role="button"
       class="ds-demo-link-trigger"
       onclick="event.preventDefault();">Pick a genre</a>
</ol-select-popover>
<script>
    document.getElementById('demo-select-custom').items = [
        { value: 'fic', label: 'Fiction' }, { value: 'nf', label: 'Non-fiction' },
        { value: 'sci', label: 'Science' }, { value: 'his', label: 'History' },
        { value: 'bio', label: 'Biography' }
    ];
</script>

Paired with a chip group

Selections render as chips next to the trigger. Clicking a chip's close icon removes that selection from the popover. Keep the chip group derived from the popover rather than giving it its own state, so there is one place to read the current selection.

Language
<div class="ds-demo-row">
    <ol-select-popover id="demo-select-chips" label="Language" placeholder="Filter languages…" unselected-heading="LANGUAGES" aria-label="Language filter">
        <ol-button slot="trigger">
            Language
        </ol-button>
    </ol-select-popover>
    <ol-chip-group id="demo-select-chips-group" gap="small">
    </ol-chip-group>
</div>
<script>
    (function () {
        const popover = document.getElementById('demo-select-chips');
        const chipGroup = document.getElementById('demo-select-chips-group');
        const items = [
            { value: 'en', label: 'English' }, { value: 'es', label: 'Spanish' },
            { value: 'fr', label: 'French' }, { value: 'de', label: 'German' },
            { value: 'it', label: 'Italian' }, { value: 'pt', label: 'Portuguese' },
            { value: 'zh', label: 'Chinese' }, { value: 'ja', label: 'Japanese' },
            { value: 'ru', label: 'Russian' }
        ];
        popover.items = items;
        const labelByValue = Object.fromEntries(items.map((item) => [item.value, item.label]));

        function renderChips(selected) {
            chipGroup.innerHTML = '';
            selected.forEach((value) => {
                const chip = document.createElement('ol-chip');
                chip.setAttribute('selected', '');
                chip.setAttribute('size', 'small');
                chip.dataset.value = value;
                chip.textContent = labelByValue[value] || value;
                chipGroup.appendChild(chip);
            });
        }

        popover.addEventListener('ol-select-popover-change', (e) => renderChips(e.detail.selected));
        // A selected chip's close icon fires ol-chip-select with detail.selected false.
        chipGroup.addEventListener('ol-chip-select', (e) => {
            if (e.detail.selected !== false) return;
            const next = (popover.selected || []).filter((value) => value !== e.target.dataset.value);
            popover.selected = next;
            renderChips(next);
        });
    })();
</script>
API reference
Properties
PropertyAttributeTypeDefaultDescription
formAssociatedValue Override point. The value(s) to submit with the form.
items items Array [] List of `{ value, label }` objects. Settable as JSON attribute (`items='[{"value":"en","label":"English"}]'`) or property.
selected selected Array [] Array of selected `value`s. Reflects to attribute as JSON.
label label String '' Default trigger button text (e.g. "Language").
searchThreshold search-threshold Number 8 Show the filter input when `items.length` exceeds this value. Default `8`. Use `0` to always show, a large number (e.g. `999`) to never show. Attribute: `search-threshold`.
placeholder placeholder String 'Filter…' Filter input placeholder.
unselectedHeading unselected-heading String '' Heading for the list when nothing is selected (e.g. "LANGUAGES"). Falls back to `suggestionsHeading` if unset.
selectedHeading selected-heading String 'SELECTED' Heading for the SELECTED group (default "SELECTED").
suggestionsHeading suggestions-heading String 'SUGGESTIONS' Heading for the suggestions group when ≥1 item is selected (default "SUGGESTIONS").
clearLabel clear-label String 'Clear selections' Label for the clear-selections button (default "Clear selections").
noMatchesLabel no-matches-label String 'No matches' Empty-state text when the filter has no matches (default "No matches").
loadingLabel loading-label String 'Loading…' Text shown beside the spinner while `loading` is set (default "Loading…").
name name string Form field name. When set, each selected value submits with the enclosing `<form>` as a repeated `name` entry (see FormAssociatedMixin).
isDisabled Whether the control is disabled from either source: its own `disabled` property or an ancestor `<fieldset disabled>`. Use this — not `disabled` — to gate interaction and to set `?disabled` on inner controls.
Events
EventTypeDescription
ol-select-popover-clear CustomEvent Fires when the clear-selections button is clicked. A change event also fires with the cleared selection.
ol-select-popover-change CustomEvent Fires when the selection changes. detail: { selected: String[], added: String|null, removed: String|null }
ol-select-popover-request-open Cancelable; fires when the patron activates the trigger on a closed panel. detail: { focusFirst: Boolean }. Calling preventDefault() defers the open: the panel stays shut until the listener calls show(). For hosts that load items on demand — the panel then opens once, at its final size, instead of resizing and re-sorting under the pointer. A host that defers owns any busy affordance, and should keep the wait short enough not to need one.
Slots
SlotDescription
trigger Optional custom trigger element. When omitted, a default `<ol-button>` is injected, labelled by the current selection: `label` when nothing is picked, the single item's own label when one is, and `label (n)` beyond that. It also carries ol-button's `selected` tint while a selection is active, and its disclosure chevron comes from ol-button automatically. A custom trigger owns its own label and state.

Options Popover <ol-options-popover>

Single-select menu

With a value and label per item the rows render as a tidy single-select menu — a sort order or layout switcher. The heading defaults to the uppercased label; override with heading.

Selected: relevance

<ol-options-popover
  label="Sort by"
  heading="SORT ORDER"
  selected="relevance"
  items='[{"value":"relevance","label":"Relevance"}, ...]'>
</ol-options-popover>

Descriptions and counts

Optional description and count per item. Both render only when present, so plain and rich rows can mix in one list.

<ol-options-popover id="demo-options-genre" label="Genre" selected="fiction" aria-label="Genre filter">
</ol-options-popover>
<script>
    document.getElementById('demo-options-genre').items = [
        { value: 'fiction', label: 'Fiction', description: 'Novels and short stories', count: '1,024' },
        { value: 'nonfiction', label: 'Nonfiction', description: 'Fact-based works and essays', count: '892' },
        { value: 'poetry', label: 'Poetry', description: 'Verse and collections', count: '213' },
        { value: 'reference', label: 'Reference', description: 'Dictionaries and encyclopedias', count: '156' }
    ];
</script>

Custom trigger

Supply your own trigger when the default button doesn't fit. The component still handles open/close, ARIA wiring, and keyboard navigation (arrows, Home/End, Escape).

<ol-options-popover id="demo-options-custom" label="Sort by" selected="newest" aria-label="Sort order">
    <button slot="trigger" class="demo-btn">Sort results</button>
</ol-options-popover>
<script>
    document.getElementById('demo-options-custom').items = [
        { value: 'relevance', label: 'Relevance' },
        { value: 'newest', label: 'Date added (newest)' },
        { value: 'oldest', label: 'Date added (oldest)' },
        { value: 'title', label: 'Title (A–Z)' }
    ];
</script>
API reference
Properties
PropertyAttributeTypeDefaultDescription
formAssociatedValue Override point. The value(s) to submit with the form.
items items Array [] List of `{ value, label, description?, count?, nested? }` objects. Settable as JSON attribute or property. `nested: true` indents the option to show it's a subset of the option above it.
selected selected String '' Currently selected `value`, or empty string for no selection. Reflects to attribute.
label label String '' Default trigger button text (e.g. "Availability").
heading heading String '' Heading shown above the options list (default: uppercased `label`).
name name string Form field name. When set, the selected value submits with the enclosing `<form>` (see FormAssociatedMixin).
isDisabled Whether the control is disabled from either source: its own `disabled` property or an ancestor `<fieldset disabled>`. Use this — not `disabled` — to gate interaction and to set `?disabled` on inner controls.
Events
EventTypeDescription
ol-options-popover-change CustomEvent Fires when the selection changes. detail: { selected: String }
Slots
SlotDescription
trigger Optional custom trigger element. When omitted, an `<ol-button>` showing `label` is injected (see _createDefaultTrigger); its disclosure chevron comes from ol-button automatically.

Dialog <ol-dialog>

Confirmation

Opening traps focus inside; closing restores focus to the trigger. The default header renders the title and a close button automatically.

Delete list…

Deleting “Want to Read” removes it permanently. This can't be undone.

Last action: (none)

<ol-dialog label="Delete this list?" width="small">
  <p>This can't be undone.</p>
  <div slot="footer">
    <ol-button variant="secondary" value="cancel">Cancel</ol-button>
    <ol-button variant="destructive" value="delete">Delete list</ol-button>
  </div>
</ol-dialog>

Width presets

small is 400px, medium 550px (default), large 800px. Never exceeds 90vw, so presets stay responsive. Override one instance with the --ol-dialog-width-* custom properties.

Small Medium Large

Using the medium preset. Resize the window to see it stay within 90vw.

<ol-button data-width="small" variant="secondary">
    Small
</ol-button>
<ol-button data-width="medium" variant="secondary">
    Medium
</ol-button>
<ol-button data-width="large" variant="secondary">
    Large
</ol-button>
<ol-dialog id="demo-dialog-width" label="Width demo">
    <p class="ds-demo-dialog-body">
        Using the <strong id="demo-dialog-width-label">medium</strong> preset. Resize the window to see it stay within 90vw.
    </p>
</ol-dialog>
<script>
    (function () {
        const dialog = document.getElementById('demo-dialog-width');
        const label = document.getElementById('demo-dialog-width-label');
        document.querySelectorAll('[data-width]').forEach((button) => {
            button.addEventListener('click', () => {
                dialog.width = button.dataset.width;
                label.textContent = button.dataset.width;
                dialog.open = true;
            });
        });
    })();
</script>

Form dialog

The focus trap cycles Tab between the fields, footer buttons, and close button — well suited to quick edit flows.

Add a note…
<ol-button id="demo-dialog-form-trigger" variant="secondary">
    Add a note…
</ol-button>
<ol-dialog id="demo-dialog-form" label="Add a note" width="medium">
    <label for="demo-dialog-note" class="ds-demo-label">Your note</label>
    <textarea id="demo-dialog-note"
              rows="3"
              class="ds-demo-textarea"
              placeholder="e.g. Recommended by a friend — start with chapter 3."></textarea>
    <div slot="footer" class="ds-demo-dialog-footer">
        <ol-button data-action="cancel" variant="secondary">
            Cancel
        </ol-button>
        <ol-button data-action="save" variant="primary">
            Save note
        </ol-button>
    </div>
</ol-dialog>
<script>
    (function () {
        const dialog = document.getElementById('demo-dialog-form');
        document.getElementById('demo-dialog-form-trigger').addEventListener('click', () => { dialog.open = true; });
        dialog.querySelectorAll('[data-action]').forEach((button) => {
            button.addEventListener('click', () => { dialog.open = false; });
        });
    })();
</script>

Command palette / search modal

placement="top" plus without-header, fullscreen-on-mobile, and --ol-dialog-padding: 0 slots a search bar into the header region and lets results grow downward while the top edge stays put. This is the site header's search modal. Focus the input on ol-after-open.

Search…
  • The Left Hand of Darkness — Ursula K. Le Guin
  • A Wizard of Earthsea — Ursula K. Le Guin
  • The Dispossessed — Ursula K. Le Guin
<ol-dialog
 placement="top"
  without-header
  fullscreen-on-mobile
  label="Search the catalog"
  style="--ol-dialog-padding: 0">
  <div slot="header"><input type="search" /></div>
  <ul>…results…</ul>
</ol-dialog>
API reference
Properties
PropertyAttributeTypeDefaultDescription
open open Boolean false Whether the dialog is open.
label label String '' Title shown in the default header. Also used as the accessible name when `withoutHeader` is true.
withoutHeader without-header Boolean false Hide the default header (title + close button). The `header` slot still works.
width width 'small' | 'medium' | 'large' 'medium' Width preset: `'small'` (400px), `'medium'` (550px, default), or `'large'` (800px). Override per-instance via `--ol-dialog-width-*` host CSS variables.
closeOnBackdropClick close-on-backdrop-click Boolean true Whether clicking the backdrop closes the dialog. Default `true`. Attribute: `close-on-backdrop-click`.
closeOnEscape close-on-escape Boolean true Whether pressing Escape closes the dialog. Default `true`. Attribute: `close-on-escape`.
fullscreenOnMobile fullscreen-on-mobile Boolean false At viewports ≤767px, render edge-to-edge (full viewport, no border-radius). Attribute: `fullscreen-on-mobile`.
placement placement 'center' | 'top' 'center' `'center'` (default) keeps the dialog vertically centered like a normal modal. `'top'` anchors it a fixed distance from the top of the viewport so the top edge stays put as content grows or shrinks (command-palette / search-modal pattern).
Events
EventTypeDescription
ol-open CustomEvent Fires when the dialog starts opening.
ol-after-open CustomEvent Fires after the open animation completes.
ol-after-close CustomEvent Fires after the close animation completes.
ol-close Fires when the dialog starts closing. Cancelable — calling `event.preventDefault()` keeps the dialog open.
Slots
SlotDescription
(default) Default slot for the dialog body.
header Optional custom header. When filled, replaces the default title + close-button row entirely. Useful for search bars, custom toolbars, etc.
footer Slot for action buttons. Footer region is hidden when empty.
CSS custom properties
PropertyDefaultDescription
--ol-dialog-padding Padding around body and footer regions. Set to `0` for edge-to-edge content (e.g. when slotting a search bar or filter row that owns its own padding).
--ol-dialog-border-radius Corner radius (ignored in fullscreen mode).
--ol-dialog-backdrop-color Backdrop color.
--ol-dialog-backdrop-blur Blur radius applied to the page behind the backdrop. Set to `0` to dim without blurring.
--ol-dialog-animation-duration Open/close animation duration.
--ol-dialog-top-offset Distance from viewport top when `placement="top"`. Default `clamp(40px, 8vh, 96px)`.

Drawer <ol-drawer>

Avoid: A centered interruption is a Dialog. A panel anchored to its trigger is a Popover.

Navigation drawer

Slides in from the right edge. Focus is trapped inside, background scroll is locked, and Escape or a backdrop click dismisses it. On touch devices you can also swipe it away.

Open menu…
  • My Books
  • My Profile
  • Settings
  • Subjects
  • Trending
  • Library Explorer
<ol-drawer label="Menu" placement="end">
  <nav>…</nav>
</ol-drawer>

<script>
  document.querySelector('ol-drawer').open = true;
</script>

Start placement

placement="start" slides from the left (right in RTL). Swipe-to-dismiss follows the placement, so the gesture always moves the panel off its own edge.

Open filters…
<ol-drawer label="Filters" placement="start">…</ol-drawer>

Custom width

--ol-drawer-width sets the panel width; it never exceeds the viewport. --ol-drawer-enter-duration and --ol-drawer-exit-duration tune the slide.

Open wide drawer…
  • 420px wide instead of the 300px default.
<ol-drawer label="Wide panel" style="--ol-drawer-width: 420px">…</ol-drawer>
API reference
Properties
PropertyAttributeTypeDefaultDescription
open open Boolean false Whether the drawer is currently visible.
placement placement 'start' | 'end' 'end' Which edge the drawer slides from: `'start'` (left in LTR) or `'end'` (right in LTR). Default: `'end'`
label label String '' Accessible label for the drawer dialog.
closeOnScrimClick close-on-scrim-click Boolean true Whether clicking the scrim closes the drawer. Default `true`. Attribute: `close-on-scrim-click`.
closeOnEscape close-on-escape Boolean true Whether Escape closes the drawer. Default `true`. Attribute: `close-on-escape`.
Events
EventTypeDescription
ol-drawer-show CustomEvent Fired when the drawer begins opening.
ol-drawer-after-show CustomEvent Fired after the enter transition completes.
ol-drawer-after-hide CustomEvent Fired after the exit transition completes.
ol-drawer-hide Fired when a close is requested, before the exit transition. Cancelable — `event.preventDefault()` keeps the drawer open. detail: { reason: 'escape' | 'scrim' | 'swipe' | 'programmatic' }
Slots
SlotDescription
(default) Default slot for drawer content.
CSS custom properties
PropertyDefaultDescription
--ol-drawer-width 300px Width of the drawer panel.
--ol-drawer-scrim-color hsla(0, 0%, 0%, 0.5) Scrim color.
--ol-drawer-enter-duration 400ms Slide-in duration.
--ol-drawer-exit-duration 300ms Slide-out duration.
--ol-drawer-scroll-padding 0 Inset kept clear when Tab scrolls a focused element into view, for drawers with a sticky header or footer.

Feedback

Toast <ol-toast>

Avoid: Anything the reader must act on belongs in a Dialog or a Banner.

Types

Errors are announced assertively (role="alert").

showToast(i18nStrings.savedToReadingLog);
showToast(i18nStrings.subjectsUpdated, { type: 'success' });
showToast(i18nStrings.listUpdateFailed, { type: 'error' });

Description and rich content

description adds a secondary line. For rich content, author the markup in an inert <template> in the page template — where $_() extraction works — and pass that element to showToast(); its content is cloned in. Nothing is ever parsed from a runtime string as HTML.

Could not save. Get help
showToast(i18nStrings.relaunchToUpdate, { description: 'v1.11187.4', persistent: true });

<!-- Rich content: write the HTML in the page template… -->
<template id="save-error-toast">
  $_("Could not save.") <a href="/help">$_("Get help")</a>
</template>

// …and pass the template element to showToast()
showToast(document.querySelector('#save-error-toast'), { type: 'error', persistent: true });

Imperative use

showToast() stacks toasts in a shared fixed <ol-toast-region> at the bottom-center of the viewport. New toasts slide up from below; older ones move up to make room. They dismiss after timeout ms (default 4000) unless persistent. Hovering or focusing the list pauses every timer. The message is set as an attribute — always text, never HTML — and should already be translated.

import { showToast } from './OlToastRegion.js';

showToast(i18nStrings.updateSuccess, { type: 'success' });
showToast(i18nStrings.failedSubmitCheckIn, { type: 'error', persistent: true });

Close event. Fires ol-toast-close once when closing begins, with the reason in event.detail.reason"timeout", "close-button", or "programmatic".

API reference
Properties
PropertyAttributeTypeDefaultDescription
type type "info" | "success" | "error" 'info' Default: "info". Errors use role="alert" / assertive announcements.
message message String '' The (already translated) message text.
description description String '' Optional secondary line, rendered smaller and muted.
persistent persistent Boolean false Toast stays until explicitly closed (no timer).
timeout timeout Number 4000 Milliseconds before auto-dismiss. Default: 4000.
labelClose label-close String 'Close' Aria label for the close button (default: "Close")
Events
EventTypeDescription
ol-toast-resize CustomEvent
ol-toast-close CustomEvent Fired once when the toast begins closing. detail: { reason: "timeout" | "close-button" | "programmatic" }
Slots
SlotDescription
(default) Rich message content (links, custom markup). Overrides message/description.

Message

A plain CSS component — no JS, no custom element. Use it for status that belongs next to the thing it describes, inline in the page flow. For page-level announcements use Banner; for transient confirmations use Toast.

Variants

The bare class is styled the same as --info.

Your changes are visible to other librarians immediately.
Edition merged successfully.
This record has unresolved import errors.
Could not save — the record was modified by someone else.
<div class="ol-message ol-message--info">Your changes are visible to other librarians immediately.</div>
<div class="ol-message ol-message--success">Edition merged successfully.</div>
<div class="ol-message ol-message--warning">This record has unresolved import errors.</div>
<div class="ol-message ol-message--error">Could not save — the record was modified by someone else.</div>

With a heading

Headings inherit the message's color rather than the page's heading color.

Import partially completed

38 of 42 records imported. The remaining 4 had missing identifiers.
<div class="ol-message ol-message--warning">
    <h4>Import partially completed</h4>
    38 of 42 records imported. The remaining 4 had missing identifiers.
</div>

Source. static/css/components/ol-message.css. Note it currently hardcodes its hsl() values rather than using the status tokens under Foundations → Colors — worth migrating when next touched.

Related. flash-messages.css styles the server-rendered flash region at the top of the page; it isn't reusable markup, but it is where post-redirect confirmations land.

Scorecard <ol-scorecard>

Displays a metadata quality scorecard as a tabbed set of sections, each listing the checks that make up its score. Data is passed as a JSON results attribute or property; labels are individually translatable.

Collapsed (default)

Shows a small ring proportional to the overall percentage. Click it to expand.

<ol-scorecard
  results='{"name":"Edition Scorecard","score":115,"maxScore":225,"sections":[...]}'>
</ol-scorecard>

Expanded

The expanded attribute renders the full tabbed UI directly. Clicking the header collapses it again.

<ol-scorecard
  expanded
  results='{"name":"Edition Scorecard","score":115,"maxScore":225,"sections":[...]}'>
</ol-scorecard>
API reference
Properties
PropertyAttributeTypeDefaultDescription
results results Object The scorecard data: `{ name, score, maxScore, sections: [{ name, score, maxScore, checks: [{ description, details, score, passing }] }] }`. Settable as a JSON attribute (`results='{"score":10,...}'`) or property.
expanded expanded Boolean false Whether the full tabbed UI is shown instead of the collapsed badge. Default `false`. Presence of the attribute expands it.
labelTotal label-total String 'Total' Label for the non-interactive "Total" gauge (default: "Total")
labelFailingChecks label-failing-checks String 'Failing Checks ({count})' Failing checks section heading template, use `{count}` (default: "Failing Checks ({count})")
labelPassingChecks label-passing-checks String 'Passing Checks ({count})' Passing checks section heading template, use `{count}` (default: "Passing Checks ({count})")
labelPoints label-points String '{score} points' Check point-value template, use `{score}` (default: "{score} points")
labelExpand label-expand String '{name}: {percentage}%. Click to expand.' Accessible label for the collapsed badge button, use `{name}`, `{percentage}` (default: "{name}: {percentage}%. Click to expand.")
labelCollapse label-collapse String 'Collapse' Accessible label for the header collapse button (default: "Collapse")
outdated outdated Boolean false Whether the solr record is stale relative to the database. Default `false`. Presence of the attribute shows a warning banner at the top of the expanded card; no banner is shown otherwise.
labelOutdated label-outdated String 'This record has been edited and is not yet reflected in Solr. It should update in a minute or so.' Warning banner text shown when outdated (default: "This record has been edited and is not yet reflected in Solr. It should update in a minute or so.")

Content

Read More <ol-read-more>

Height-based truncation

max-height limits the content by pixel height.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

<ol-read-more max-height="80px">
            <p>    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</p>
        </ol-read-more>

Line-based truncation

max-lines limits by number of lines.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

<ol-read-more max-lines="3">
            <p>    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</p>
        </ol-read-more>

Custom button text

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, quis nostrud exercitation ullamco laboris.

<ol-read-more max-lines="2" more-text="Show more" less-text="Show less">
            <p>    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, quis nostrud exercitation ullamco laboris.
</p>
        </ol-read-more>

Custom background color

Match the gradient fade to your container's background.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, quis nostrud exercitation ullamco laboris.

<ol-read-more max-lines="2" background-color="hsl(202, 80%, 95%)">
            <p>    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, quis nostrud exercitation ullamco laboris.
</p>
        </ol-read-more>

Small label size

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, quis nostrud exercitation ullamco laboris.

<ol-read-more max-lines="2" label-size="small">
            <p>    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, quis nostrud exercitation ullamco laboris.
</p>
        </ol-read-more>

Content shorter than the limit

When the content fits, no toggle button renders.

This is short content that fits within 5 lines, so no button appears.

<ol-read-more max-lines="5">
    <p>This is short content that fits within 5 lines, so no button appears.</p>
</ol-read-more>
API reference
Properties
PropertyAttributeTypeDefaultDescription
maxHeight max-height String '80px' Collapsed height of the content before truncating (default: "80px")
moreText more-text String 'Read More' Label for the expand toggle (default: "Read more")
lessText less-text String 'Read Less' Label for the collapse toggle (default: "Read less")
backgroundColor background-color String Background color for the gradient fade (default: white)
labelSize label-size "medium" | "small" 'medium' Size of the toggle button text: "medium" (default) or "small" (12px)
Slots
SlotDescription
(default) The collapsible content
CSS custom properties
PropertyDefaultDescription
--ol-readmore-link-color var(--color-link) Color of the more/less toggle button
--ol-readmore-gradient-color white Solid color the fade gradient blends toward (match the surrounding background)
--ol-readmore-gradient-color-transparent rgba(255, 255, 255, 0) Transparent end of the fade gradient
CSS parts
PartDescription
toggle-btn The toggle button element (targets both "more" and "less" buttons)

Markdown Editor <ol-markdown-editor>

Default

Point target-id at an existing textarea. The editor reads its initial content from that textarea and syncs changes back to it, so the textarea stays the value the form submits.

<textarea id="my-input">
  **Hello** world
</textarea>
<ol-markdown-editor
 target-id="my-input">
</ol-markdown-editor>

Mixed Markdown and HTML

enable-html-block exposes the HTML block button. HTML blocks render with a preview and an Edit button for the source.

<ol-markdown-editor
 target-id="my-input"
  enable-html-block>
</ol-markdown-editor>

Code blocks and inline code

enable-code exposes the inline-code and code-block buttons. Off by default because only the wiki page renderer is guaranteed to render fenced code blocks.

<ol-markdown-editor
 target-id="my-input"
  enable-code>
</ol-markdown-editor>

Change event

Fires ol-markdown-editor-change on every change, with the raw Markdown in event.detail.value.


    
<ol-markdown-editor
 target-id="my-input"
  height="100px">
</ol-markdown-editor>

<script>
  editor.addEventListener('ol-markdown-editor-change', (e) => {
    console.log(e.detail.value);
  });
</script>
API reference
Events
EventTypeDescription
ol-markdown-editor-change CustomEvent

Icon <ol-icon>

<ol-icon> is the client-side way to draw a glyph, for markup a component builds at runtime. Server-rendered markup should use the icon() macro instead, and a glyph inside another component's shadow root has to be inlined — <use> is unreliable there. The Icons section has the gallery, all three techniques side by side, and the <ol-icon> API.

Same sprite and same CSS as the macro; size is sm (16px), md (20px, default) or lg (24px).

<ol-icon name="search" size="sm"></ol-icon>
<ol-icon name="search"></ol-icon>
<ol-icon name="search" size="lg"></ol-icon>