A modern re-styling of the Hacker News front page, including automatic light/dark mode.
-
-
Save christippett/5097af0ea59c867c4578996350933776 to your computer and use it in GitHub Desktop.
/* ==UserStyle== | |
@name Hacker's New CSS | |
@author Chris Tippett <[email protected]> (https://christippett.dev) | |
@description A modern retake on the classic Hacker News design. | |
@version 2024.08.12 | |
@namespace @christippett | |
@homepageURL https://github.com/christippett | |
@supportURL https://gist.github.com/christippett/5097af0ea59c867c4578996350933776/ | |
@updateURL https://gist.github.com/christippett/5097af0ea59c867c4578996350933776/raw/hn.user.css | |
@license MIT | |
@preprocessor uso | |
==/UserStyle== */ | |
@-moz-document domain("news.ycombinator.com") { | |
:root { | |
color-scheme: light dark; | |
} | |
html { | |
--font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", | |
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, | |
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", | |
"Noto Color Emoji"; | |
--primary: #ff6600; | |
--primary-light: rgb(246, 125, 50); | |
--primary-alt: #c8c5c5; | |
--text-color: #4b4b4b; | |
--text-color-alt: #828282; | |
--table-bg: #fcfffa; | |
--body-bg: #f6f6ef; | |
--border-color: #f6f6ef; | |
--border-radius: 3px; | |
--smaller: 0.85em; | |
--content: 0.96rem; | |
--magic-num: 0.4rem; | |
font-size: 14px; | |
color: var(--text-color); | |
} | |
@media (prefers-color-scheme: dark) { | |
html { | |
--text-color: #f0f0f0; | |
--text-color-alt: #c3c3c3; | |
--primary-alt: #828282; | |
--border-color: #1b1b1c; | |
--table-bg: #232323; | |
--body-bg: #1b1b1c; | |
} | |
} | |
* { | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
-webkit-border-horizontal-spacing: 0; | |
-webkit-border-vertical-spacing: 0; | |
} | |
body { | |
padding: 20px 40px 50px; | |
background-color: var(--body-bg); | |
} | |
body, | |
td, | |
input[type="submit"], | |
.default, | |
.admin, | |
.title, | |
.subtext, | |
.yclinks, | |
.pagetop, | |
.comhead, | |
.comment { | |
font-family: var(--font-family); | |
} | |
code { | |
color: var(--primary-light); | |
} | |
@media (max-width: 750px) { | |
body { | |
padding: 0; | |
} | |
} | |
body>center { | |
position: relative; | |
} | |
table { | |
width: 100%; | |
} | |
td, | |
td.default, | |
.admin td { | |
font-size: var(--content); | |
color: var(--text-color); | |
line-height: 1.4; | |
} | |
p, | |
.default p, | |
.admin p { | |
margin-top: var(--smaller); | |
margin-bottom: var(--smaller); | |
} | |
br { | |
padding: 0; | |
} | |
/* hide consecutive <br> tags */ | |
br+br, | |
span+br:first-of-type { | |
display: none; | |
} | |
font[size] { | |
font-size: var(--smaller); | |
} | |
/* help text on profile & post submission pages */ | |
font[size="2"], | |
html[op="submit"] form>table tr:last-child td { | |
font-size: var(--smaller); | |
color: var(--text-color-alt); | |
font-style: italic; | |
} | |
a, | |
a:link, | |
.c00, | |
.c00 a:link { | |
color: var(--text-color); | |
text-underline-offset: 0.12em; | |
text-decoration-thickness: 1px; | |
} | |
form { | |
padding: 0; | |
max-width: 700px; | |
} | |
input[type="submit"] { | |
margin-top: var(--magic-num); | |
} | |
input[name="q"] { | |
background: var(--table-bg); | |
} | |
textarea, | |
input, | |
a.morelink { | |
max-width: 90%; | |
color: var(--text-color); | |
font-size: max(var(--smaller), calc(var(--magic-num) * 2)); | |
background-color: var(--body-bg); | |
outline: 1px solid var(--border-color); | |
border: none; | |
border-radius: var(--border-radius); | |
box-shadow: | |
inset 0 0 1px var(--text-color), | |
2px 2px 4px var(--border-color); | |
padding: calc(var(--smaller) / 2); | |
} | |
textarea+a { | |
padding: var(--magic-num); | |
} | |
/* button style */ | |
input[type="submit"], | |
select, | |
a.morelink, | |
.reply a { | |
cursor: pointer; | |
text-transform: capitalize; | |
} | |
.reply a:link { | |
color: var(--text-color-alt); | |
background-color: color-mix(in srgb, var(--body-bg) 50%, rgba(0, 0, 0, 0)); | |
border-radius: var(--border-radius); | |
font-size: 0.85em; | |
padding: 0.3em 0.5em; | |
transition: background-color 120ms ease; | |
&:before { | |
content: "+ "; | |
display: inline; | |
} | |
&:hover { | |
background-color: var(--body-bg); | |
} | |
} | |
td[style="overflow:hidden"] { | |
overflow: visible !important; | |
} | |
/* --- LOGIN / CREATE ACCOUNT --*/ | |
html:not([op]) body { | |
background-color: var(--table-bg); | |
} | |
html:not([op]) b { | |
display: inline-block; | |
margin-top: 1rem; | |
} | |
a[href="forgot"] { | |
display: inline-block; | |
margin-top: -1rem; | |
padding: calc(var(--magic-num) * 2); | |
color: var(--text-color-alt) !important; | |
font-style: italic; | |
text-decoration: none; | |
font-size: var(--smaller); | |
} | |
/* --- USER PROFILE --- */ | |
#pagespace+tr>td:empty { | |
display: none; | |
} | |
table td:first-child:not([class])[valign="top"]+td { | |
padding: 0.25rem; | |
width: 100%; | |
} | |
/* field labels */ | |
table td:first-child:not([class])[valign="top"] { | |
width: auto; | |
min-width: 120px; | |
padding-right: 1rem; | |
font-size: 0.75rem; | |
font-weight: 500; | |
text-transform: uppercase; | |
} | |
/* --- TABLE / LAYOUT --- */ | |
#hnmain { | |
max-width: 900px; | |
overflow: hidden; | |
background: var(--table-bg); | |
border-radius: var(--border-radius); | |
box-shadow: 0 0 1px var(--primary-alt); | |
} | |
/* only add padding for parent table cells */ | |
#hnmain>tbody>tr:not([id])>td:not([bgcolor]) { | |
padding: 1rem 1.25rem; | |
width: 100%; | |
} | |
/* add left/right padding when viewing user's threads */ | |
#hnmain>tbody>tr.athing.comtr[id]>td { | |
padding: 0 1.25rem; | |
} | |
#hnmain tr:not(.spacer):empty { | |
display: none; | |
} | |
.morespace { | |
display: none; | |
} | |
.morespace+tr td { | |
padding: var(--magic-num) 0; | |
} | |
.spacer { | |
height: 1rem !important; | |
} | |
/* error message shown if not logged in when making comment */ | |
html[op="x"] #hnmain>tbody>tr:last-child td { | |
color: transparent; | |
} | |
/* --- HEADER --- */ | |
td[bgcolor] { | |
padding: var(--magic-num); | |
} | |
#hnmain>tbody>tr:first-child>td:first-child>table td { | |
height: auto !important; | |
line-height: 1 !important; | |
vertical-align: middle; | |
} | |
.pagetop { | |
color: white; | |
line-height: 1; | |
font-weight: 500; | |
font-size: var(--smaller); | |
padding: 0 var(--magic-num); | |
} | |
.pagetop font { | |
font-weight: bold; | |
} | |
.pagetop :is(a, a:visited) { | |
color: white; | |
line-height: 1rem; | |
} | |
.pagetop a:hover { | |
text-decoration: underline; | |
text-decoration-color: white; | |
} | |
/* 'hacker news' title */ | |
.hnname { | |
margin-right: var(--magic-num); | |
font-size: 1.1rem; | |
} | |
.hnname a:hover { | |
text-decoration: none !important; | |
} | |
#me { | |
font-weight: 600; | |
font-style: italic; | |
} | |
#karma { | |
font-size: var(--smaller); | |
line-height: 1rem; | |
display: inline-flex; | |
align-items: center; | |
cursor: grabbing; | |
} | |
/* can't remember what this was supposed to hide */ | |
span:not([class])[id]:not(#karma) { | |
display: none; | |
} | |
/* ---- FRONT PAGE ---- */ | |
/* stories from [Month] [Day], [Year] */ | |
#pagespace+tr>td>div { | |
padding: 0 0 1rem; | |
margin: 0 0 1rem !important; | |
border-bottom: 1px solid var(--primary-alt); | |
font-size: var(--content); | |
font-weight: 500; | |
} | |
#pagespace+tr>td>div>div { | |
font-style: italic; | |
font-size: var(--smaller); | |
margin-top: 0.25rem !important; | |
} | |
.hnmore a, | |
.hnmore a:is(:link, :visited) { | |
color: var(--text-color-alt); | |
} | |
/* --- ARTICLE --- */ | |
.rank { | |
color: var(--text-color-alt); | |
font-weight: 500; | |
} | |
.title { | |
font-size: 1rem; | |
overflow: unset; | |
} | |
.titleline { | |
width: 100%; | |
font-weight: 500; | |
display: flex; | |
gap: 1rem; | |
justify-content: flex-start; | |
align-items: baseline; | |
} | |
html[op="item"] .titleline>a { | |
font-size: 1.2rem; | |
font-weight: 700; | |
} | |
/* transparent parantheses around url */ | |
.titleline .sitebit { | |
flex: 1; | |
text-align: right; | |
color: transparent; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
display: block; | |
padding: 0 var(--magic-num); | |
} | |
.titleline .sitebit a { | |
font-weight: 300; | |
pointer-events: auto; | |
transition: color 220ms ease; | |
&:not(:active):hover { | |
color: var(--text-color); | |
} | |
} | |
.titleline:hover .sitebit a { | |
color: var(--text-color); | |
} | |
.subtext, | |
.subtext a { | |
display: inline-block; | |
color: var(--text-color-alt); | |
font-size: 0.9rem; | |
padding: 2px 0; | |
} | |
.subtext { | |
cursor: default; | |
} | |
.subtext .score { | |
color: var(--text-color-alt); | |
font-weight: 500; | |
border-radius: var(--border-radius); | |
} | |
/* ---- COMMENTS ---- */ | |
table.itemlist, | |
table.fatitem, | |
table.comment-tree { | |
position: relative; | |
} | |
html:not([op="reply"]) table.fatitem { | |
padding-bottom: 0.75rem; | |
margin-bottom: 0.75rem; | |
border-bottom: 1px solid var(--primary-alt); | |
} | |
td.default { | |
width: 100%; | |
position: relative; | |
} | |
.toptext { | |
font-size: var(--content); | |
margin-bottom: -1rem; | |
} | |
/* comment: header/author/etc */ | |
.comhead { | |
font-size: var(--smaller); | |
color: var(--text-color-alt); | |
} | |
a.hnuser { | |
font-weight: 500; | |
} | |
.comment { | |
margin: 0 0 var(--magic-num); | |
padding: var(--magic-num); | |
font-size: inherit; | |
} | |
.reply p { | |
margin-top: var(--smaller); | |
margin-bottom: var(--smaller); | |
} | |
.reply :is(u, a), | |
.reply a:is(:link, :visited) { | |
text-decoration: none; | |
} | |
.navs { | |
margin-left: 0.25ch; | |
display: inline-flex; | |
align-items: center; | |
gap: 0.75ch; | |
} | |
a.togg { | |
padding: 0.1em 0.2em; | |
font-weight: bold; | |
border-radius: var(--border-radius); | |
} | |
/* --- UPVOTE/DOWNVOTE LINKS --- */ | |
.votelinks center:not(:has(font)) { | |
opacity: 0; | |
margin-top: 2px; | |
transition: opacity 120ms ease; | |
} | |
tr.athing:hover .votelinks center:has(a.clicky:not(.nosee)) { | |
opacity: 1; | |
} | |
/* self-post indicator */ | |
.votelinks center font { | |
width: 1em; | |
display: block; | |
color: transparent; | |
position: relative; | |
} | |
.votelinks center font:before { | |
content: "β "; | |
color: var(--primary); | |
display: inline; | |
font-size: 0.7em; | |
position: absolute; | |
} | |
/* space between author/links and coment text */ | |
.votelinks>center> :is(font+br, br+img) { | |
display: none; | |
} | |
.admin td { | |
padding: 0 1rem; | |
} | |
.admin td>p:before, | |
.admin td:before { | |
content: "β "; | |
font-size: 1.2em; | |
margin-right: 0.25em; | |
} | |
/* ---- FOOTER ---- */ | |
#hnmain>tbody>tr:last-of-type:has(img) { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
display: flex; | |
justify-content: center; | |
transform: translateY(100%); | |
z-index: 99; | |
padding: 1rem 0; | |
} | |
/* footer: hide orange horizontal bar above footer */ | |
img[src="s.gif"]+table { | |
display: none; | |
} | |
.yclinks { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: var(--magic-num); | |
color: var(--text-color-alt); | |
margin: var(--magic-num) 0; | |
padding: var(--magic-num) 1rem; | |
width: max-content; | |
border-radius: calc(var(--border-radius) * 2); | |
} | |
.yclinks a:link { | |
color: var(--text-color-alt); | |
font-size: 0.9rem; | |
} | |
.yclinks a:hover { | |
text-decoration: underline; | |
text-underline-offset: 2px; | |
text-decoration-thickness: 1px; | |
text-decoration-color: var(--primary); | |
} | |
form[action="//hn.algolia.com/"] { | |
color: var(--text-color-alt); | |
font-size: var(--smaller); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 1ch; | |
} | |
/* yc application notice */ | |
center>a[href$="ycombinator.com/apply/"] { | |
color: var(--text-color-alt); | |
text-decoration: underline; | |
text-underline-offset: 2px; | |
font-weight: 500; | |
} | |
} |
The challenge is making sure that all possible states are accounted for. You'd probably want to test against static HTML to be as deterministic as possible, but the risk is you miss some minute change to the live site that messes everything up. Not forgetting that different users have varying levels of functionality available to them depending on their karma.
And then you start wondering if it's all a big over-engineered footgun and the easiest option is to just wait until someone leaves a comment here complaining about something not working π
PS. @pmarreck I see you're into EDM. I'm about to make my way to go see LCD Soundsystem π€
@christippett DUDE I AM SO JEALOUS. James Murphy is the MAN! Truly a creative inspiration. Ha, enjoy!!
Ever see this? https://www.youtube.com/watch?v=Zj9Sv1JpmPs
I just left this comment on it: "If 'edging' was a song" π
I'm going to play around with the upvote buttons again.. I miss the visual feedback of which posts/comments I've upvoted (the default hides the upvote arrows once voted). I've also unlocked downvote functionality for my account (501 karma), so I can easily see/test this particular use-case without relying on someone else's HTML dump!
I've also started using this style on mobile which has surfaced a bunch of new issues (e.g. horizontal scroll at default zoom). I'd like to address these too as part of a future update.
That seems like a reasonable middle ground and would certainly automate most of the checking while also not forgetting anything!