News

ArticleHeader

The top of a story: its label and section, the headline, the dek, every author grouped by role, published and updated times, and the reading time.

#news#article#typography#byline#no-js

Demo

Preview

AnalysisClimate

The river that ran out of water

Three summers of drought have turned the Douro into a string of ponds, and the vineyards along it are deciding what to give up.

Published Updated 9 min read

LiveElection 2026

Polls close in the first states

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/article_header.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/article_header.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/article_header.qwe
view ArticleHeaderDemo() =    <div.demo-stack style="gap: 40px">        <ArticleHeader id="article-header-demo-main" kicker="Climate" kicker_href="#climate" label="Analysis"                       headline="The river that ran out of water"                       dek="Three summers of drought have turned the Douro into a string of ponds, and the vineyards along it are deciding what to give up."                       authors={[{ name: "Inês Carvalho", href: "#by-ines" }, "Tom Reid", { name: "Maya Okafor", href: "#by-maya" },                                 { name: "Luís Pinto", role: "Photographs by" }, { name: "Sara Klein", role: "With additional reporting by" }]}                       published="2026-10-29T08:00:00-04:00" updated="2026-10-30T14:20:00-04:00" zone_label="ET" read_time="9 min read"/>        <ArticleHeader id="article-header-demo-live" label="Live" kicker="Election 2026" level={2} align="center"                       headline="Polls close in the first states"                       authors={["The Ledger staff"]} published="2026-11-03T19:00:00-05:00" published_text="Started 19 minutes ago"/>    </div>

Source

