# 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.
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](https://ui.shadcn.com/r/styles/new-york-v4/card.json) and [theming docs](https://ui.shadcn.com/docs/theming). The Card component classes are:
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
```css
.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
```css
.rt-dark .rt-card {
background: #171717;
color: #fafafa;
border-color: rgba(255, 255, 255, 0.1);
}
```
### Hover Effect
Border darkens one step on hover:
```css
.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`:
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) |
| `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 (`#f6f7f8` light, `#1a1a1b` dark) provides natural contrast with the white/dark card surfaces.
-`"shadcn"` — override the content area background to shadcn's `--muted` token (`#f5f5f5` light / `#262626` dark). 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:
`align-items: flex-start` preserves block-layout behavior — non-post children (pagination links, RES markers, empty-state messages) keep their intrinsic width instead of stretching to fill the container. Posts (`.rt-card`) are set to full width explicitly.
Non-post children of `#siteTable` (ads, "next page" links, clearfix divs) participate in the flex flow. Clearfix divs that add unwanted gaps are hidden:
| `lib/settings.js` | Add `badgeLayout`, `cardLayout`, `cardBackground`, `cardGap` to `DEFAULTS`. Move `isDarkMode()` and `getColorPrefix()` here from `badges.js` as shared utilities. |
| `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:
```js
window.RedditTweaks.cardTweak = {
apply(settings), // process existing posts + start observer
updateSettings(settings), // live-update gap/background without reload
};
```
### `apply(settings)` Responsibilities
1. Add `rt-card-enabled` class to `<html>`.
2. Query all `div.thing.link`, add `rt-card` class to each.
3. Set `--rt-card-gap` custom property on `#siteTable` from `settings.cardGap`.
4. Apply background class (`rt-card-bg-shadcn`) to `<html>` if `settings.cardBackground === "shadcn"`.
5. Apply dark mode class (`rt-dark`) based on current dark mode state.
6. Start MutationObserver on `#siteTable` (or fallback container) to add `rt-card` to dynamically loaded posts (RES infinite scroll, etc.).
### `updateSettings(settings)` Responsibilities
1. Update `--rt-card-gap` custom property.
2. Toggle `rt-card-bg-shadcn` class on `<html>`.
3. 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 `enabled` is on): no tweaks are applied, original Reddit layout is preserved.
The card tweak respects the same dark mode setting (`auto`/`light`/`dark`) as badges. The `rt-dark` class is applied to the `<html>` element so both card and badge CSS can reference it.
`isDarkMode(settings)` and `getColorPrefix(settings)` are moved from `badges.js` into `settings.js` as shared infrastructure, exposed as `window.RedditTweaks.isDarkMode` and `window.RedditTweaks.getColorPrefix`. Both `badges.js`, `cards.js`, and `color-popover.js` reference these shared functions. This eliminates the card module's dependency on the badge module for dark mode detection.
- **RES infinite scroll:** The MutationObserver catches new `.thing.link` elements and adds `rt-card`. Same pattern as the badge tweak's observer.
- **RES night mode toggle:** Dark mode observer (already in `badges.js`) triggers `updateSettings()` to refresh card dark mode.
- **Promoted posts / ads:** Reddit's promoted posts use `.thing.promoted.link`. They match `div.thing.link` and get card treatment. If this is undesirable, they can be excluded via `.thing.promoted` selector, but for now they're included for visual consistency.
- **Self-text expanders:** Clicking "expand" on a self-post adds content inside `.entry`. Since `.entry` has `flex: 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 — `.entry` fills the available space regardless of whether `.thumbnail` is present.
- **Non-post elements in `#siteTable`:** The flex column + gap on `#siteTable` affects 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 `cardLayout` off requires a page reload (same pattern as the existing `enabled` toggle). The `storage.onChanged` listener detects the change and reloads.
- **Cards enabled but badges disabled:** The `.reddit-tweaks-badges` container 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 to `html.rt-badges-enabled`).
- **Popover positioning:** The color popover uses `position: absolute` relative 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:
1. Enable card layout — each post renders as a card with white background, border, rounded corners, and subtle shadow.
2. Hover a card — border darkens subtly, no shadow change.
3. Cards have proper internal layout: vote arrows, thumbnail, and entry are horizontally aligned with 12px gaps.
4. Long post titles wrap within the card without overflowing.
5. Posts without thumbnails render correctly (entry fills the card width).
6. Self-text expansion works — card grows to fit expanded content.
7. Card gap setting: changing from 16px to 8px/24px/32px updates spacing between cards live.
8. Background setting "shadcn" changes the page background to `#f5f5f5`.