← All entries ~/design/highlighter-button
Copy Copy agent prompt Requirements, the notes and the full source: paste it into Claude Code, Codex or Cursor. Copy source index.html Copy demo link /design/demos/highlighter-button/
What makes it work
The stroke is a background, not a pseudo-element : the link’s background is linear-gradient(transparent 52%, yellow 52%, yellow 90%, transparent 90%), covering only the lower half of the line height, like a highlighter dragged low; background-size transitions from 0% 100% to 100% 100%, which paints left to right.
Wrapped lines work too : box-decoration-break: clone gives each line of a wrapped link its own stroke, instead of one block that starts on the first line and ends on the last.
Slanted tip : the stroke on buttons is a ::before, with skewX(-18deg) cutting both ends at an angle and rotate(-2deg) making it less straight; scaleX goes from 0 to 1.
Direction of the wipe : on leave, transform-origin is switched to the right, so retracting looks like wiping off to the right rather than shrinking back the way it came.
Marking emphasis : the third kind is toggleable, keeping the yellow stroke when aria-pressed="true", suitable for states like “read” or “saved”.
How to use it
Body links: copy .hl and add the class name to the <a>; change the color with --c.
Buttons: copy .mk with its ::before and :hover rules; the button background must be transparent so the yellow bar shows up under the text (isolation: isolate + z-index: -1 already handle the layering).
In dark mode, swap the highlighter yellow for a darker saturated color (#7d6416 in the example), so light text on top stays readable.
Pitfalls
The background-size transition may drop frames in very old versions of Safari, which is acceptable.
Links have no underline by default; remember to leave a marker that makes them recognizable as links at rest (the example uses a dashed line), otherwise people only learn they are clickable when hovering.
Touch screens have no hover: a tapped link navigates away so the animation isn’t seen, which is fine; for buttons the same effect can be attached to :active.
Source MIT licensed. Take it. index.html
<! doctype html >
< html lang = "zh-CN" >
< head >
< meta charset = "utf-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1" />
< title >荧光笔划过</ title >
< style >
:root {
--paper : #fbf8f1 ;
--card : #fffdf8 ;
--ink : #1c2230 ;
--ink-2 : #4a5163 ;
--ink-3 : #636877 ;
--line : #e6dfd0 ;
--dot : #d8d0bf ;
--blue : #2f5bd3 ;
--seal : #c0391f ;
--hi : #ffe066 ; /* highlighter yellow */
--hi-pink : #ffb3c7 ;
--hi-green : #b6f0a8 ;
--ease : cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
--f-sans : system-ui , -apple-system , "PingFang SC" , "Microsoft YaHei" , sans-serif ;
--f-mono : ui-monospace , "SF Mono" , Menlo, Consolas, monospace ;
color-scheme : light ;
}
@media (prefers-color-scheme: dark) {
:root {
--paper : #14161b ;
--card : #1e2129 ;
--ink : #ece7dc ;
--ink-2 : #bab6ac ;
--ink-3 : #959aa8 ;
--line : #31353f ;
--dot : #2c3039 ;
--blue : #8eaaff ;
--seal : #f0694e ;
/* On dark paper the ink is light, so the marker turns into a dim, saturated stripe. */
--hi : #7d6416 ;
--hi-pink : #7a2e44 ;
--hi-green : #2f5f2a ;
color-scheme : dark ;
}
}
* { box-sizing : border-box ; }
body {
margin : 0 ;
min-height : 100 vh ;
display : grid ;
place-items : center ;
padding : 32 px 16 px ;
background : var ( --paper ) radial-gradient ( var ( --dot ) 1 px , transparent 1.2 px ) 0 0 / 22 px 22 px ;
color : var ( --ink );
font : 16 px / 1.8 var ( --f-sans );
}
.sheet {
width : min ( 560 px , 100 % );
padding : 28 px ;
background : var ( --card );
border : 1 px solid var ( --line );
border-radius : 14 px ;
box-shadow : 0 10 px 28 px -14 px rgba ( 28 , 34 , 48 , 0.28 );
}
.label { margin : 0 0 10 px ; font : 500 11 px / 1 var ( --f-mono ); letter-spacing : 0.12 em ; color : var ( --ink-3 ); }
.label + * { margin-top : 0 ; }
section + section { margin-top : 26 px ; }
/* ---------- 1. a link the marker sweeps across ----------
The stroke is a background that grows from 0 to 100% wide. The gradient stops leave a band
in the lower part of the line, like a highlighter dragged a bit low; box-decoration-break
makes each wrapped line get its own stroke. */
.hl {
--c : var ( --hi );
color : inherit ;
text-decoration : none ;
background : linear-gradient ( transparent 52 % , var ( --c ) 52 % , var ( --c ) 90 % , transparent 90 % ) no-repeat 0 0 / 0 % 100 % ;
-webkit-box-decoration-break : clone ;
box-decoration-break : clone ;
padding : 0 2 px ;
transition : background-size 0.45 s var ( --ease );
border-bottom : 1 px dashed var ( --ink-3 );
}
.hl:hover ,
.hl:focus-visible { background-size : 100 % 100 % ; border-bottom-color : transparent ; outline : none ; }
.hl.pink { --c : var ( --hi-pink ); }
.hl.green { --c : var ( --hi-green ); }
/* ---------- 2. a button with a marker stroke behind its label ----------
The stroke is a pseudo-element: skewed so its ends are cut at an angle like a chisel tip,
scaled from the left edge, and a little rotated so it is never perfectly straight. */
.mk {
position : relative ;
isolation : isolate ;
padding : 10 px 18 px ;
border : 1.5 px solid var ( --ink );
border-radius : 999 px ;
background : transparent ;
color : var ( --ink );
font : 600 15 px / 1 var ( --f-sans );
cursor : pointer ;
transition : scale 0.2 s ;
}
.mk::before {
content : "" ;
position : absolute ;
z-index : -1 ;
left : 10 px ;
right : 10 px ;
top : 52 % ;
height : 42 % ;
background : var ( --hi );
border-radius : 2 px ;
transform-origin : 0 50 % ;
transform : rotate ( -2 deg ) skewX ( -18 deg ) scaleX ( 0 );
transition : transform 0.4 s var ( --ease );
}
.mk:hover::before ,
.mk:focus-visible::before { transform : rotate ( -2 deg ) skewX ( -18 deg ) scaleX ( 1 ); }
.mk:focus-visible { outline : 2 px solid var ( --blue ); outline-offset : 3 px ; }
.mk:active { scale : 0.95 ; }
/* Leaving the button, the stroke wipes away to the right instead of shrinking back. */
.mk:not ( :hover ) :not ( :focus-visible ) ::before { transform-origin : 100 % 50 % ; }
/* ---------- 3. "already read" lines that stay highlighted ----------
Toggling aria-pressed draws the stroke; it stays, like marking up a printout. */
.pick {
all : unset ;
cursor : pointer ;
--c : var ( --hi );
background : linear-gradient ( 104 deg , transparent 0.6 em , var ( --c ) 0.6 em calc ( 100 % - 0.4 em ), transparent calc ( 100 % - 0.4 em )) no-repeat 0 62 % / 0 % 58 % ;
-webkit-box-decoration-break : clone ;
box-decoration-break : clone ;
transition : background-size 0.5 s var ( --ease );
border-radius : 3 px ;
margin : 0 -0.6 em 0 -0.6 em ;
padding : 0 0.4 em 0 0.6 em ;
}
.pick [ aria-pressed = "true" ] { background-size : 100 % 58 % ; }
.pick:focus-visible { outline : 2 px dashed var ( --blue ); outline-offset : 2 px ; }
p .quote { color : var ( --ink-2 ); margin : 0 ; }
.hint { margin : 8 px 0 0 ; font-size : 12 px ; color : var ( --ink-3 ); }
@media (prefers-reduced-motion: reduce) {
.hl , .mk::before , .pick { transition : none ; }
}
</ style >
</ head >
< body >
< main class = "sheet" >
< section >
< p class = "label" >01 · 链接</ p >
< p >
正文里的链接平时只有一条虚线,指上去才被荧光笔< a class = "hl" href = "#" >从左往右划一道</ a >;换个颜色也行:< a class = "hl pink" href = "#" >粉色</ a >、< a class = "hl green" href = "#" >绿色</ a >。跨行的长链接< a class = "hl" href = "#" >每一行都会单独划到,不会被拉成一整块奇怪的色带</ a >。
</ p >
</ section >
< section >
< p class = "label" >02 · 按钮</ p >
< div style = "display: flex; flex-wrap: wrap; gap: 12px" >
< button class = "mk" type = "button" >读全文 →</ button >
< button class = "mk" type = "button" >订阅更新</ button >
</ div >
< p class = "hint" >笔头是斜的,进来从左往右涂,离开往右边擦掉。</ p >
</ section >
< section >
< p class = "label" >03 · 点一下标重点</ p >
< p class = "quote" >
< button class = "pick" type = "button" aria-pressed = "false" >先给结论,再给依据。</ button >
读者没时间的时候,只看标黄的那几句也能明白你要说什么。
< button class = "pick" type = "button" aria-pressed = "true" >数字放进图里,别堆在段落里。</ button >
</ p >
</ section >
</ main >
< script >
for ( const b of document. querySelectorAll ( ".pick" )) {
b. addEventListener ( "click" , () => b. setAttribute ( "aria-pressed" , String (b. getAttribute ( "aria-pressed" ) !== "true" )));
}
for ( const a of document. querySelectorAll ( 'a[href="#"]' )) a. addEventListener ( "click" , ( e ) => e. preventDefault ());
</ script >
</ body >
</ html >