@andystevensname/poem-element
v1.0.3
Published
A web component for displaying poetry with advanced formatting and line numbering.
Maintainers
Readme
poem-element
Overview
<poem-element> was developed to help solve the following difficulties with displaying poems on the web.
- No semantic element: A poem is composed of lines grouped into stanzas. Many developers wrap stanzas or lines in paragraph tags (
<p>), which, while understandable, is semanticaly incorrect. Preformatted text (<pre>) is a better option, but by itself it's limited. - No control over line formatting: No single text element can offer control over individual lines.
- Difficult to set within a reponsive site design: Poems in print have always been constrained by the page, and while a responsive web design provides more options to adapt to a poem than physical media, sometimes a poem needs to respond to the constraints of a site design.
Features
Element Options
<poem-element> allows for the following per-poem options.
Line wrap:
<poem-element>defaults to horizontal scrolling when the content box is constrained by a parent element. Optionally,<poem-element>can wrap longer lines with or without a hanging indent, and with or without a graphic glyph denoting the line wrap.Line numbers: Line numbers are a booksetting convention for longer poems or academic settings. Two layout modes are available:
grid(default) uses a CSS grid layout to display lines and line numbers.listuses a browser's built-in list-item counter with a::markerpseudo element, and is only available with wrapped poems.
Other Features
Accessibility:
<poem-element>usesrole="group"with a configurablearia-labelon the poem container to help screen readers announce the poem as a whole, androle="none"on individual lines to prevent screen readers from announcing list items. Line numbers are markedaria-hidden. In non-wrapping mode, the poem container is keyboard-focusable (tabindex="0") so users can scroll horizontally with arrow keys.Server-side rendering:
<poem-element>supports Declarative Shadow DOM for instant rendering without JavaScript. A Node.js SSR helper renders the complete HTML output when used with Astro, 11ty, or any templating system.Print support:
poem-elementprovides a@media printrule that forces text wrapping and removes overflow clipping, allowing poems to render completely when printed or saved as a PDF.External styling:
<poem-element>exposespartattributes and CSS custom properties, allowing developers to style the component via a site's main stylesheet.
Installation
npm install poem-elementOr include the script directly:
<script type="module" src="poem-element.js"></script>Usage
Basic usage
<poem-element>
Buffalo Bill 's
defunct
who used to
ride a watersmooth-silver
stallion
and break onetwothreefourfive pigeonsjustlikethat
Jesus
he was a handsome man
and what i want to know is
how do you like your blueeyed boy
Mister Death
</poem-element>Line wrap
<!-- Standard wrap -->
<poem-element wrap>
This is the forest primeval. The murmuring pines and the hemlocks,
Bearded with moss, and in garments green, indistinct in the twilight,
</poem-element>
<!-- Indented wrap -->
<poem-element wrap="indent">
Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:
</poem-element>
<!-- Indented wrap with continuation arrow -->
<poem-element wrap="indent-arrow">
Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:
</poem-element>Line numbers
<!-- Numbers every 5th line (default, grid layout) -->
<poem-element numbers>
...
</poem-element>
<!-- Numbers every 4th line -->
<poem-element numbers="4">
...
</poem-element>
<!-- Numbers with indented wrap (grid layout, default) -->
<poem-element numbers wrap="indent">
...
</poem-element>
<!-- Numbers with list layout (only available with wrap) -->
<poem-element numbers numbers-layout="list" wrap="indent">
...
</poem-element>
<!-- Numbers positioned outside (in the margin) -->
<poem-element numbers numbers-position="outside" wrap="indent">
...
</poem-element>
<!-- Numbers on the right side (grid layout only) -->
<poem-element numbers numbers-align="right">
...
</poem-element>Accessibility
<poem-element aria-label="Sonnet 18 by William Shakespeare" numbers wrap="indent">
Shall I compare thee to a summer's day?
...
</poem-element>FOUC prevention
Add this to your page CSS to prevent a flash of unstyled content. This will preserve the poem's whitespace while hiding its content while the component registers:
poem-element:not(:defined) {
visibility: hidden;
}Demo
Because the web component is served over HTTP, you'll need to run the included local server to view the demo:
npx serve .Then open index.html in your browser.
API Reference
Attributes
| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| wrap | (boolean), "indent", "indent-arrow" | No line wrap | Controls line wrapping behavior. Adding this attribute without a value forces horizontal scrolling. Adding one of the values changes the wrap behavior. |
| numbers | (boolean), or a positive integer | 5 | Enables line numbering. Adding this attribute without a value creates line numbers every 5th line. A number sets the interval. |
| numbers-layout | "grid", "list" | "grid" | grid uses CSS Grid with DOM elements for numbers. list uses display: list-item with ::marker for numbers (only effective with wrap; silently ignored without it). |
| numbers-position | "inside", "outside" | "inside" | inside positions numbers flush with surrounding content. outside hangs numbers in the left margin. |
| numbers-align | "right" | left | Places line numbers to the right of the poem text. Only applies to grid layout. |
| aria-label | any string | "poem" | Accessible label for the poem container. Forwarded to the inner role="group" element. |
Properties
| Property | Type | Description |
|----------|------|-------------|
| text | string | Get or set the poem text. Setting triggers a re-render. |
Methods
| Method | Description |
|--------|-------------|
| render() | Manually re-render the component. Called automatically on attribute changes. |
| scheduleRender() | Queue a render via microtask. Multiple calls are debounced into a single render. |
Events
| Event | Detail | Description |
|-------|--------|-------------|
| poem-rendered | { lines: number, config: object } | Fired after each render completes. Bubbles and crosses shadow DOM boundaries (composed: true). |
Customization
CSS ::part() Selectors
Internal elements expose part attributes for direct styling:
/* Style the poem container (font, color, background, line-height, etc.) */
poem-element::part(block) {
font-family: Georgia, serif;
font-size: 1.1em;
color: #333;
background: #fafafa;
line-height: 1.6;
}
/* Style individual poem lines */
poem-element::part(line) {
color: #333;
}
/* Style line numbers (grid layout) */
poem-element::part(line-number) {
color: #c00;
}CSS Custom Properties
These properties control values that can't be styled through ::part() — either because they're used in internal calc() expressions or because they target ::marker, which isn't styleable via ::part().
poem-element {
/* Layout calc values */
--poem-num-col: 3ch; /* Width of the line number column */
--poem-num-gap: 0.5rem; /* Gap between numbers and text */
--poem-text-indent: 2em; /* Hanging indent depth for wrap="indent" */
/* ::marker styling (not reachable via ::part) */
--poem-line-number-color: inherit;
--poem-line-number-font: inherit;
--poem-line-number-font-size: inherit;
--poem-line-number-font-weight: inherit;
--poem-line-number-line-height: inherit; /* Adjust when line number font-size differs from text */
}For general text styling (font, color, background, line-height, etc.), use ::part() selectors.
Server-Side Rendering
The SSR helper produces Declarative Shadow DOM output for instant rendering without JavaScript:
import { renderPoemElement } from 'poem-element/ssr';
const html = renderPoemElement(
`Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:`,
{
numbers: true,
wrap: 'indent',
'aria-label': 'Sonnet 18',
}
);The output includes a <template shadowrootmode="open"> with the fully rendered shadow DOM. The browser attaches it immediately on parse, with no JavaScript required for the initial render.
When the client-side JS loads, it detects the existing DSD content and skips re-rendering. Dynamic attribute changes after load will trigger re-renders as normal.
Framework integration
Astro:
---
import { renderPoemElement } from 'poem-element/ssr';
const html = renderPoemElement(`Shall I compare...`, { numbers: true, wrap: 'indent' });
---
<Fragment set:html={html} />
<script>import 'poem-element';</script>11ty:
const { renderPoemElement } = require('poem-element/ssr');
eleventyConfig.addShortcode('poem', (text, attrs) => renderPoemElement(text, attrs));Package exports
import 'poem-element'; // Client-side custom element
import { renderPoemElement } from 'poem-element/ssr'; // SSR helper
import { parseAttributes, parseLines, transformLineText, formatLineNumber, generateCSS, STATIC_CSS, generateDynamicCSS } from 'poem-element/core'; // Shared utilities