根据视口宽度动态缩放 CSS 值并不是什么新鲜话题。您可以在 CSS-Tricks 上找到许多深入的报道,例如 这篇 或 这篇。
但是,大多数这些示例使用 相对 CSS 单位 和 无单位值 来实现流畅缩放。这会丢失像素完美,并且通常会在屏幕低于或高于某个阈值时导致文本换行和布局偏移。
但是,如果我们真的想要像素完美怎么办?例如,如果我们正在开发一个复杂的实时分析仪表板,以便在会议室的大型电视上观看,或者作为一些仅在移动设备和平板电脑上打开的 PWA,而不是文本密集型博客和新闻网站?在这些情况下,我们需要更高的精度。
换句话说,如果我们想统一缩放设计怎么办?当然,可以根据可用宽度使用 CSS 转换来 缩放
内容,如 本文 中所述——这样,就可以保持正确的比例。
但是,我们也可以使用 CSS 中的像素值来实现流畅的比例缩放 UI。它们会根据设备屏幕尺寸进行适当缩放,同时保持其像素完美比例。此外,如果使用像素更舒适或更熟悉,我们仍然可以使用像素值并将其自动转换为相对 CSS 单位。
缩放我们的 UI
让我们尝试实现这个由 Craftwork 提供的 很棒的仪表板。我们需要以一种使其完美缩放并保留所有文本行数、边距、图像大小等的方式来制作它。

