13 KiB
Card Layout: shadcn-Style Post Cards — Design Spec
Date: 2026-08-26 Status: Draft
Overview
Replace old Reddit's flat post list with individual cards styled to match shadcn/ui's Card component. Each div.thing.link gets card treatment — background, border, radius, shadow, and internal flexbox layout — using the exact design tokens from shadcn's default neutral theme.
Cards are a separate toggle from badges. Either can be enabled independently.
Approach
Class injection + scoped CSS overrides. Each .thing.link receives an rt-card class via JS. All card styling is scoped under .rt-card in CSS. The existing DOM tree stays intact — no element relocation or wrapper injection. The internal float-based layout is overridden to flexbox within the .rt-card scope.
shadcn Reference Values
All values sourced from the shadcn Card registry and theming docs. The Card component classes are:
flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm
Token-to-CSS Mapping (Default Neutral Theme)
| Token | Light | Dark |
|---|---|---|
--card |
#ffffff (oklch 1 0 0) |
#171717 (oklch 0.205 0 0) |
--card-foreground |
#0a0a0a (oklch 0.145 0 0) |
#fafafa (oklch 0.985 0 0) |
--border |
#e5e5e5 (oklch 0.922 0 0) |
rgba(255,255,255,0.1) (oklch 1 0 0 / 10%) |
--muted-foreground |
#737373 (oklch 0.556 0 0) |
#a3a3a3 (oklch 0.708 0 0) |
--muted |
#f5f5f5 (oklch 0.97 0 0) |
#262626 (oklch 0.269 0 0) |
--radius |
0.625rem (10px) |
same |
Computed CSS Values
| Property | Tailwind class | Value |
|---|---|---|
| border-radius | rounded-xl |
0.875rem (14px) — --radius × 1.4 |
| vertical padding | py-6 |
1.5rem (24px) |
| horizontal padding | px-6 (CardContent) |
1.5rem (24px) |
| section gap | gap-6 |
1.5rem (24px) |
| shadow | shadow-sm |
0 1px 2px 0 rgb(0 0 0 / 0.05) |
Card Container Styling
shadcn's Card root has py-6 (vertical only); horizontal padding lives on CardContent/CardHeader/CardFooter (px-6). Since we're styling a flat .thing.link without shadcn's section anatomy, we apply padding: 1.5rem on all sides — equivalent to both py-6 and px-6 combined.
Light Mode
.rt-card {
background: #ffffff;
color: #0a0a0a;
border: 1px solid #e5e5e5;
border-radius: 0.875rem;
padding: 1.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
transition: border-color 0.15s;
}
Dark Mode
.rt-dark .rt-card {
background: #171717;
color: #fafafa;
border-color: rgba(255, 255, 255, 0.1);
}
Hover Effect
Border darkens one step on hover:
.rt-card:hover {
border-color: #d4d4d4; /* neutral-300 */
}
.rt-dark .rt-card:hover {
border-color: rgba(255, 255, 255, 0.18);
}
Shadow does not change on hover — consistent with shadcn's static card.
Internal Layout
Old Reddit uses float-based layout inside .thing.link:
[.rank] [.midcol (votes)] [.thumbnail] [.entry (title + tagline + buttons)]
All children float left; .entry flows after the floats. A .clearleft div clears at the end.
The card overrides this to flexbox:
.rt-card {
display: flex;
align-items: flex-start;
gap: 0.75rem;
overflow: hidden;
}
.rt-card .midcol,
.rt-card .thumbnail {
float: none;
flex-shrink: 0;
}
.rt-card .entry {
flex: 1;
min-width: 0;
}
.rt-card .clearleft {
display: none;
}
The .rank element (post number, only visible when enabled in Reddit preferences) participates in the flex flow naturally.
The badges container (.reddit-tweaks-badges) stays at the top of .entry where it's already injected by the badge tweak. No change to badge positioning — badges sit inside the card when both features are enabled.
Settings
Four new settings:
| Key | Type | Default | Description |
|---|---|---|---|
badgeLayout |
boolean |
true |
Enable/disable badge replacements (new toggle for existing feature) |
cardLayout |
boolean |
true |
Enable/disable card layout |
cardBackground |
"reddit" | "shadcn" |
"reddit" |
Page background style |
cardGap |
number |
16 |
Pixel gap between cards |
badgeLayout is a new toggle that controls the existing badge replacement feature. Previously, badges always ran when enabled was on. Now enabled remains the master switch, and badgeLayout/cardLayout are independent sub-toggles beneath it.
The FOUC CSS that hides original subreddit/comment elements is scoped to html.rt-badges-enabled (renamed from html.rt-enabled) so it only applies when badges are active. main.js adds rt-badges-enabled to <html> only when settings.badgeLayout is true.
Page Background Options
"reddit"— no background override. Reddit's native background (#f6f7f8light,#1a1a1bdark) provides natural contrast with the white/dark card surfaces."shadcn"— override the content area background to shadcn's--mutedtoken (#f5f5f5light /#262626dark). Gives cards slightly more visual lift.
Card Gap
The gap between cards is controlled by a CSS custom property --rt-card-gap set on #siteTable. The popup offers preset values: 8px, 12px, 16px (default), 24px, 32px.
Popup UI
A badge toggle is added to the existing "Enabled" section, and a new "Card Layout" section appears between "Dark Mode" and "Light Mode Colors":
┌─────────────────────────────┐
│ [✓] Enabled │
│ [✓] Badge layout │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Card Layout │
│ │
│ [✓] Enable card layout │
│ │
│ Background Style │
│ [Reddit Default ▼] │
│ │
│ Card Spacing │
│ [16px ▼] │
└─────────────────────────────┘
Post List Container
The #siteTable (parent of all .thing.link elements) is converted to a flex column so the gap property works:
html.rt-card-enabled #siteTable {
display: flex;
flex-direction: column;
gap: var(--rt-card-gap, 1rem);
}
Non-post children of #siteTable (ads, "next page" links, clearfix divs) participate in the flex flow. Ads and clearfix divs that add unwanted gaps are hidden:
html.rt-card-enabled #siteTable > .clearleft {
display: none;
}
Page background override:
html.rt-card-bg-shadcn .content[role="main"] {
background: #f5f5f5;
}
html.rt-card-bg-shadcn.rt-dark .content[role="main"] {
background: #262626;
}
Architecture
New Files
| File | Purpose |
|---|---|
content/tweaks/cards.js |
Card tweak module |
content/styles/cards.css |
Card CSS |
Modified Files
| File | Change |
|---|---|
manifest.json |
Add content/styles/cards.css to CSS array, add content/tweaks/cards.js to JS array (before main.js) |
lib/settings.js |
Add badgeLayout, cardLayout, cardBackground, cardGap to DEFAULTS |
content/main.js |
Gate badge tweak behind settings.badgeLayout. Call cardTweak.apply(settings) when settings.cardLayout is true. Add rt-badges-enabled class only when badges are on. Wire storage.onChanged to update card settings live. |
content/styles/badges.css |
Change FOUC selectors from html.rt-enabled to html.rt-badges-enabled |
popup/popup.html |
Add "Card Layout" section with toggle, background dropdown, spacing dropdown. Add badge toggle to existing section. |
popup/popup.js |
Wire the new controls to save/load |
Module API
content/tweaks/cards.js exposes:
window.RedditTweaks.cardTweak = {
apply(settings), // process existing posts + start observer
updateSettings(settings), // live-update gap/background without reload
};
apply(settings) Responsibilities
- Add
rt-card-enabledclass to<html>. - Query all
div.thing.link, addrt-cardclass to each. - Set
--rt-card-gapcustom property on#siteTablefromsettings.cardGap. - Apply background class (
rt-card-bg-shadcn) to<html>ifsettings.cardBackground === "shadcn". - Apply dark mode class (
rt-dark) based on current dark mode state. - Start MutationObserver on
#siteTable(or fallback container) to addrt-cardto dynamically loaded posts (RES infinite scroll, etc.).
updateSettings(settings) Responsibilities
- Update
--rt-card-gapcustom property. - Toggle
rt-card-bg-shadcnclass on<html>. - No need to reprocess individual posts — the CSS handles the rest via the scoped classes.
Interaction with Badges
Cards and badges are fully independent:
- Both enabled: badges sit inside the card (in
.entry) as they do today. - Cards only: post renders as a card without badge replacements.
- Badges only: existing behavior unchanged, no card styling.
- Neither (but
enabledis on): no tweaks are applied, original Reddit layout is preserved.
Dark Mode
The card tweak respects the same dark mode setting (auto/light/dark) as badges. The rt-dark class on the card container (already managed by badges.js for badge containers) is extended to also apply to the <html> element so card CSS can reference it. The cards.js module reads the dark mode state from the shared badgeTweak.isDarkMode() utility.
Edge Cases
- RES infinite scroll: The MutationObserver catches new
.thing.linkelements and addsrt-card. Same pattern as the badge tweak's observer. - RES night mode toggle: Dark mode observer (already in
badges.js) triggersupdateSettings()to refresh card dark mode. - Promoted posts / ads: Reddit's promoted posts use
.thing.promoted.link. They matchdiv.thing.linkand get card treatment. If this is undesirable, they can be excluded via.thing.promotedselector, but for now they're included for visual consistency. - Self-text expanders: Clicking "expand" on a self-post adds content inside
.entry. Since.entryhasflex: 1, the card grows to accommodate the expanded content naturally. - Thumbnails missing: Some posts have no thumbnail (text posts, or when thumbnails are disabled). The flex layout handles this —
.entryfills the available space regardless of whether.thumbnailis present. - Non-post elements in
#siteTable: The flex column + gap on#siteTableaffects all children, including the "next page" nav links. These continue to render normally in the flex flow. Clearfix divs (.clearleft) are hidden. - Cards disabled mid-session: Toggling
cardLayoutoff requires a page reload (same pattern as the existingenabledtoggle). Thestorage.onChangedlistener detects the change and reloads. - Cards enabled but badges disabled: The
.reddit-tweaks-badgescontainer is not injected. The card renders the post without badges, showing the original subreddit link and comments link in their default positions (not hidden, since the badge tweak's FOUC CSS is scoped tohtml.rt-badges-enabled). - Popover positioning: The color popover uses
position: absoluterelative to the badge. Inside a card, the badge's offset parent is still.entry(or the card itself). The popover's existing viewport-aware positioning logic handles this.
Testing
Manual testing checklist:
- Enable card layout — each post renders as a card with white background, border, rounded corners, and subtle shadow.
- Hover a card — border darkens subtly, no shadow change.
- Cards have proper internal layout: vote arrows, thumbnail, and entry are horizontally aligned with 12px gaps.
- Long post titles wrap within the card without overflowing.
- Posts without thumbnails render correctly (entry fills the card width).
- Self-text expansion works — card grows to fit expanded content.
- Card gap setting: changing from 16px to 8px/24px/32px updates spacing between cards live.
- Background setting "shadcn" changes the page background to
#f5f5f5. - Background setting "Reddit Default" uses Reddit's native background.
- Dark mode: cards switch to dark surface (
#171717), dark border, light text. - Dark mode: shadcn background switches to
#262626. - Dark mode hover: border lightens subtly.
- RES infinite scroll: new posts loaded via infinite scroll get card treatment.
- RES night mode toggle: cards respond to night mode changes.
- Cards + badges both enabled: badges appear inside the card at the top of
.entry. - Cards enabled, badges disabled: posts render as cards without badge replacements.
- Cards disabled, badges enabled: existing badge behavior unchanged, no card styling.
- Disable card layout toggle — page reloads and posts return to flat list.
- Card spacing dropdown offers 8px, 12px, 16px, 24px, 32px.
- Settings persist across browser restart.