News

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.

#news#banner#alert#live#interactive

Demo

Preview
Breaking

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.

Developing

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.qwe
main.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>}
Thread
98Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.