Libon

CSS Conditional Rules

#CSS
学习并掌握 CSS 条件规则,以及它们的用法与用途.

ToC

Is CSS Turing complete?

首先我们需要知道 CSS Conditional Rules 是什么东西。和字面意思一样,就是用于在 CSS 中做某些条件判断,就像其他编程语言中的 if 语句一样。大家都在说 CSS 不是一个编程语言,也包括我们一直在学习的教程里也是这么说,但你可能不知道,CSS 其实是一门 图灵完备 的语言,不过它并不是完整的,因为它还需要依赖于 HTML,所以“如果将用户交互视为 CSS ‘执行’ 的一部分”,那么 CSS + HTML 在创建布局这个方面上,它是图灵完备的。我在网上找到了一些能够证明这一观点的文章及一些仓库等,如果你有兴趣的话可以点击看看:

那什么是“图灵完备” ?宏观来讲,如果一个编程语言等价于 图灵机,那么它就是图灵完备的。图灵机是 Alan Turing 在 1936 年发明的,它称之为 “a-manchine”,也就是 ”自动机器“,而现代用于判断一门语言是否是图灵完备的条件则是使用了 Rule 110 规则,即:它是否可以完成 Rule 110 自动机。Rule 110 自动机简单来讲可以理解为它是否可以创建一个无限循环的状态机,详细的论证可以看一下 wiki 上的具体介绍。

关于图灵完备和 Rule 110 我们暂时先讲到这,以上内容只是为了证明在 CSS 中其实是存在着基本判断语句的,那么回到我们今天的主题,在 CSS 中,Conditional Rules 又是什么?

Conditional Rules

condition 即:条件,也就是我们日常编程中写的 if 语句或者是 ?: 三元表达式的那种条件。这个东西并不是空穴来风,也不是我的凭空捏造,它是确确实实存在的,证据如下:

在 MDN 中列举出了 4 条 rules ,它们分别是 @document @media @supports @import

@document

@docuemnt 目前只有 firefox 浏览器对其进行了实验性的支持,它的作用是如果当前的 url 和 @document url(<url>) 中的 <url> 进行匹配成功的话,那么其嵌套的样式就会生效。

@document url("https://www.example.com/") {
  h1 {
    color: green;
  }
}

@media

@media 这个大家应该都用过,这是用于判断浏览器当前是否支持某种设备类型或是设备类型的某项参数是否符合要求的时候使用的,比如说 @media print 是用于判断当前是否是打印机预览的,@media (any-hover: hover) 是用于判断浏览器中是否支持触点设备进行 :hover 的,@media screen and (min-width: 900px) 是用于判断当前显示器的屏幕像素是否大于 900px 的,等等类似这些的判断还有很多,鉴于大家对此都较为熟悉,就不详细展开了。

@supports

@supports 则是用于判断浏览器是否支持某个特性的,比如在移动端 chrome、safari 浏览器在点击链接和按钮的时候会有一个背景高亮,正常情况下我们会使用 -webkit-tap-highlight-color: transparent 来将背景颜色设置为透明,但是它带有 -webkit- 前缀,不是一个标准的属性,意味着它不能在其他所有浏览器中正常工作,而在其他无法工作的浏览器中运行的话,会将这个属性作为无效属性处理,在 Edge 浏览器中的 development devtools 面板中会提示这个属性不是标准的,这可能会降低网站的评分,那么我们就可以使用 @supports 来解决这个问题:

@supports (-webkit-tap-highlight-color: transparent) {
  a, [role="button"], button, input[type="button"] {
    -webkit-tap-highlight-color: transparent;
  }
}

同样的,在部分浏览器中支持 display 双值 的写法:display: block flow,同样可以使用 @supports 来进行检测判断:

@supports (display: block flow) {
	.block-flow-box {
		display: block flow;
	}
}

最重要的是,它还可以通过判断 css variables 自定义属性,比如:

@supports (--display: true) {
	.display-box {
		display: block
	}
}

当然这种用法只是一种抛砖引玉,真正要用时肯定不仅局限于此,我只是想用它来激发你产生更多的灵感而已。