ui/article_header.qwe
// ArticleHeader: the top of a news story: a label ("Analysis", "Exclusive") and the// section it is in, the headline, the dek (the line under it that says what the story// is), the byline, with every author and their roles grouped as papers write them// ("By Ada Lovelace and Grace Hopper", "Photographs by Alan Turing"), when it was// published and when it was last updated, and how long it takes to read.////   <ArticleHeader kicker="Climate" kicker_href="/climate" label="Analysis"//                  headline="The river that ran out of water"//                  dek="Three summers of drought have turned the Douro into a string of ponds."//                  authors={[{ name: "Ada Lovelace", href: "/by/ada" }, "Grace Hopper",//                            { name: "Alan Turing", role: "Photographs by" }]}//                  published="2026-10-29T08:00:00-04:00" updated="2026-10-30T14:20:00-04:00" zone_label="ET" read_time="7 min read"/>//// headline: the story's title, an <h1> (level={2} makes it an <h2>, for a story shown// inside another page). kicker: the section or topic over it, a link to kicker_href// when given. label: a short tag before the kicker for the kind of story ("Analysis",// "Opinion", "Exclusive", "Live"); label_tone: its colour, "accent" (default),// "danger" (Live and Breaking are danger by default), "warning" or "neutral".// dek: the summary line under the headline. authors: strings, or maps of { name,// href?, role? }; role is the words before the names ("By" when left out, "With// additional reporting by", "Photographs by"), and authors with the same role are// written together in one line, "A, B and C". published and updated: ISO dates or// date-times, shown in <time> elements as "Oct 29, 2026, 8:00 AM" (in their own// offset; zone_label, "ET", is written after the time) after "Published" and// "Updated"; published_text and updated_text, when given, say the whole of it instead// ("Started 19 minutes ago", "Last updated 2 hours ago"). An updated time the same as// published is not shown. read_time: "7 min read". locale: the language of the month names// ("en-US"). align: "start" (default) or "center". id: the header's id.import ui/theme.qwe use ui_clslet article_header_css = css`.ui-arth { display: grid; gap: 12px; max-width: 760px; margin: 0; color: var(--ui-fg); font: 15px/1.5 var(--ui-font); }.ui-arth-center { justify-items: center; margin-inline: auto; text-align: center; }.ui-arth-top { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 0; font-size: 13px; font-weight: 650;  letter-spacing: .05em; text-transform: uppercase; }.ui-arth-label { --t: var(--ui-accent); display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 4px;  background: color-mix(in srgb, var(--t) 14%, transparent); color: var(--t); font-size: 12px; }.ui-arth-label-danger { --t: var(--ui-danger); }.ui-arth-label-warning { --t: var(--ui-warning); }.ui-arth-label-neutral { --t: var(--ui-muted); }.ui-arth-label-danger::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor;  animation: ui-arth-pulse 1.6s ease-in-out infinite; }@keyframes ui-arth-pulse { 50% { opacity: .25; } }.ui-arth-kicker { color: var(--ui-accent); text-decoration: none; }a.ui-arth-kicker:hover { text-decoration: underline; text-underline-offset: 3px; }a.ui-arth-kicker:focus-visible, .ui-arth-byline a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-arth .ui-arth-headline { margin: 0; font-size: clamp(28px, 4.2vw, 44px); line-height: 1.12; font-weight: 750; letter-spacing: -.015em;  text-wrap: balance; }.ui-arth .ui-arth-dek { margin: 0; color: var(--ui-muted); font-size: clamp(17px, 2vw, 20px); line-height: 1.45; text-wrap: pretty; }.ui-arth-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin-top: 4px; padding-top: 12px;  border-top: 1px solid var(--ui-border); }.ui-arth-center .ui-arth-meta { justify-content: center; }.ui-arth-faces { display: flex; flex: none; }.ui-arth-face { display: grid; place-items: center; width: 34px; height: 34px; border: 2px solid var(--ui-bg); border-radius: 50%;  background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 12px; font-weight: 700; letter-spacing: 0; }.ui-arth-face + .ui-arth-face { margin-left: -10px; }.ui-arth-credits { display: grid; gap: 2px; min-width: 0; }.ui-arth-byline { margin: 0; font-size: 14px; color: var(--ui-muted); }.ui-arth-byline b { color: var(--ui-fg); font-weight: 650; }.ui-arth-byline a { color: var(--ui-fg); font-weight: 650; text-decoration: none; border-bottom: 1px solid var(--ui-strong); }.ui-arth-byline a:hover { color: var(--ui-accent); border-color: currentColor; }.ui-arth-byline:first-child { font-size: 15px; }.ui-arth-times { display: flex; flex-wrap: wrap; gap: 2px 12px; margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-arth-center .ui-arth-times { justify-content: center; }.ui-arth-times > span + span::before { content: "·"; margin-right: 12px; color: var(--ui-strong); }.ui-arth-updated { color: var(--ui-fg); }@media (prefers-reduced-motion: reduce) { .ui-arth-label-danger::before { animation: none; } }`// An author as a map: "Ada Lovelace" is { name: "Ada Lovelace", role: "By" }.fn article_header_author(a) = type_of(a) == "str" ? { name: a, role: "By" } : { ...a, role: a.role ?? "By" }// The authors in lines by role, in the order each role first comes: [{ role, people }].fn article_header_lines(authors: list) {    let all = authors | map(article_header_author)    let roles = all | map((a) => a.role) | unique    roles | map((r) => { role: r, people: all | filter((a) => a.role == r) })}// Initials for a face: "Ada Lovelace" is "AL".fn article_header_initials(name: str) -> str {    let words = split(trim(name), " ") | filter((w) => w != "")    upper(len(words) == 0 ? "?" : (len(words) == 1 ? slice(words[0], 0, 1) : words[0][0] + words[-1][0]))}// A date or date-time as it reads: "Oct 29, 2026" or "Oct 29, 2026, 8:00 AM".fn article_header_when(t: str, locale: str, zone_label: str?) -> str {    let timed = len(t) > 10    let text = date.format(t, { locale, pattern: timed ? "MMM D, YYYY, h:mm A" : "MMM D, YYYY" })    timed && zone_label != null ? `${text} ${zone_label}` : text}view ArticleHeader(headline: str, kicker: str? = null, kicker_href: str? = null, label: str? = null, label_tone: str? = null,                   dek: str? = null, authors: list = [], published: str? = null, published_text: str? = null,                   updated: str? = null, updated_text: str? = null, zone_label: str? = null, read_time: str? = null,                   level: int = 1, locale: str = "en-US", align: str = "start", id: str? = null, class: str? = null) {    style global article_header_css    let tone = label_tone ?? (label != null && contains(["live", "breaking"], lower(label)) ? "danger" : "accent")    let lines = article_header_lines(authors)    let faces = authors | map(article_header_author) | take(3)    let person = (a) => a.href != null ? <a href={a.href} rel="author">{a.name}</a> : <b>{a.name}</b>    let names = (people: list) => for [i, a] in enumerate(people) => [        i > 0 && (i == len(people) - 1 ? " and " : ", "),        person(a),    ]    let show_updated = updated != null && updated != published    <header class={ui_cls(["ui-arth", align == "center" && "ui-arth-center", class])} id={id}>        {(label != null || kicker != null) && <p.ui-arth-top>            {label != null && <span class={`ui-arth-label ui-arth-label-${tone}`}>{label}</span>}            {kicker != null && (kicker_href != null ? <a.ui-arth-kicker href={kicker_href}>{kicker}</a> : <span.ui-arth-kicker>{kicker}</span>)}        </p>}        {level == 2 ? <h2.ui-arth-headline>{headline}</h2> : <h1.ui-arth-headline>{headline}</h1>}        {dek != null && <p.ui-arth-dek>{dek}</p>}        {(len(lines) > 0 || published != null || read_time != null) && <div.ui-arth-meta>            {len(faces) > 0 && <span.ui-arth-faces aria-hidden="true">                {for a in faces => <span.ui-arth-face>{article_header_initials(a.name)}</span>}            </span>}            <div.ui-arth-credits>                {for l in lines => <p.ui-arth-byline data-role={l.role}>{l.role} {names(l.people)}</p>}                {(published != null || read_time != null) && <p.ui-arth-times>                    {published != null && <span>{published_text == null && "Published "}<time datetime={published}>{published_text ?? article_header_when(published, locale, zone_label)}</time></span>}                    {show_updated && <span.ui-arth-updated>{updated_text == null && "Updated "}<time datetime={updated}>{updated_text ?? article_header_when(updated, locale, zone_label)}</time></span>}                    {read_time != null && <span>{read_time}</span>}                </p>}            </div>        </div>}    </header>}
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

On ArticleHeader: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.