Components

Shared components for independent pages.

Directory

Links and explanations form rows. Set the columns to suit the material. Keep the list semantics.

Directory markup
<ul class="ui-directory">
  <li><a href="guide.html">Guide</a>
    <p>A short explanation of what the reader will find.</p>
  </li>
</ul>

Controls near their results

Native labels and controls wrap together. To filter this example catalogue, use search or select a surface.

Dropdown markup
<label class="ui-field">Surface
  <select name="surface" data-ui-select>
    <option value="all">All surfaces</option>
    <option value="reading">Reading</option>
  </select>
</label>

The native select supplies the value and form events. The shared menu supports arrow keys, Home, End, typeahead, Enter, and Escape. Without the script, the styled native control remains available.

6 patterns

Show where the table continues

An indicator appears only when columns extend beyond the viewport. Put keyboard focus in the table region. Then use the arrow keys to scroll. The table keeps its native headings and caption.

Pattern compatibility
PatternWithout JavaScriptKeyboard behaviorLayout ownership
DirectoryAll links are availableNative linksPage-specific columns
Responsive disclosureContents remains visibleToggle, Escape, focus returnPage-specific breakpoint
DialogNative close form works once openEscape and native focus returnShared shell; local content
Table markup
<div class="table-scroll" data-scroll-region aria-label="Comparison">
  <table class="ui-table">…caption, headings, and rows…</table>
</div>

One record, in context

A native dialog contains keyboard focus and supports Escape. Its close control remains available when long content scrolls.

Dialog markup
<dialog class="ui-dialog" data-dialog aria-labelledby="record-title">
  <form method="dialog" class="ui-dialog-dismiss">
    <button aria-label="Close details">×</button>
  </form>
  <div class="ui-dialog-body">
    <h2 id="record-title">Record title</h2>
    <p>Record content.</p>
  </div>
</dialog>

Code as code

Highlight source during the build. The palette uses the theme tones, so dark mode and print need no second theme. Long lines scroll inside the listing.

def visible_count(rows, query=""):
    """Count rows whose label contains the query."""
    query = query.casefold()
    return sum(1 for row in rows if query in row["label"].casefold())  # 0 for no rows

Use only what the page needs

Load the optional component layer after the foundation and before local styles. Explicit data attributes enable its behavior. Pages without these attributes keep their existing behavior.

<script src="https://jehlp.net/site-theme/v2/theme.js?v=20260930-header-home"></script>
<link rel="stylesheet" href="https://jehlp.net/site-theme/v2/base.css?v=20261009-reading-folio">
<link rel="stylesheet" href="https://jehlp.net/site-theme/v2/components.css">
<link rel="stylesheet" href="assets/styles.css">
<script src="https://jehlp.net/site-theme/v2/components.js" defer></script>

For components added later, call window.JehlpUI?.enhance(container). Repeated calls are safe. See the full component contracts for examples and limits.

reusable behavior

Native dialog

Keep the record’s title, explanation, and actions together. After you fill the content, open the dialog with showModal(). Close it with a native method="dialog" form.

Escape returns focus to the opener. A click inside the dialog, including its padding, keeps it open. A click that starts and ends on the backdrop closes it.