BreakingNews
A 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.
Demo
Magnitude 6.1 earthquake strikes off the northern coast
A tsunami warning is in force for the islands; residents are told to move to higher ground.
Power cut hits half the city; trams stopped
Not dismissed yet.
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/breaking_news.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/breaking_news.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/breaking_news.qwe
component BreakingNewsDemoLive() { state note = "Not dismissed yet." <div.demo-stack> <BreakingNews id="breaking-news-demo-dev" label="Developing" tone="warning" headline="Power cut hits half the city; trams stopped" time="Updated 14:20" on_dismiss={() => { note = "on_dismiss was called." }}/> <p.demo-note id="breaking-news-demo-note">{note}</p> </div>}view BreakingNewsDemo() = <div.demo-stack> <BreakingNews id="breaking-news-demo-main" headline="Magnitude 6.1 earthquake strikes off the northern coast" href="#quake-live" summary="A tsunami warning is in force for the islands; residents are told to move to higher ground." datetime={date.from_ms(now() - 4 * 60000 - 5000, "UTC")} remember="demo-quake"/> <BreakingNewsDemoLive/> <BreakingNews id="breaking-news-demo-fixed" label="Live" tone="accent" headline="Election night: results as they come in" href="#results" datetime={date.from_ms(now() - 3 * 3600000, "UTC")} dismissable={false}/> </div>Source
ui/breaking_news.qwe
// BreakingNews: the banner across the top of a news page when something big has just// happened: "Breaking" in a block of colour, the headline (a link to the story), a// line more if there is one, and how long ago it came in, which keeps itself up to// date ("Just now", "4 min ago", "2 h ago"). It can be closed, and remembered closed.//// <BreakingNews headline="Magnitude 6.1 earthquake strikes off the coast" href="/live/quake"// datetime="2026-10-30T14:02:00Z" summary="Tsunami warning issued for the northern islands."/>// <BreakingNews label="Developing" tone="warning" headline="Power cut hits half the city" time="Updated 14:20"// remember="power-1030" on_dismiss={() => { seen = true }}/>//// headline: what happened; href makes it a link to the story or the live page.// summary: a line under it. label: the word in the block ("Breaking"); tone: its// colour, "danger" (default), "warning" (for "Developing"), "accent" or "neutral".// datetime: when it came in, an ISO date-time; the banner says how long ago, as// "Just now", "N min ago", "N h ago", then the date, and counts on every 30 seconds// in the browser. time: what to say instead ("Updated 14:20"), when given.// dismissable: a close button (true). remember: a name for this story; closed, it// stays closed in this browser (localStorage) until the name changes. name: what// screen readers call the region ("Breaking news"). on_dismiss(): inside one of your// components, called with nothing when it is closed. id: the banner's id.import ui/theme.qwe use ui_clslet breaking_news_css = css`.ui-brk-host { display: contents; }.ui-brk { --t: var(--ui-danger); display: flex; align-items: stretch; min-width: 0; overflow: hidden; border: 1px solid var(--t); border-radius: var(--ui-radius-sm); background: color-mix(in srgb, var(--t) 8%, var(--ui-surface)); color: var(--ui-fg); font: 15px/1.4 var(--ui-font); }.ui-brk-warning { --t: var(--ui-warning); }.ui-brk-accent { --t: var(--ui-accent); }.ui-brk-neutral { --t: var(--ui-fg); }.ui-brk-label { flex: none; display: flex; align-items: center; gap: 7px; padding: 10px 14px; background: var(--t); color: var(--ui-bg); font-size: 12px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; white-space: nowrap; }.ui-brk-label::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; animation: ui-brk-pulse 1.4s ease-in-out infinite; }@keyframes ui-brk-pulse { 50% { opacity: .2; } }.ui-brk-body { flex: 1; min-width: 0; display: grid; gap: 2px; align-content: center; padding: 9px 14px; }.ui-brk .ui-brk-headline { margin: 0; font-size: 16px; font-weight: 750; line-height: 1.3; }.ui-brk-headline a { color: inherit; text-decoration: none; }.ui-brk-headline a:hover { text-decoration: underline; text-decoration-color: var(--t); text-decoration-thickness: 2px; text-underline-offset: 3px; }.ui-brk-headline a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-brk-summary { margin: 0; color: var(--ui-muted); font-size: 14px; }.ui-brk-time { flex: none; align-self: center; padding: 0 4px 0 6px; color: var(--ui-muted); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }.ui-brk-time:last-child { padding-right: 14px; }.ui-brk-x { flex: none; align-self: center; display: grid; place-items: center; width: 30px; height: 30px; margin: 0 8px 0 2px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); font-size: 20px; line-height: 1; cursor: pointer; }.ui-brk-x:hover { background: color-mix(in srgb, var(--t) 14%, transparent); color: var(--ui-fg); }.ui-brk-x:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (max-width: 560px) { .ui-brk { flex-wrap: wrap; } .ui-brk-label { padding: 6px 12px; } .ui-brk-body { order: 3; flex-basis: 100%; padding-top: 4px; } .ui-brk-time { margin-left: auto; }}@media (prefers-reduced-motion: reduce) { .ui-brk-label::before { animation: none; } }`// How long ago `since` (an ISO date-time) was, at `at` (ms): "Just now", "4 min ago",// "2 h ago", or its date ("Oct 28") after a day. The same on the server and in the browser.fn breaking_news_ago(since: str, at: num) -> str { let mins = floor((at - date.ms(since)) / 60000) mins < 1 ? "Just now" : (mins < 60 ? `${mins} min ago` : (mins < 1440 ? `${floor(mins / 60)} h ago` : date.format(since, "MMM D")))}component BreakingNews(headline: str, href: str? = null, summary: str? = null, label: str = "Breaking", tone: str = "danger", datetime: str? = null, time: str? = null, dismissable: bool = true, remember: str? = null, name: str = "Breaking news", on_dismiss = null, id: str? = null, class: str? = null) { style global breaking_news_css state shown = true state at = now() let key = remember != null ? `ui-breaking:${remember}` : null if shown && datetime != null && time == null { every(30000, () => { at = now() }) } let close = () => { shown = false if key != null { js("localStorage.setItem", key, "1") } if on_dismiss != null { on_dismiss() } } <div class="ui-brk-host" @mount={(_el) => { if key != null && js("localStorage.getItem", key) == "1" { shown = false } }}> {shown && <section class={ui_cls(["ui-brk", tone != "danger" && `ui-brk-${tone}`, class])} id={id} aria-label={name}> <span.ui-brk-label>{label}</span> <div.ui-brk-body> <p.ui-brk-headline>{href != null ? <a href={href}>{headline}</a> : headline}</p> {summary != null && <p.ui-brk-summary>{summary}</p>} </div> {(time != null || datetime != null) && <time.ui-brk-time datetime={datetime}>{time ?? breaking_news_ago(datetime, at)}</time>} {dismissable && <button.ui-brk-x type="button" aria-label={`Dismiss: ${headline}`} @click={close}>×</button>} </section>} </div>}Related
LiveBlogA live blog: timestamped updates with a pinned summary on top, key events that jump to their entry, key-events-only and oldest-first switches, and paging.8 views · 8 downloadsMatchScoreA game's score: both teams with crests or colours, the score and scorers, and where it stands: kick-off, live with the minute, half time, full time or postponed.10 views · 8 downloadsPaywallMeterA metered paywall: a slim bar counting the free articles read, a warning on the last, then the gate with the offer, its price, benefits and sign-in.8 views · 8 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 downloadsArticleHeaderThe 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.8 views · 8 downloadsCorrectionNoticeA 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 downloads
Thread
Thread 0 posts
On BreakingNews: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.