@import

@import 这个方法咋一看会觉得它为什么也是 conditional rules ?但有些同学不知道的是它也是可以设置媒体查询的条件的,比如:

@import url("fineprint.css") print; // 在打印机打印的时候生效
@import url("bluish.css") projection, tv; // 在投影仪、或者大屏 tv 的时候生效
@import "common.css" screen, projection; // 在普通屏幕、投影仪设备上生效

More?

MDN 上收录的就只有这四个了,但其实还有很多可以实现这种类似的条件语句。比如说更为常见的 :empty :active :hover :focus :link :visited :first-child 等等其他关于元素状态的伪类,他们都是只有在特定场景下,某些条件符合的情况下才会生效的伪类,以下列出了一些比较常见的CSS伪类:

:activea:active选择活动的链接。
:checkedinput:checked选择被选中的 input 元素。
:disabledinput:disabled选择被禁用的 input 元素。
:emptyp:empty选择没有子元素的 p 元素。
:enabledinput:enabled选择已启用的 input 元素。
:first-childp:first-child选择其父级的首个子元素的 p 元素。
:first-of-typep:first-of-type选择其父级的首个 p 元素的 p 元素。
:focusinput:focus选择获得焦点的 input 元素。
:focus-withindiv:focut-within当子元素获取到焦点, 同时使用 :focus-within 的元素也可以获取焦点的时候生效。
:hovera:hover选择鼠标悬停其上的链接。
:in-rangeinput:in-range选择具有指定范围内的值的 input 元素。
:invalidinput:invalid选择所有具有无效值的 input 元素。
:lang(language)p:lang(it)选择 lang 属性值以 “it” 开头的 p 元素。
:last-childp:last-child选择其父级的最后一个子元素的 p 元素。
:last-of-typep:last-of-type选择其父级的最后一个 p 元素的 p 元素。
:linka:link选择所有未被访问的链接。
:not(selector):not(p)选择非 p 元素的元素。
:nth-child(n)p:nth-child(2)选择其父级的第二个子元素的 p 元素。
:nth-last-child(n)p:nth-last-child(2)选择作为父的第二个子元素的 p 元素,从最后一个子元素计数。
:nth-last-of-type(n)p:nth-last-of-type(2)选择作为父的第二个 p 元素的 p 元素,从最后一个子元素计数
:nth-of-type(n)p:nth-of-type(2)选择其父级的第二个 p 元素的 p 元素。
:only-of-typep:only-of-type选择其父级的唯一 p 元素的 p 元素。
:only-childp:only-child选择其父级的唯一子元素的 p 元素。
:optionalinput:optional选择不带 “required” 属性的 input 元素。
:out-of-rangeinput:out-of-range选择值在指定范围之外的 input 元素。
:read-onlyinput:read-only选择指定了 “readonly” 属性的 input 元素。
:read-writeinput:read-write选择不带 “readonly” 属性的 input 元素。
:requiredinput:required选择指定了 “required” 属性的 input 元素。
:rootroot选择元素的根元素。
:target#news:target选择当前活动的 #news 元素(单击包含该锚名称的 URL)。
:validinput:valid选择所有具有有效值的 input 元素。
:visiteda:visited选择所有已访问的链接。

但我们要聊的并不止这些。

@container

@container 是一个容器查询的条件规则,可以通过它来查询某个元素的尺寸大小、某些样式等,它内部的样式只有在容器查询对其元素的查询容器为 true 的时候才会进行匹配(生效).

<style>
    body {
			// 这条属性是必须的, 需要加在想要查询的父级元素上
			// 它的值允许为 normal | size | inline-size
			// https://www.w3.org/TR/css-contain-3/#container-type
      container-type: size

    }

    @container (min-width: 400px) {
      div {
        color: #f00
      }
    }
  </style>

<div>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Atque nesciunt accusantium ullam voluptate, laudantium rerum impedit tempora non
  temporibus minima recusandae esse nemo praesentium, debitis voluptatibus ad officia, fugiat earum.
