Data

ArticleCard

A post to click into: image, category, title, excerpt, author, date and reading time; image on top or to the side.

#card#marketing#page#link#no-js

Demo

Preview
Rocks in a calm sea
Engineering

Islands, resumed

How a page's components wake up exactly where the server left them, without rendering twice.

  • components
  • runtime
Ada Lovelace
4 min read
Design

One theme, two modes

Every colour is a variable, so dark mode is the system's choice and yours: set ui-dark on any element.

Alan Turing
3 min read

Get it

It needs theme.qwe, as every component does, and avatar.qwe; 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/avatar.qwe$ curl -O https://quillolang.org/components/ui/article_card.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/article_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/article_card.qwe
view ArticleCardDemo() =    <div.demo-stack>        <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px">            <ArticleCard id="article-card-demo-first" href="#article_card" src="https://picsum.photos/seed/islands/640/360" alt="Rocks in a calm sea"                         category="Engineering" category_href="#article_card" title="Islands, resumed"                         excerpt="How a page's components wake up exactly where the server left them, without rendering twice."                         author="Ada Lovelace" date="2026-10-12" read_time="4 min read" tags={["components", "runtime"]}/>            <ArticleCard href="#article_card" src="https://picsum.photos/seed/forms/640/360" category="Guides" title="Forms that post without a script"                         excerpt="Every field in the kit is a real input, so a plain form sends it." author="Grace Hopper"                         author_src="https://i.pravatar.cc/64?img=47" date="2026-09-28" read_time="7 min read"/>            <ArticleCard id="article-card-demo-bare" href="#article_card" title="Release notes, October" date="2026-10-01"/>        </div>        <ArticleCard id="article-card-demo-wide" layout="horizontal" href="#article_card" src="https://picsum.photos/seed/wide/640/400"                     category="Design" title="One theme, two modes"                     excerpt="Every colour is a variable, so dark mode is the system's choice and yours: set ui-dark on any element."                     author="Alan Turing" date="2026-08-15" read_time="3 min read"/>    </div>

Source

ui/article_card.qwe
// ArticleCard: a post or an article to click into: its image, category, title,// a line of excerpt, who wrote it and when, and how long it takes to read.////   <ArticleCard href="/blog/islands" src="/img/islands.jpg" category="Engineering"//                title="Islands, resumed" excerpt="How a page's components wake up where the server left them."//                author="Ada Lovelace" date="2026-10-12" read_time="4 min read"/>//   <ArticleCard layout="horizontal" {...post}/>//// title: the heading, a link to href. src, alt: the image (none, no image area).// category: a small label over the title, a link to category_href when given.// excerpt: a line or two under the title. author, author_src: who wrote it, with// their picture (initials without one). date: an ISO date, shown as "Oct 12, 2026".// read_time: "4 min read", after the date. tags: a few words shown as chips.// layout: "vertical" (default, image on top) or "horizontal" (image to the left,// on top again where it is narrow). id: the card's id. Needs avatar.qwe.import ui/avatar.qwe use Avatarimport ui/theme.qwe use ui_clslet article_card_css = css`.ui-article-card { position: relative; display: flex; flex-direction: column; min-width: 0; overflow: hidden; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); container-type: inline-size; }.ui-article-card-img { flex: none; aspect-ratio: 16 / 9; background: var(--ui-subtle); overflow: hidden; }.ui-article-card-img img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .3s; }.ui-article-card:hover .ui-article-card-img img { transform: scale(1.03); }.ui-article-card-body { display: flex; flex-direction: column; gap: 6px; flex: 1; padding: 16px 18px 18px; }.ui-article-card-cat { align-self: flex-start; color: var(--ui-accent); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; text-decoration: none; }a.ui-article-card-cat:hover { text-decoration: underline; }.ui-article-card-title { margin: 0; font-size: 17px; font-weight: 650; line-height: 1.3; }.ui-article-card-title a { color: inherit; text-decoration: none; }.ui-article-card-title a::after { content: ""; position: absolute; inset: 0; }.ui-article-card:hover .ui-article-card-title { color: var(--ui-accent); }.ui-article-card:has(.ui-article-card-title a:focus-visible) { box-shadow: var(--ui-focus); border-color: var(--ui-accent); }.ui-article-card-title a:focus-visible { outline: none; }.ui-article-card-excerpt { margin: 0; color: var(--ui-muted); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }.ui-article-card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin: 2px 0 0; padding: 0; list-style: none; }.ui-article-card-tags li { padding: 1px 8px; border-radius: 999px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; }.ui-article-card-meta { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 10px; color: var(--ui-muted); font-size: 13px; }.ui-article-card-meta > .ui-article-card-pic { --s: 26px; }.ui-article-card-author { color: var(--ui-fg); font-weight: 500; }.ui-article-card-when { display: flex; flex-wrap: wrap; gap: 4px; }.ui-article-card-when span + span::before { content: "·"; margin-right: 4px; }.ui-article-card-horizontal { flex-direction: row; }.ui-article-card-horizontal .ui-article-card-img { flex: 0 0 38%; aspect-ratio: auto; min-height: 160px; }@container (max-width: 480px) { .ui-article-card-horizontal { flex-direction: column; } .ui-article-card-horizontal .ui-article-card-img { flex: none; aspect-ratio: 16 / 9; } }@media (prefers-reduced-motion: reduce) { .ui-article-card-img img { transition: none; } }`let article_card_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date as "Oct 12, 2026"; other text as it is.fn article_card_date(d) -> str {    let p = split(slice(str(d), 0, 10), "-")    len(p) == 3 && int(p[1]) != null && int(p[2]) != null ? `${article_card_months[int(p[1]) - 1]} ${int(p[2])}, ${p[0]}` : str(d)}view ArticleCard(title: str, href: str, src: str? = null, alt: str = "", category: str? = null, category_href: str? = null,                 excerpt: str? = null, author: str? = null, author_src: str? = null, date = null, read_time: str? = null,                 tags: list = [], layout: str = "vertical", id: str? = null, class: str? = null) {    style global article_card_css    <article class={ui_cls(["ui-article-card", layout == "horizontal" && "ui-article-card-horizontal", class])} id={id}>        {src != null && <div.ui-article-card-img><img src={src} alt={alt} loading="lazy"/></div>}        <div.ui-article-card-body>            {category != null && (category_href != null                ? <a.ui-article-card-cat href={category_href}>{category}</a>                : <span.ui-article-card-cat>{category}</span>)}            <h3.ui-article-card-title><a href={href}>{title}</a></h3>            {excerpt != null && <p.ui-article-card-excerpt>{excerpt}</p>}            {len(tags) > 0 && <ul.ui-article-card-tags>{for t in tags => <li key={t}>{t}</li>}</ul>}            {(author != null || date != null || read_time != null) && <div.ui-article-card-meta>                {author != null && <Avatar name={author} src={author_src} size="xs" class="ui-article-card-pic"/>}                <div>                    {author != null && <div.ui-article-card-author>{author}</div>}                    <div.ui-article-card-when>                        {date != null && <span><time datetime={str(date)}>{article_card_date(date)}</time></span>}                        {read_time != null && <span>{read_time}</span>}                    </div>                </div>            </div>}        </div>    </article>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.