<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.
<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.
-
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.
npm install poem-elementOr include the script directly:
<script type="module" src="poem-element.js"></script><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><!-- 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><!-- 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><poem-element aria-label="Sonnet 18 by William Shakespeare" numbers wrap="indent">
Shall I compare thee to a summer's day?
...
</poem-element>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;
}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.
| 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. |
| Property | Type | Description |
|---|---|---|
text |
string |
Get or set the poem text. Setting triggers a re-render. |
| 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. |
| Event | Detail | Description |
|---|---|---|
poem-rendered |
{ lines: number, config: object } |
Fired after each render completes. Bubbles and crosses shadow DOM boundaries (composed: true). |
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;
}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.
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.
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));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