创造 CANDY 主题,只为更好的交互
众所周知,Icarus[1] 是一个非常优秀的 Hexo 主题。它不仅提供清爽、简洁的界面,还与各种主流插件、组件兼容的很好。更令人欣喜的是——它几乎是最活跃的 Hexo 主题之一,有着非常良好的社区氛围。作者时刻保持更新,并提供了非常完善的文档。
种种因素使我最终选择了它,并准备在其基础上修改出自己满意的设计,我将其命名为 Candy,并将在 GitHub 上与 Icarus 保持更新。本文将是这一系列文章的第一节——基础设计修改。
真正的卡片式设计——让最频繁的交互更人性化
分析
Icarus 主题很美观,但对我来说它与许多 Hexo 主题一样都存在一个核心问题,即首页的文章如需访问全文,需要点击 Read More
按钮、图片或标题才可以进入,而点击主文字或卡片空白处是没有反应的,恰好很多博主喜欢把摘要写成长篇大论,此时的 Read More
按钮看起来更小,很容易被忽视,点击大段文字也许是很多读者的第一反应,虽然这并不会得到任何回应。
点击、进入全文,这应该是每个博客最频繁的交互。尴尬的小按钮难免给读者造成一些细微的障碍,当然如果对自己的内容足够自信,或者没那么讲究的话,你就可以跳过这一节了。
尝试
为了达成真正的卡片效果,我探索过一些方法。
我的第一反应是在 <article>
标签外套一层 <a>
标签然后设置 href
属性,同时 <article>
要设置成块级元素。于是我便在 layout/common/article.jsx
中这样修改:
<a class="card-link" href={index ? url_for(page.link || page.path):null}>
<article class={`card-content article${'direction' in page ? ' ' + page.direction : ''}`} role="article">
...
</article>
</a>
感觉非常合理,但其实并不行,如果这样操作,在实际页面中,<a>
标签并不会正确包裹,反而会被添加到 <article>
元素下每个子元素的首位。我并不确定是什么原因,但是根据 Stack Overflow 上的一篇回答中的评论:
You should really point out (by suggesting this way) that: using this solution you cannot have other Anchor elements inside the article.Roko C. Buljan Jul 14 '16 at 6:51
貌似如果想这样操作的话,子元素不能再有其他 <a>
元素。我便以为是不可以的,当时就作罢了。
接下来,我尝试在外面先套一层 <object>
标签,再套 <a>
标签链接到正文,这样我们就实现了整张卡片可点击的效果。
<a class="card-link" href={index ? url_for(page.link || page.path) : null}>
<object class="card-object">
<article class={`card-content article${'direction' in page ? ' ' + page.direction : ''}`} role="article">
...
</article>
</object>
</a>
这样确实可以了,本来这一段到这里就结束了,但我想起了文档里对 <object>
标签的描述:
HTML
<object>
元素(或者称作 HTML 嵌入对象元素)表示引入一个外部资源,这个资源可能是一张图片,一个嵌入的浏览上下文,亦或是一个插件所使用的资源。MDN
啊这,有一丝隐隐的不安。迅速查了下 SEO,发现目前 Google 是会索引 object
元素的,但心里就是不舒服,毕竟不太符合语义。
于是乎,就换来了接下来的方法,也是最终的实现方法。
解法
这种方法的思路其实和第一种是一致的,但是具体实现有区别,这次我们不在 article.jsx
里修改了,直接在 source/js/main.js
最底下添加:
if ($('.article-licensing.box').length === 0){
$(".card-content.article").each(function(){
$(this).wrap('<a class="card-link" href="' + $(this).find('h1 a').attr('href') + '"></a>');
})}
其实就是用了 jQuery 的 warp()
方法[2],这里我们指定 <a>
元素来包裹 <article>
元素。同时因为我们只需要在首页让这些卡片有链接,所以我们可以通过判断正文末尾的版权信息盒子是否存在,来控制这条语句是否生效。
我们还需要设置一下 card-link
的 color
为 inherit
,这样摘要的颜色就会恢复成白色,否则会是你 a
标签的颜色(自定义 CSS 的方法在后文):
.card-link
color: inherit
这样我们就实现了整个卡片可点击,且卡片里原有的分类链接照样可以点击并不受影响。但我并不知道为何第一种和第三种方法会有这样的差异,如果有大佬清楚请在评论区解答我,谢谢。
加入一点动效会让体验更好吗?答案是肯定的
所谓「交互」,代表着交流和互动。一个好的交互设计,组件与用户的互动是必不可少的。互联网厂商们早已发现了这一逻辑,并将这些互动悄悄藏在设计里,通过微小的震动、令人愉悦的变换俘获用户的感官。
简单来说,光将整张卡片链接到正文还不够,它得是让用户有感知的,这个感知就由动效来实现。
废话少说,先上效果图👇:
为实现这个效果,我增加了:
向上的位移
阴影的变换
题图的变大(深色模式下,还增加了题图的变暗)
我希望能用纯 CSS 解决的,尽量只用 CSS。为了后期与 Icarus 同步更方便,我们修改 source/css
目录下的 style.styl
来自定义样式。Icarus 使用 Stylus[3] 作为 CSS 预处理器。它的定义生效规则是:
一个变量不能影响在定义它以前的输出样式。[4]
所以为了方便,我们可以把所有自定义样式放在最前面,把 @import
放在最后。
位移与阴影
首先,我们将位移与阴影一起添加,效果模仿自 Gridsome Blog Starter 🙏。
因为我不希望在正文处也出现动效,同时希望尽可能用纯 CSS 解决,所以我在 article.jsx
最前面添加一个只在正文生效的空的 div
元素,并通过 class 名「controller」来控制:
return <Fragment>
// 通过判断 index 是否存在,来决定是否是正文。
{!index ? <div class={'controller'}></div>: null}
...
</Fragment>
这样一来,正文的最前面就会多一个空的、class 名为「controller」的 div
元素。接下来我们写伪类:
.order-2 .card:hover
transform: translateY(-5px)
box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)
-webkit-transform: translateY(-5px)
-webkit-box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)
// 将兄弟元素为 .controller 的 .card 的 :hover 样式重制(无效化)
.order-2 .controller ~ .card:hover
transform: none
box-shadow: 0 4px 10px rgba(0,0,0,0.05), 0 0 1px rgba(0,0,0,0.1)
-webkit-transform: none
-webkit-box-shadow: 0 4px 10px rgba(0,0,0,0.05), 0 0 1px rgba(0,0,0,0.1)
// 加入过渡效果
.card
transition: opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.6s !important
可以看出来,这样写会有一些啰嗦和冗余,我尝试过一些想法都不太好,比如将 index
判断反写,把「controller」加在首页最上方,但 Icarus 原版的 <Fragment>
元素写法,会在每个卡片上加上一个 div
元素,从而打乱布局,让菜菜的我觉得麻烦,所以先就这样,日后再改。
如果有大佬们有更好的解决方案,请在评论区赐教。
点击展开查看:一个现在不兼容,但未来可能可以的做法。
菜菜的我本来很辛苦的摸索了出来这种写法,结果发现目前只有 Safari 和最新版本的 Firefox 可以比较好的支持,这取决于浏览器对 CSS Selector Level 4 的支持程度(Safari 91% 支持,而 Chrome 只有 88%,这个特性恰好在这不支持的 3% 里)。
简单来说就是 :not
里嵌套兄弟选192择器 ~
:
.order-2 > :not(.controller ~ .card):hover
transform: translateY(-5px)
box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)
-webkit-transform: translateY(-5px)
-webkit-box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)
很好理解,这个选择器会选择兄弟不为 .controller
的 .card
,这样只需要这一个语句即可。
关于这一特性的详细可以看 W3C Editor’s Draft 和 CSS4 Selectors,另附 Can I use 兼容情况。
题图
最后我们添加题图放大的动效,这里的效果模仿自 Apple 官网的 Newsroom 📰,注意 :hover
的写法,我希望鼠标放在卡片上时就激活(而不仅仅是放在题图上):
.card:hover
.card-image a
-webkit-transform: scale(1.03)
transform: scale(1.03)
// 加入过渡效果
.card .card-image a
-webkit-transition: opacity 1s cubic-bezier(0.4, 0, 0.25, 1),
-webkit-transform 400ms cubic-bezier(0.4, 0, 0.25, 1),200ms -webkit-filter linear
transition: opacity 1s cubic-bezier(0.4, 0, 0.25, 1),
transform 400ms cubic-bezier(0.4, 0, 0.25, 1),200ms filter linear
细心的朋友可能会发现,光这样还不完美——图片在变换的过程中会先取消圆角,再恢复圆角,也就是说中间会有一段时间「四角方方」,这是我们不愿意看到的,解决方法也很简单,再添加一个 0 度的旋转[5]即可:
// 修复变换时的圆角
.card-image
transform: rotate(0deg)
-webkit-transform: rotate(0deg)
在黑暗模式下我还加入了题图变暗的效果。关于黑暗模式我会在之后详解,就不在本文中赘述了:
-webkit-filter: brightness(70%)
filter:brightness(70%)
至此,我们便完成了一个用户友好的卡片设计,它整张卡片可点击,且有合适的动效互动,但这并不是 Candy 主题的全部,还有很多细节性的动效就不在这里赘述了,感兴趣的话可以去查看 style.styl
源码。
「跟着走」的导航栏,贴心而不恼人
最后,我想我需要一个会跟随页面的导航栏,它得是贴心的——当你需要它的时候,比如回首页、切换浅色/深色模式、搜索,它就在那里;它还得是不恼人的——它不应该遮挡太多,影响到正文或视觉平衡,它得是灵巧的,且更好的适配响应式设计。
跟着走?No!它只是固定在那里
导航栏不会真的跟着走的,我们只是给它添加一个 position
为 fixed
的样式。这么做的同时还需要调整 width
为 100%
并将下一个元素 section
下移:
.navbar-main
position: fixed !important
width: 100% !important
top: 0px
.section
margin-top: 45px
当然,也可以采用 sticky
不过兼容性会差一些,在此案例中效果是一样的。使用 sticky
就不需要设置 section
的下移,不过需要设置 top
属性:
.navbar-main
position: sticky !important
position: -webkit-sticky !important
top: 0px
「瘦身」+「毛玻璃」,让导航栏灵巧一些
接下来我想让导航栏看起来更轻巧一些,于是缩窄了导航栏的高度并加入了所谓「毛玻璃」效果。
「瘦身」
通过修改 navbar-item
的内外边距来缩窄高度:
.navbar-item
margin: 4px !important
padding: 8.5px !important
同时 Icarus 原版对于移动端的处理是将 logo 放在一个自适应的 div
里,当屏幕宽度小于 1088 px 的时候,导航栏会分成上下两行。
但这对于一个一直固定在顶端的导航栏来说太大了,所以这里我做了个修改,将自适应去掉,把 logo 也放在 navbar-menu
里:
- <div class="navbar-brand justify-content-center">
- <a class="navbar-item navbar-logo" href={siteUrl}>{navbarLogo}</a>
- </div>
<div class="navbar-menu">
+ <a class="navbar-item navbar-logo" href={siteUrl}>{navbarLogo}</a>
...
</div>
效果图:
当然,还有些更好的方案,比如制作下拉菜单栏,以解决导航栏里的标签页太多超出屏幕宽度的问题,可以见这个旧版本的 PR。有空的时候我会改进导航栏,如果真的有人看的话。
「毛玻璃」
所谓「毛玻璃」,更专业一点的说法叫「Backdrop Filter effect」,即背景过滤效果。所以核心就是 backdrop-filter
样式。我这里依旧模仿的 Apple Newsroom,将 navbar
背景色变透明并增加饱和度和模糊:
.navbar
-webkit-backdrop-filter: saturate(180%) blur(20px)
backdrop-filter: saturate(180%) blur(20px)
background-color: rgba(255,255,255,0.7) !important
//黑暗模式下颜色为 rgba(29,29,31,0.7)
再将 navbar-menu
背景设为透明,这样小尺寸屏幕下也能正常显示:
.navbar-main .navbar-menu
background-color: transparent
效果图:
最后,修复锚元素定位
我们固定了导航栏,紧接着问题就来了。你会发现之前的页内锚元素跳转全都错位了(比如目录、脚注),更准确的来说是正好被现在的导航栏所遮挡,所以我们要来修复这个 bug。
脚注修复
先来修复简单的,修复脚注我们只需要用到 CSS(因为新版目录是用 JavaScript 来控制 href
属性值的,这个方法就不行了),这里我们用到一个小技巧,即用 :target
去定位目标元素,并用 ::before
在前面加一个空的行内元素,然后通过 padding-top
和 margin-top
配合来控制位置这样就修复了正文到脚注的跳转错位:
:target::before
content: ""
display: inline-block
padding-top: 73px
margin-top: -73px
稍微解释一下,这里就是先用 padding-top
将这个空的 inline-block
移动到距离其容器上方 N px 的位置,然后通过 margin-top
将这个容器整体上移 N px 的位置,这样我们就可以通过调整 N 的值来控制元素的位置。
同理,我们还需要修复下脚注到正文的跳转:
.footnote-item::before
display: block
padding-top: 55px
margin-top: -55px
margin-bottom: -25px
这里有一些细微的差别,因为脚注是一个列表,列表的元素应该按一整条一整条来看,所以我们把 display
改为 block
,再按照之前的方法适当调整一下距离,最后我们还需要通过 margin-bottom
来对齐文本和序号。
目录修复
如果你不需要一个「随着滚动到不同位置,动态折叠展开」的目录,你可以使用这个旧版本的 toc.js
,并且采用上面修复脚注的方式来修复错位,这里我就不赘述了。
如果你需要这样一个「智能」的目录,请采用最新版本的 toc.js
并把它放在 source/js
里,这里我只说下对于错位的修复。
首先在 source/js/toc.js
中创建一个 scrollTo
方法来代替不支持偏移量的 scrollIntoView
方法:
function scrollTo(id) {
var element = document.getElementById(id);
var headerOffset = -20; // 偏移量
var elementPosition = element.offsetTop;
var offsetPosition = elementPosition - headerOffset;
document.documentElement.scrollTop = offsetPosition;
document.body.scrollTop = offsetPosition; // 适配 Safari
}
没有什么可说的,非常好理解,然后在下面替换即可:
if (typeof $heading.scrollTo === 'function') {
- $heading.scrollIntoView({ behavior: 'smooth' });
+ scrollTo($heading.id);
}
就是这么简单,我们完成了所有错位修复。
写在最后的、其他的一些玩意儿
感谢你看到这里,零基础的我可能比较啰嗦,这当然不是我在视觉和交互上做出的全部修改,尽量挑了些对我来说比较有代表性的「坑」,大佬们见笑了。一些细枝末节的修改,网络上可能已经有无数篇相同的文章教你如何去操作了,我就不费口舌了。当然如果你有任何疑问、评价或想教我做事儿,也请不要吝啬,请在文末的评论区给我留言,我会第一时间回复。
这是 Candy 主题修改系列的第一篇,应该也是最零碎的一篇,我非常迫不及待地想分享关于黑暗模式的实现,但是先忍住,下一篇将会是关于 Twikoo 的,一个非常崭新的评论系统,美观、安全、易用、免登录、免费等等,虽然它还有很长的路要走,但目前配得上这些美好的词汇。希望我能在官方作者写出详细教程之前发布(逃)。
本文完。
原创封面图,使用需授权,请勿盗用脚注
.wrap( wrappingElement ): Wrap an HTML structure around each element in the set of matched elements. ↩︎
创造 CANDY 主题,只为更好的交互