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.
Demo
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.
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.qwemain.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>}Related
CorrectionNoticeA correction, clarification, update, editor's note or retraction on a story, dated, with a link to the corrections policy; boxed on top or a line at the end.8 views · 7 downloadsFactCheckA fact-check: the claim and who said it, the rating large and on its scale from true to false, what was found, the sources, with ClaimReview markup.8 views · 12 downloadsKeyPointsThe story in a few lines at the top of an article: points with links, marks or numbers, when they were updated, and the rest folded under Show more.8 views · 9 downloadsSectionFrontA section's front: the lead story large with its picture and dek, the next stories beside it, and a column of more headlines; one column where narrow.9 views · 9 downloadsAdSlotAn advertisement's place kept at its size so the story never jumps: standard sizes by name, another on phones, a clear label, a placeholder, sticky in a rail.6 views · 8 downloadsBreakingNewsA breaking-news banner: the label in a block of colour, the headline and a line more, how long ago it came in, kept up to date, and a close that can be remembered.9 views · 8 downloads
Thread
Thread 0 posts
On ArticleHeader: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.