Layout

BentoGrid

A grid of tiles spanning columns and rows, with titles, tints and links, packed to leave no holes.

#layout#grid#card#marketing#no-js

Demo

Preview

Fast

Pages in 40 ms, components resumed, not rebuilt.

40 ms

Safe

Escaped by default.

Docs

Guides and the reference.

Small

30 KB of runtime, once.

Tested

98%

Live

Pages follow the data.

The same wall in a 480px-wide box: two columns, wide tiles across both.

Fast

Pages in 40 ms, components resumed, not rebuilt.

40 ms

Safe

Escaped by default.

Docs

Guides and the reference.

Small

30 KB of runtime, once.

Tested

98%

Live

Pages follow the data.

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/bento_grid.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/bento_grid.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/bento_grid.qwe
view BentoGridDemoTiles() =    [        <BentoTile cols={2} rows={2} tone="accent" title="Fast" subtitle="Pages in 40 ms, components resumed, not rebuilt." class="bento-grid-demo-big">            <p style="margin: 12px 0 0; font-size: 40px; font-weight: 650; letter-spacing: -.02em">40 ms</p>        </BentoTile>,        <BentoTile title="Safe" subtitle="Escaped by default." class="bento-grid-demo-small"/>,        <BentoTile title="Docs" subtitle="Guides and the reference." href="https://quillolang.org/docs/" class="bento-grid-demo-link"/>,        <BentoTile cols={2} tone="subtle" title="Small" subtitle="30 KB of runtime, once." class="bento-grid-demo-wide"/>,        <BentoTile tone="success" title="Tested"><p style="margin: 0; font-size: 28px; font-weight: 650">98%</p></BentoTile>,        <BentoTile tone="info" title="Live"><p style="margin: 0; color: var(--ui-muted)">Pages follow the data.</p></BentoTile>    ]view BentoGridDemo() =    <div.demo-stack>        <BentoGrid id="bento-grid-demo-wall" columns={4} rows="140px"><BentoGridDemoTiles/></BentoGrid>        <p.demo-note>The same wall in a 480px-wide box: two columns, wide tiles across both.</p>        <div style="width: 480px; max-width: 100%">            <BentoGrid id="bento-grid-demo-narrow" columns={4} rows="120px" gap="sm"><BentoGridDemoTiles/></BentoGrid>        </div>    </div>

Source

