Yeti 的 Media 布局配方:用 contenteditable="false">【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.
项目地址: https://gitcode.com/gh_mirrors/fo/yeti
Yeti 的 Media 是一个开箱即用的"图配文"布局配方:一行.media类名即可把图片(figure)放在一段文本旁,文本宽度一旦跌到容器一半以下就自动垂直堆叠。本文以 docs/media.md 为主线,结合 media.css、manifest.json 与 测试用例,讲清它的使用场景、五个data-*属性的完整取值、源码级的实现原理,以及它在浏览器兼容与无障碍方面的保障。读完你可以直接用它搭建头像评论、商品缩略图、演讲者名片等常见 UI,也能把它拆回 sidebar、frame、stack 三个原语做任意定制。
Media 是什么
Media 是"图片与几行文字并排摆放"这一经典形态的零构建实现。官方描述是:
Puts a figure beside a block of text and stacks them when the text would drop below half the width; the one-class form of a sidebar holding a frame and a stack.
即:把图形放在文本块旁边,当文本宽度跌到容器一半以下时自动堆叠;它是"sidebar 内放一个 frame 和一个 stack"的单类名形态。
最基础的用法来自 src/recipes/media/example.html:
<div class="media"><div class="sidebar">.media { display: flex; flex-wrap: wrap; align-items: var(--_yeti-align); gap: var(--_yeti-gap); } .media:not([data-gap]) { --_yeti-gap: var(--yeti-space-md); } .media:not([data-width]) { --_yeti-width: var(--yeti-width-xs); } .media:not([data-align]) { --_yeti-align: stretch; } .media:not([data-ratio]) { --_yeti-aspect: 1 / 1; } .media > * { margin: 0; }注意:not([data-gap])这类写法:只有属性缺席时才注入默认值。这样嵌套布局永远不会继承父级的私有属性值(attributes.css 文件头注释明确说明了这一设计意图)。flex-wrap: wrap是"自动堆叠"的基石。
2. figure 的识别::is()+:has()
.media > :is(img, video, picture), .media > :has(> :is(img, video, picture)) { flex-basis: var(--_yeti-width); flex-grow: 1; aspect-ratio: var(--_yeti-aspect); overflow: hidden; } .media > :is(img, video) { object-fit: cover; }figure 的判定条件是:直接子元素本身就是图片媒体,或者直接子元素内包裹了图片媒体(> :has(> :is(img, video, picture)))。后者正是"figure 外包了一层 div/picture"场景能工作的原因——<div><picture><img></picture></div>也能被正确识别为 figure。这一点被测试the figure is found wherever it sits和the fill reaches an img inside a picture inside a wrapped figure覆盖,后者断言最内层img的宽高与 wrapper 完全一致(误差 0px)。
CSS 注释里还解释了一个陷阱:reset 层会把picture设为display: block,因此对"figure 与 img 之间夹着 picture"的情况,需要给picture自身补上inline-size: 100%; block-size: 100%,否则 img 的百分比高度没有可解析的参照物,会回退到内容高度。源码中:is(picture, :has(...))这条规则正是为此存在。
3. 正文的"保底半宽"与堆叠
.media > :not(:is(img, video, picture), :has(> :is(img, video, picture))) { display: flex; flex-direction: column; gap: var(--yeti-space-sm); flex-basis: 0; flex-grow: 999; min-inline-size: 50%; }正文的识别用:not()排除掉所有 figure 形态。关键在三个数字:
flex-basis: 0+flex-grow: 999:正文吃掉所有剩余空间,figure 只占其首选宽度;min-inline-size: 50%:正文拒绝缩到容器一半以下——这就是"文本掉到半宽以下就堆叠"的机制。当容器足够宽时,figure(flex-grow: 1)和正文(flex-grow: 999)并排;当容器窄到连正文的一半宽都保不住时,flex-wrap: wrap让 figure 换行到上方,二者垂直堆叠。
这个机制与 sidebar.css 中"内容子元素flex-basis: 0; flex-grow: 999; min-inline-size: 50%"的写法完全同源——Media 就是 sidebar 的食谱化封装。
4. 带标题的 figure(figcaption)
.media > figure:has(> figcaption) { display: flex; flex-direction: column; aspect-ratio: auto; overflow: visible; } .media > figure:has(> figcaption) > :is(img, video, picture) { aspect-ratio: var(--_yeti-aspect); block-size: auto; }如果 figure 内部含有figcaption,只有图片本身被裁剪到宽高比,figcaption正常排在图片下方(不会被裁掉),并应用--yeti-text-sm字号与--yeti-color-text-muted的弱化文字颜色。测试a figure with a figcaption keeps its caption below the image, and the image keeps its ratio验证了 caption 位于图片下方且图片保持 1:1。
5.>.media[data-side="start"] > :is(img, video, picture), .media[data-side="start"] > :has(> :is(img, video, picture)) { order: -1; } .media[data-side="end"] > :is(img, video, picture), .media[data-side="end"] > :has(> :is(img, video, picture)) { order: 1; }
order只影响 flex 布局中的视觉顺序,DOM 顺序和屏幕阅读顺序均不受影响——这正是文档"moves it visually only; reading order stays as written"的源码依据。
子元素规则
manifest 与文档对子元素有明确约束:
- 必须恰好两个直接子元素(
> *,min 2 / max 2); - 第一个是 figure:
img、video、picture,或包裹其中之一的元素; - 第二个是正文 body:正文内部不得再直接嵌套
img、video、picture(会被当作第二个 figure); - figure 内若带
figcaption,标题保留在图片下方(见上文实现)。
测试children have no margins同时断言了.media的所有直接子元素都没有默认外边距。
Tokens
Media 依赖的公开 token(可在主题中覆盖):
| Token | 说明 |
|---|---|
--yeti-width-xs | 默认 figure 宽度 |
--yeti-space-md | 默认 gap(figure 与正文之间) |
--yeti-space-sm | 正文内部子元素之间的间距 |
另有四个内部 token(--_yeti-gap、--_yeti-width、--_yeti-align、--_yeti-aspect),以下划线开头,可能在小版本之间变化,不建议在主题中依赖:
--_yeti-gap:figure 与正文间距,来自data-gap;--_yeti-width:figure 宽度,来自data-width;--_yeti-align:交叉轴对齐,来自data-align;--_yeti-aspect:figure 宽高比,来自data-ratio。
无障碍(Accessibility)
manifest 中记录的 a11y 说明有两点:
- figure 会被裁剪到固定宽高比,因此alt 文本应描述图片实际可见的部分——被裁掉的内容不应写进 alt;
data-side改变的是 figure 的视觉位置而非阅读顺序,所以无论图片在左在右,屏幕阅读器都会按源码顺序朗读。
media.spec.js 最后一条has no accessibility violations用 axe 对 fixture 页面做完整扫描,断言无任何无障碍违规。
JavaScript 与浏览器支持
- JavaScript:无。manifest 中
"js": null,Media 是纯 CSS 组件,可用 test/browser/fixtures/recipes/media.html 这类 fixture 在无 JS 场景下直接验证。 - 浏览器支持(无守卫使用,manifest
support.unguarded):flexbox gap、aspect-ratio、object-fit、:has();@supports守卫:无。也就是说整套实现依赖现代 CSS 特性,:has()与aspect-ratio需要较新的浏览器版本(Chrome 105+ / Safari 15.4+ / Firefox 121+ 等);旧浏览器不支持时不会有降级方案。 - 可用版本:自
7.0.0起提供(manifest"since": "7.0.0")。
完整实战示例
1. 头像评论(默认方形,图在左)
<div class="media"> <img src="avatar.jpg" alt="Portrait of Ada Lovelace"> <div> <h3>Ada Lovelace</h3> <p>Wrote the first published algorithm, for Babbage's Analytical Engine.</p> </div> </div>2. 商品缩略图(图在右、16:9、大间距)
<div class="media"><div class="media"><div class="media"> <figure> <img src="summit.jpg" alt="Sunrise from the summit"> <figcaption>A view worth the early start.</figcaption> </figure> <div> <h3>Weekend in the hills</h3> <p>Six miles, one summit.</p> </div> </div>图片被裁剪到 1:1,标题留在图片下方、使用弱化文字颜色,不会被裁掉。
5. 包裹式 figure(picture 响应式图片)
<div class="media"> <div> <picture> <source srcset="hero.avif" type="image/avif"> <img src="hero.jpg" alt="A hero shot of the product"> </picture> </div> <div> <h3>A picture wrapped in a div</h3> <p>The fill rule reaches past the wrapper and the picture.</p> </div> </div>得益于:has(> :is(img, video, picture))选择器,外包 div 也能被识别为 figure 并正确铺满裁剪区域。
小结
Media 是 Yeti 中"以最小代价获得经典图配文形态"的入口:一个类名 + 五个data-*属性,即可获得固定比例裁剪、半宽保底自动堆叠、垂直对齐控制与阅读顺序安全的左右切换。它的实现完全建立在 sidebar + frame + stack 三个原语之上,源码(media.css)与测试(media.spec.js)互为印证,既适合直接在生产中使用,也适合作为学习"用属性驱动 CSS 布局"的范本。
【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.项目地址: https://gitcode.com/gh_mirrors/fo/yeti
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考