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; | |
} | |
} |
(actually, I applied to tilde.town via SSH. Which is an amazing user filter. LOL)
Hey, it seems like the Userstyle has stopped working - starting today, at least for me.
Using the Userstyle directly from this gist, fwiw.
What extension are you using to apply the CSS? The gist hasn't been updated for awhile and it's still working fine for me on both Safari and Firefox.
Hey, it seems like the Userstyle has stopped working - starting today, at least for me.
Using the Userstyle directly from this gist, fwiw.What extension are you using to apply the CSS? The gist hasn't been updated for awhile and it's still working fine for me on both Safari and Firefox.
Stylus with Firefox, oddly enough, it seems to have fixed itself, but prior to that, it just wasn't getting applied at all.
And it's back to not being applied. I'm clueless as to what's happening.
And it's back to not being applied. I'm clueless as to what's happening.
When you next notice it not working, can you dump the HTML source here so I can take a look? Maybe there's an overly specific CSS selector that's not applying.
And it's back to not being applied. I'm clueless as to what's happening.
When you next notice it not working, can you dump the HTML source here so I can take a look? Maybe there's an overly specific CSS selector that's not applying.
Yeah, I probably should've started with that, sorry.
Here it is: https://gist.github.com/nervous-inhuman/1e1f470ab7a108a8e4f759bcb77391ca
FYI I've been playing around with the style, mostly around the layout of items and the upvote/downvote links. I'll dogfeed it for a week to ensure I haven't completely broken things before I publish it. Maybe it'll fix things for a few of you β who knows!
@nervous-inhuman I loaded your HTML gist and the page rendered just fine with the current CSS. It perhaps suggests there's something else interfering with the style on your end?
Oh yikes, yes that's awful. I've removed the td.default div:first-child
rule altogether, the HTML has obviously been updated and it no longer targets whatever it was it was originally supposed to! Hopefully the update propagates quickly to a stylesheet extension near you...
Thanks @christippett ! I noticed the same the other day; turned it off assuming (correctly) that it'll get fixed. Appreciate this HN restyle!
Just noticed that if I try to style a code block by indenting it 4 spaces, it won't monospace it as it's supposed to...
@pmarreck leave it with me, I'll look into it.
While we're posting bugs: there's an issue with the column widths becoming unwieldy if the first item on a page happens to be a sponsored post (ie. a YC company job ad). These items don't allow for up/down votes which throws out the alignment for the rest of the table. I've noticed this mostly when viewing page 2+ onwards.
@pmarreck Just pushed an update. Code blocks should be back to being in monospace... I was too overzealous with my use of * { font-family: ... }
π«‘
I also ended up removing most of the styles used customise the up/down vote icons in this update... it was giving me too much grief and the various corner-cases and workarounds needed to make them work weren't worth it.
Hopefully I haven't messed up anything too bad in the process!
Looking good!
Looks great over here, thanks @christippett !
Good call. I'll make the change later today.
I also only just now made the connection between your account here and on HN... I commented on your macOS automation article completely oblivious that it was you. Thank you for not making it weird and picking my comment to take a screenshot of ;-)
OK, this is better:
td.nosee + td > div:first-child {
margin-bottom: 10px !important;
}
Sorry for the delay! Rather than piling on more styles, I removed the style I'd added that removed the original margin between comments.
Unfortunately the diff is masked by whitespace changes (opps), what I changed was to remove .default br:has(+ div.comment)
from the following section:
/* space between author/links and coment text */
.default br:has(+ div.comment),
.votelinks>center> :is(font+br, br+img) {
display: none;
}
All good!
I can't believe I'm saying this but I feel like CSS needs some kind of unit-testing framework lol
If only because a ton of the possible declarative changes you can make could result in regressions in other content that wasn't considered
It's possible that an LLM could help here because then you could assert on abstract notions like "make sure these voting controls in this one view are visible, but only when the related content is rolled over, and shaped like up/down triangles"
Just thinking aloud because I could probably build something like this if it was useful (and because I'm looking for work...)
I've been giving this some thought too. What I think would be useful is to configure Playwright to take a bunch of screenshots of different pages which could then be manually reviewed before publishing a release. Each screenshot could have a side-by-side before/after view that would hopefully surface any obvious regressions or visual quirks...
That seems like a reasonable middle ground and would certainly automate most of the checking while also not forgetting anything!
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.
Since I got banned by Reddit across everything thanks to "intelligence-free and thus sometimes accidentally too punitive fingerprinting", I can't even join "tildes" apparently. Is there another way to join?