</div>

如果想要查询父级元素的样式是否符合某个值则可以使用 style() 来对 CSS 自定义属性进行匹配:

<style>
  body {
		// 使用样式查询的时候 container-name 属性是必须的
		// 它的作用是给当前父容器起一个容器名称, 方便查询
    container-name: body;
    --featured: true; // 设置一个查询的值
		--theme: dark;
  }

  @container body style(--featured: true) {
    div {
      color: #f00
    }
  }

	// 或者作为主题属性查询
	@container body style(--theme: dark) {
    div {
      color: #fff
    }
  }
</style>

<div>
    Lorem ipsum dolor sit, amet consectetur adipisicing elit. Atque nesciunt accusantium ullam voluptate, laudantium rerum impedit tempora non
    temporibus minima recusandae esse nemo praesentium, debitis voluptatibus ad officia, fugiat earum.
</div>

:has + :nth-last-child

以下的例子都需要将其组合起来才能看到效果。

从倒数第三个开始(包含倒数第三个),往上将所有的 li 的字体颜色设置为红色, 如果去掉 -last, 则会变成正数, 最后的结果是从正数第三个开始, 往下所有的 li 字体颜色都会变成红色

li:nth-last-child(n + 3) {
  color: red;
}

如果子项小于5条, 那么会展示下边框, 如果多于5条则不会展示边框, 同时因为 display 不再为 list-item, 所以左侧的圆点也会消失, 效果如下

<style>
li {
  border-bottom: 1px solid #000
}

li:nth-last-child(n + 5),
li:nth-last-child(n + 5)~li {
  width: 50%;
  display: inline-block;
  border-bottom: 0;
}
</style>
<ul>
  <li><span>name: </span><span>1</span></li>
  <li><span>name: </span><span>2</span></li>
  <li><span>name: </span><span>3</span></li>
  <li><span>name: </span><span>4</span></li>
</ul>

<ul>
  <li><span>name: </span><span>1</span></li>
  <li><span>name: </span><span>2</span></li>
  <li><span>name: </span><span>3</span></li>
  <li><span>name: </span><span>4</span></li>
  <li><span>name: </span><span>5</span></li>
  <li><span>name: </span><span>6</span></li>
</ul>

根据子元素的数量动态调整布局样式

<style>
.site-header__wrapper {
  display: flex;
  align-items: center;
  gap: 1rem;
  background-color: #f2ebff;
  border-radius: 12px;

  > * {
    flex: 1;
    padding: 0.5rem 1rem;
  }
}

.site-header__middle {
  text-align: center;

  a {
    font-weight: bold;
  }
}

.site-header__end {
  display: flex;
  justify-content: flex-end;
}

.c-nav {
  display: flex;
  gap: 0.5rem;

  a {
    display: block;
    padding: 1rem 0.5rem;
  }
}

.site-header__wrapper:has(li:nth-last-child(n + 4)) {
  --layout-2: true;
}

@container style(--layout-2: true) {
  .site-header__wrapper {
    > * {
      flex: initial;
    }
  }

  .site-header__start {
    order: 2;
  }

  .site-header__middle {
    order: -1;
    text-align: start;
  }

  .site-header__end {
    margin-left: auto;
  }
}

a {
  text-decoration: none;
  color: #222;
}
</style>

<header class="site-header">
  <div class="wrapper site-header__wrapper">
    <div class="site-header__start">
      <button>Search</button>
    </div>
    <div class="site-header__middle">
      <a href="#">Brand</a>
    </div>
    <div class="site-header__end">
      <ul class="c-nav">
        <li><a href="#">Home</a></li>
        <li><a href="#">About</a></li>
        <li><a href="#">Home</a></li>
        <li><a href="#">Home</a></li>
        <li><a href="#">Home</a></li>
      </ul>
    </div>
  </div>
</header>

根据子元素的数量动态调整布局样式