让我们使用 CSS 像素值并在 SCSS 中工作以提高速度和便利性。因此,如果我们要定位其中一个卡片小部件的标题,我们的 SCSS 可能如下所示
.cardWidget {
.cardHeading {
font-size: 16px;
}
}
没什么花哨的。我们以前从未见过。作为像素值,它不会缩放。
此设计是使用宽度为 1600px
的容器创建的。让我们假设在 1600px
时,卡片标题的理想字体大小应为 16px
,因为这就是它的设计方式。
现在我们有了此宽度的“理想”容器宽度字体大小,让我们使用当前*视口宽度相应地缩放我们的 CSS 像素值
/*
1600px is the ideal viewport width that the UI designers who
created the dashboard used when designing their Figma artboards
Please not we are not using pixel units here, treating it purely
as a numeric value.
*/
--ideal-viewport-width: 1600;
/*
The actual width of the user device
*/
--current-viewport-width: 100vw;
.cardWidget {
.cardHeading {
/*
16px is the ideal font size that the UI designers want for
1600px viewport width.
Please note that we are not using pixel units here,
treating it purely as a numeric value.
*/
--ideal-font-size: 16;
/*
Calculate the actual font size:
We take our idealFontSize and multiply it by the difference
between the current viewport width and the ideal viewport width.
*/
font-size: calc(
var(--ideal-font-size) * (var(--current-viewport-width) / var(--ideal-viewport-width)
);
}
}
如您所见,我们将从设计中获得的理想字体大小视为基准,并将其乘以当前和理想视口宽度之间的差值。从数学上讲,这看起来如何?假设我们在与模型具有完全相同宽度的屏幕上查看此 Web 应用程序
--current-device-width: 100vw; // represents 1600px or full width of the screen
--ideal-viewport-width: 1600; // notice that the ideal and current width match
--ideal-font-size: 16;
// this evaluates to:
font-size: calc(16 * 1600px / 1600);
// same as:
font-size: calc(16 * 1px);
// final result:
font-size: 16px;
因此,由于我们的视口宽度完全匹配,因此我们的 font-size
在 1600px
的理想视口宽度下最终正好为 16px
。
再举一个例子,假设我们在宽度为 1366px
的较小笔记本电脑屏幕上查看 Web 应用程序。以下是更新后的数学计算
font-size: calc(16 * 1366px / 1600);
// same as:
font-size: calc(16 * 0.85375px);
// final result:
font-size: 13.66px;
或者假设我们在宽度为 1920px
的全高清显示屏上查看它
font-size: calc(16 * 1920px / 1600);
// same as:
font-size: calc(16 * 1.2px);
// final result:
font-size: 19.2px;
您可以亲眼看到,即使我们使用像素值作为参考,我们实际上也能够根据理想视口大小和当前视口大小之间的宽度差来按比例缩放我们的 CSS 值。
我构建了一个小型演示来说明此技术
为了方便起见,这里有一个视频
限制最小和最大视口宽度
使用这种当前方法,设计会缩放以匹配视口大小,无论视口大小如何。我们可以使用 CSS clamp()
防止这种情况,它允许我们设置 350px
的最小宽度和 3840px
的最大宽度。这意味着如果我们要在宽度为 5000px
的设备上打开 Web 应用程序,我们的布局将保持锁定在 3840px
--ideal-viewport-width: 1600;
--current-viewport-width: 100vw;
/*
Set our minimum and maximum allowed layout widths:
*/
--min-viewport-width: 350px;
--max-viewport-width: 3840px;
.cardWidget {
.cardHeading {
--ideal-font-size: 16;
font-size: calc(
/*
The clamp() function takes three comma separated expressions
as its parameter, in the order of minimum value, preferred value
and maximum value:
*/
--clamped-viewport-width: clamp(var(--min-viewport-width), var(--current-viewport-width), var(--max-viewport-width);
/*
Use the clamped viewport width in our calculation
*/
var(--ideal-font-size) * var(--clamped-viewport-width) / var(--ideal-viewport-width)
);
}
}
让我们为单位转换创建一个辅助函数
我们的代码非常冗长。让我们编写一个简单的 SCSS 函数,将我们的值从像素转换为相对单位。这样,我们就可以在任何地方导入和重用它,而无需重复太多
/*
Declare a SCSS function that takes a value to be scaled and
ideal viewport width:
*/
@function scaleValue(
$value,
$idealViewportWidth: 1600px,
$min: 350px,
$max: 3840px
) {
@return calc(
#{$value} * (clamp(#{$min}, 100vw, #{$max}) / #{$idealViewportWidth})
);
}
/*
We can then apply it on any numeric CSS value.
Please note we are passing not pixel based, but numeric values:
*/
.myElement {
width: #{scaleValue(500)};
height: #{scaleValue(500)};
box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
font-size: #{scaleValue(24)};
}
移植到 JavaScript
有时 CSS 不够用,我们必须使用 JavaScript 来调整组件的大小。假设我们正在 动态构建 SVG,并且我们需要根据理想的设计宽度调整其 width
和 height
属性的大小。以下是如何实现此目的的 JavaScript 代码
/*
Our helper method to scale a value based on the device width
*/
const scaleValue = (value, idealViewportWidth = 1600) => {
return value * (window.innerWidth / idealViewportWidth)
}
/*
Create a SVG element and set its width, height and viewbox properties
*/
const IDEAL_SVG_WIDTH = 512
const IDEAL_SVG_HEIGHT = 512
const svgEl = document.createElement('svg')
/* Scale the width and height */
svgEl.setAttribute('width', scaleValue(IDEAL_SVG_WIDTH))
svgEl.setAttribute('height', scaleValue(IDEAL_SVG_WIDTH))
/*
We don't really need to scale the viewBox property because it will
perfectly match the ratio of the scaled width and height
*/
svg.setAttribute('viewBox', `0 0 ${IDEAL_SVG_WIDTH} ${IDEAL_SVG_HEIGHT}`)
此技术的缺点
此解决方案并不完美。例如,一个主要的缺点是 UI **不再可缩放**。无论用户放大多少,设计都会保持锁定,就像在 100% 放大状态下查看一样。
也就是说,我们可以轻松地使用传统的媒体查询,在其中为不同的视口宽度设置不同的理想数值
.myElement {
width: #{scaleValue(500)};
height: #{scaleValue(500)};
box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
font-size: #{scaleValue(24)};
@media (min-width: 64em) {
width: #{scaleValue(800)};
font-size: #{scaleValue(42)};
}
}
现在,我们可以同时受益于媒体查询和我们的像素完美线性缩放。
总结
所有这些都是实现流畅 UI 的另一种方法。我们将像素完美值视为纯数值,并将其乘以当前视口宽度和设计中的“理想”视口宽度之间的差值。
我在自己的工作中广泛使用了这种技术,希望您也能从中找到一些用途。
当你看它并想,“天哪,我为什么没想到呢?!”时,你就知道这是一个很棒的解决方案!
非常感谢,非常有趣的方法。
这项技术的性能如何?这些值是否只评估一次?
它在每次调整大小时都会进行评估,即当
100vw
的数值表示发生变化时。所有计算都卸载到 CSS,因此我认为从性能方面来看它非常快。
有趣!我从未想过以这种方式使用 calc,我认为 scaleValue 函数很优雅。我个人倾向于对所有内容都使用 em 单位,并将 body 的字体大小设置为 VW 单位。我不确定我是否看到了我的方法的缺点……有人可以帮我解释一下吗?
嗨,Koen,我认为使用“流体”单位(如 em/vw)的主要缺点是您不可避免地会失去像素完美。您可以非常接近,但您的布局总会在某些分辨率下发生偏移/重新排列。
此技术试图最大程度地减少此问题并在各种不同分辨率下保留大小。
你的意思是,如果我在根元素上将字体大小设置为1rem,并使其随视口宽度缩放,那么在从设计团队获得的分辨率下,设计将无法缩放成完美的像素版本?我不同意,但你没有告诉我们它是如何“失去像素完美”的,所以我无法反驳。
以上引述将差异描述为除法的结果。
我相信差异是减法的结果,而商是除法的结果。
使用这种方法,文本在小屏幕上往往非常小,难以阅读。我认为它对于图表之类的东西很好,但不适用于整体页面设计。
嗨,Raphael,我们可以将此技术与媒体查询混合使用,并确保我们对较小的分辨率使用更大的值!
我同意。迫使用户全屏使用你的应用程序。在桌面端,如果他们调整屏幕大小,它就会变得无法使用,如视频中所示。
我喜欢这个!
最后,你是说只有在使用媒体查询的情况下它才能“缩放”吗?
非常棒且优雅!
我唯一想补充的是,也许可以添加另一个参数,允许我设置函数返回的最小值,例如,我可能希望一个最小字体大小值。
但这会弄乱
widget
的整体外观,在这种情况下我甚至可能会看到一些文本溢出。如果我们这样操作,有什么建议吗?不错的文章和实现,但我担心的是字体大小和可访问性。我们真的应该缩放字体大小来保持这种像素完美的视觉效果吗?可读性会发生什么?
我发现使用这种方法有很多缺点。
代码变得更难维护。
在更大规模的项目中,css大小会增加。
在较小的屏幕上,应用程序将变得无法使用,例如在移动分辨率下,设计在填充、边距、字体等的尺寸方面有太多变化。因此,你需要为所有这些缩放逻辑编写媒体查询,这有时甚至可能变得更难维护。
尝试将所有这些东西结合起来:编写一个可以在移动设备、平板电脑和桌面设备上使用的、具有JS CSS处理的大规模项目。
我已经使用类似的技术好几个月了,我很高兴这篇文章最终验证了我的工作。
不过,与文章不同的是,我发现使用REM值比使用calc() + SCSS函数更容易。
我使用的技术是这样的
html { font-size: 1.6vw }
p { font-size: 1rem; } /* 16px */
div { width: 3.75rem } /* 60px */
还有一些我还没有解决的难题,而且我最近使用了一种升级版技术,如果视口超过1680px,那么我使用绝对PX值,因为更大的屏幕将等于更大的设计,并且会产生后果。在960-1440之间,我使用缩放,这样我就不必使用多个媒体查询来使布局在所有屏幕尺寸上看起来更美观。
在移动设备和平板电脑上,我也更喜欢使用像素值!
虽然这里的想法很巧妙,但我发现这种方法存在一些问题。
一个问题是它不是真正的像素完美,因为你从浮点计算中损失了精度。在某些视口宽度下,你的计算将产生不精确的值,这些值会被四舍五入/截断,因此页面看起来与其他宽度略有不同。例如,在你链接的Codepen中,如果你将页面缩小到大约600px,一些卡片的标签就开始换行。如果你缓慢调整页面大小,你会注意到在某些视口宽度下,图像会发生变形,并且宽度大于高度。页面在你调整大小的时候会抖动,表明它没有正确地保持比例。
这种方法更明显的问题是你的字体大小小到难以阅读。在360px的移动设备上,你会得到一个3.6px的小基础字体大小。你可以使用媒体查询来解决这个问题,但这会让你一开始使用这种技术的意义变得毫无意义。
相关的是,这忽略了用户的字体大小偏好。在1600px宽的设备上,用户始终会得到16px的基础字体大小,即使他们已将其浏览器配置为具有不同的根字体大小。
相反,我建议将62.5%根字体大小技巧与1.6rem的正文字体大小结合使用。这允许你以像素为单位思考,但以响应式单位进行编码,这些单位尊重用户的字体大小偏好。你甚至可以将其与clamp结合使用以创建流体排版。
我使用的技术是这样的
`html { font-size: 1.6vw }
p { font-size: 1rem; } /* 16px */
div { width: 3.75rem } /* 60px */`
还有一些我还没有解决的难题,而且我最近使用了一种升级版技术,如果视口超过1680px,那么我使用绝对PX值,因为更大的屏幕将等于更大的设计,并且会产生后果。在960-1440之间,我使用缩放,这样我就不必使用多个媒体查询来使布局在所有屏幕尺寸上看起来更美观。
不错!但是如果我的视口没有缩放,而是我的容器缩放了呢?我默认在一个给定的视口中有一个容器div。但是使用JS,我可以切换到另一个两个容器div的视图。并且在这些div中,我想缩放文本,当三个div并排显示时,如果只有一个容器div显示,则文本会更小。视口没有变化,只有容器div的相对大小发生了变化。
我过去也做过这个,但发现它对可访问性不太友好。文本在调整大小时会变小,变得非常难以阅读。需要注意的是,并非每个人都全屏浏览你的网站。
此时,当你引入媒体查询时,这种技术不幸地需要更多工作才能设置,最终没有好处,因为你已经在不同的尺度上定义了尺寸。
在这方面有一些用例,例如应根据视口大小进行响应并相应缩放的隔离组件,但我不会对整个网站这样做。
这很棒,尤其是在你需要纯CSS解决方案的情况下。但如果你愿意使用Javascript,我觉得它过于复杂,而且很容易出错(注意在示例中,有些东西实际上会换行,因为缩放并不完美)。
我做了这个Codepen示例,它应该更简单,并且可能更容易维护和可靠,而没有上面提到的缺点。
https://codepen.io/daledesilva/pen/dyejOMe
希望对某些人有所帮助!