ui/bento_grid.qwe
// BentoGrid: a grid of tiles of different sizes, each spanning columns and rows,// as on a product page's feature wall or a dashboard's front. The tiles are// BentoTiles; a wide one takes two columns on a phone and a narrow one takes one.////   <BentoGrid columns={4} rows="160px">//       <BentoTile cols={2} rows={2} title="Fast" subtitle="Pages in 40 ms">...</BentoTile>//       <BentoTile title="Safe">...</BentoTile>//       <BentoTile cols={2} title="Small" href="/docs/size">...</BentoTile>//   </BentoGrid>//// BentoGrid: columns, how many across (4); rows: each row's height, as CSS ("180px");// gap: pixels (16, the default) or "none", "xs" (4), "sm" (8), "md" (16), "lg" (24), "xl" (32).// Narrower than 640px it is two columns, and narrower than 400px one; tiles are// packed to leave no holes. The grid measures its own width, not the window's.// BentoTile: cols, rows: how many it spans (1 each). title, subtitle: its heading// and the line under it, at the top; children: the rest. href makes the tile a link.// tone: "plain" (default), "subtle", "accent", "success", "warning", "danger" or// "info", a tint. image: a URL shown across the whole tile, behind the text.import ui/theme.qwe use ui_clslet bento_grid_css = css`.ui-bento-wrap { container-type: inline-size; min-width: 0; }.ui-bento {  display: grid; grid-template-columns: repeat(var(--ui-bento-cols, 4), minmax(0, 1fr)); grid-auto-rows: var(--ui-bento-row, 180px);  grid-auto-flow: dense; gap: var(--ui-bento-gap, 16px); min-width: 0; color: var(--ui-fg); font: 14px/1.5 var(--ui-font);}.ui-bento-tile {  position: relative; display: flex; flex-direction: column; grid-column: span var(--ui-bento-c, 1); grid-row: span var(--ui-bento-r, 1);  min-width: 0; overflow: hidden; padding: 18px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: inherit; text-decoration: none;}.ui-bento-head { display: grid; gap: 2px; margin-bottom: 8px; }.ui-bento-title { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; }.ui-bento-sub { margin: 0; color: var(--ui-muted); }.ui-bento-body { flex: 1; min-height: 0; }.ui-bento-body > :first-child { margin-top: 0; }.ui-bento-body > :last-child { margin-bottom: 0; }.ui-bento-image { position: absolute; inset: 0; z-index: -1; background: center / cover no-repeat; }.ui-bento-image::after { content: ""; position: absolute; inset: 0; background: linear-gradient(transparent 40%, color-mix(in srgb, var(--ui-surface) 90%, transparent)); }.ui-bento-tile:has(.ui-bento-image) { isolation: isolate; justify-content: flex-end; }.ui-bento-subtle { background: var(--ui-subtle); border-color: transparent; }.ui-bento-accent { background: var(--ui-accent-soft); border-color: transparent; }.ui-bento-accent .ui-bento-title { color: var(--ui-accent); }.ui-bento-success { background: var(--ui-success-soft); border-color: transparent; }.ui-bento-success .ui-bento-title { color: var(--ui-success); }.ui-bento-warning { background: var(--ui-warning-soft); border-color: transparent; }.ui-bento-warning .ui-bento-title { color: var(--ui-warning); }.ui-bento-danger { background: var(--ui-danger-soft); border-color: transparent; }.ui-bento-danger .ui-bento-title { color: var(--ui-danger); }.ui-bento-info { background: var(--ui-info-soft); border-color: transparent; }.ui-bento-info .ui-bento-title { color: var(--ui-info); }a.ui-bento-tile { transition: border-color .15s, box-shadow .15s; }a.ui-bento-tile:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }a.ui-bento-tile:focus-visible { outline: none; box-shadow: var(--ui-focus); }@container (max-width: 639px) {  .ui-bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }  .ui-bento-tile { grid-column: span 1; }  .ui-bento-wide { grid-column: span 2; }}@container (max-width: 399px) {  .ui-bento { grid-template-columns: minmax(0, 1fr); }  .ui-bento-tile, .ui-bento-wide { grid-column: span 1; }}@media (prefers-reduced-motion: reduce) { a.ui-bento-tile { transition: none; } }`fn bento_grid_gap(gap) -> str =    match gap {        int as n => `${n}px`        float as n => `${n}px`        "none" => "0"        "xs" => "4px"        "sm" => "8px"        "lg" => "24px"        "xl" => "32px"        _ => "16px"    }view BentoGrid(columns: int = 4, rows: str = "180px", gap = 16, id: str? = null, class: str? = null, children) {    style global bento_grid_css    <div class={ui_cls(["ui-bento-wrap", class])} id={id}>        <div.ui-bento style={`--ui-bento-cols: ${columns}; --ui-bento-row: ${rows}; --ui-bento-gap: ${bento_grid_gap(gap)}`}>{children}</div>    </div>}view BentoTile(cols: int = 1, rows: int = 1, title: str? = null, subtitle: str? = null, href: str? = null, tone: str = "plain",               image: str? = null, id: str? = null, class: str? = null, children) {    style global bento_grid_css    let cls = ui_cls(["ui-bento-tile", cols > 1 && "ui-bento-wide", tone != "plain" && `ui-bento-${tone}`, class])    let st = `--ui-bento-c: ${cols}; --ui-bento-r: ${rows}`    let body = [        image != null && <div.ui-bento-image style={`background-image: url("${replace(image, "\"", "%22")}")`} aria-hidden="true"></div>,        (title != null || subtitle != null) && <div.ui-bento-head>            {title != null && <h3.ui-bento-title>{title}</h3>}            {subtitle != null && <p.ui-bento-sub>{subtitle}</p>}        </div>,        <div.ui-bento-body>{children}</div>    ]    href != null ? <a class={cls} id={id} style={st} href={href}>{body}</a> : <div class={cls} id={id} style={st}>{body}</div>}
Thread
96Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.