<div class="modal">
  <div class="modal__header">
    <h2>Title to be added in here. It should be about two lines.</h2>
    <button class="btn btn--icon" aria-label="Close">
      <svg width="100pt" height="100pt" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
        <path d="m50 43.57l-19.281-19.281c-1.7891-1.7852-4.6562-1.7773-6.4336-0.003906-1.7852 1.7891-1.7734 4.6562 0.003906 6.4336l19.281 19.281-19.281 19.281c-1.7852 1.7891-1.7773 4.6562-0.003906 6.4336 1.7891 1.7852 4.6562 1.7734 6.4336-0.003906l19.281-19.281 19.281 19.281c1.7891 1.7852 4.6562 1.7773 6.4336 0.003906 1.7852-1.7891 1.7734-4.6562-0.003906-6.4336l-19.281-19.281 19.281-19.281c1.7852-1.7891 1.7773-4.6562 0.003906-6.4336-1.7891-1.7852-4.6562-1.7734-6.4336 0.003906z" fill-rule="evenodd" />
      </svg>

    </button>
  </div>
  <div class="modal__body">
    <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Perspiciatis harum odit repellat beatae dolore voluptate doloremque eaque in voluptatem similique tenetu.</p>
  </div>
  <div class="modal__footer">
    <a href="#" class="btn">Ok, I understand</a>
  </div>
</div>

<style lang="scss">
.modal {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 450px;
  margin: 3rem auto;
  padding: 1rem;
  background-color: #eee7f8;
  border-radius: 15px;
}

.modal__header {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  h2 {
    font-weight: bold;
    font-size: 1.5rem;
  }
}

.btn {
  display: inline-flex;
  color: #fff;
  background-color: #7f4fce;
  padding: 0.5rem 1rem;
  border-radius: 100px;
  appearance: none;
  border: 0;
  font-size: 15px;
  cursor: pointer;
  border: 1px solid transparent;
}

.btn--ghost {
  background-color: transparent;
  color: #222;
  border-color: rgba(#000, 0.15);
}

.btn--icon {
  --size: 32px;
  flex: 0 0 var(--size);
  width: var(--size);
  height: var(--size);
  align-items: center;
  justify-content: center;
  padding: 0;
  background-color: transparent;

  svg {
    fill: #797979;
    width: var(--size);
    height: var(--size);
  }
}

.modal__footer {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}

.modal__footer:has(a:nth-last-child(n + 2)) {
  justify-content: flex-end;
}
</style>

根据子元素的数量动态调整布局样式

<div class="wrapper">
  <div style="text-align: center; margin-bottom: 3rem;">
    <button id="add">Add author</button>
    <button id="remove">Remove author</button>
  </div>

  <div class="post-author" style="margin-bottom: 2rem;">
    <h3>Written by:</h3>
    <div class="post-author__wrapper">
      <div class="avatars-list">
        <img class="avatar" src="https://xsgames.co/randomusers/avatar.php?g=male" alt="">
      </div>
      <p class="authors"><a href="#">Ahmad Shadeed</a></p>
    </div>
  </div>
</div>

<style lang="scss">
.post-author {
  display: flex;
  flex-flow: column wrap;
  gap: 1rem;

  h3 {
    text-transform: uppercase;
    font-size: 13px;
  }
}

.post-author__wrapper {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;

  a {
    text-decoration: underline;
  }
}

.post-author:has(img:nth-last-child(n + 2)) {
  --multiple-avatars: true;
}

@container style(--multiple-avatars: true) {
  .avatars-list {
    display: flex;
    background-color: #efefef;
    padding: 8px 12px;
    border-radius: 50px;
  }

  img:not(:first-child) {
    border: solid 2px #fff;
    margin-left: -0.25rem;
  }
}

.avatars-list {
  display: flex;
}

.avatar {
  width: 40px;
  height: 40px;
  background-color: green;
  border-radius: 50%;

  @container style(--multiple-avatars: true) {
    width: 30px;
    height: 30px;
  }
}

.wrapper {
  max-width: 600px;
  margin: auto;
  padding: 1rem;
}

a {
  text-decoration: none;
  color: #222;
}

img {
  display: inline-block;
  object-fit: cover;
}
</style>

以上。


CD ..