Re-creating a Pantone Color Deck in CSS — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogRe-creating a Pantone Color Deck in CSS
    Back to Blog
    Re-creating a Pantone Color Deck in CSS
    html

    Re-creating a Pantone Color Deck in CSS

    Mads Stoumann February 11, 2026
    0 views

    If you’ve ever held a Pantone fan deck — the kind graphic designers used to carry around like a...

    If you’ve ever held a Pantone fan deck — the kind graphic designers used to carry around like a sacred artifact — you know the satisfying way those cards fan out from a single rivet point. Each card swings on its own arc, and you flip through the colors by hand. I wanted to recreate that experience for this week’s CodePen challenge, which is all about color palettes. Follow along as we build a fully interactive color fan deck where the spread adapts to the container width, cards know their position among their siblings, and clicking a card to "focus" it is handled entirely by the browser’s native `<details>` element. No JavaScript! Let’s dive in! --- ## The Markup Our fan deck is a `<section>` containing a cover card, followed by color cards, each wrapped in a `<details>` element: ```html <section> <!-- cover card --> <details name="deck"> <summary>Reds <span>×</span></summary> <ul> <li style="--c: lab(45% 67 30)"> <strong>Poppy Red</strong> <dl> <dt>HEX</dt><dd>#DC3D4C</dd> <dt>RGB</dt><dd>220, 61, 76</dd> <dt>LAB</dt><dd>45, 56, 25</dd> </dl> </li> <!-- more colors --> </ul> </details> <details name="deck"> <summary>Blues <span>×</span></summary> <!-- ... --> </details> <!-- more cards --> </section> ``` A few things to note: - The **cover card** doesn’t toggle — it just sits at the front of the deck. - Each **color card** is a `<details name="deck">` element. The `name` attribute is the key — it makes them an *exclusive accordion*. Only one can be open at a time, and clicking the open one closes it. - The `<summary>` serves as both the card label and the click target. Not much to see yet. Let’s add some CSS: ![Single Color Card](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg00o8rv9j7nu26ju4aw.png) I won’t go into the CSS in depth here; it’s simply a `<ul>` with the color values defined in a `<dl>` and wrapped up in a grid. --- ## Stacking the Deck First, we need all cards to occupy the same grid cell, stacked on top of each other: ```css section { container-type: inline-size; display: grid; place-items: end center; } section > * { grid-area: 1 / -1; z-index: calc(sibling-count() - sibling-index()); } ``` Setting `container-type: inline-size` on the `<section>` lets us use **container query units** later. Every direct child is placed in the same grid cell with `grid-area: 1 / -1`, creating a stack. The `z-index` line uses two new CSS functions — `sibling-count()` and `sibling-index()` — to ensure the first card sits on top. The first child has `sibling-index()` of 1, so it gets the highest `z-index`. The last child gets 1. Natural stacking order — no hardcoded values, no counters, no JavaScript. So, for now, we just see the cover card — the color cards are hidden behind it (the rivet is an `::after` pseudo-element with a `radial-gradient`): ![Cover Card](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3jebi24ijiwzlymx45tf.png) --- ## The Fan Spread with `progress()` This is where it gets interesting. A real fan deck spreads wider when you have room, and collapses into a tight stack in a narrow space. We want the same behavior — and the new CSS `progress()` function makes it elegant: ```css section > * { --spread: progress(100cqi, 300px, 1440px); --end-degree: calc(var(--spread) * 45deg); --start-degree: calc(var(--spread) * -45deg); } ``` **`progress()`** returns a value between `0` and `1` based on where a value falls within a range. Here, `progress(100cqi, 300px, 1440px)` asks: "How far is the container’s inline size between 300px and 1440px?" - At 300px or below: `--spread` is `0` — no fan, cards stacked flat. - At 1440px or above: `--spread` is `1` — full fan, cards spanning from -45° to +45°. - At 870px (midpoint): `--spread` is `0.5` — half fan. No `@container` queries, just one line of CSS, and the spread is *continuously responsive*. --- ## Positioning Each Card with `sibling-index()` Now each card needs its own rotation angle, interpolated between `--start-degree` and `--end-degree` based on its position in the deck: ```css section > * { rotate: calc( var(--start-degree) + (var(--end-degree) - var(--start-degree)) * (sibling-index() - 1) / (sibling-count() - 1) ); transform-origin: calc(100% - var(--rivet)) calc(100% - var(--rivet)); } ``` Let’s break it down: 1. `sibling-index() - 1` gives us a zero-based position (0 for first card, 1 for second, etc.) 2. `sibling-count() - 1` gives us the total number of "gaps" between cards 3. Dividing them gives a progress value from `0` to `1` for each card’s position 4. We multiply that by the degree range and add the start offset The `transform-origin` is set to the bottom-right corner — offset by `--rivet` — so all cards rotate around the same pivot point, just like a physical fan deck with a rivet pin. Cool! The cards now fan out from a single point, and the spread adjusts automatically with the container width, but they’re not interactive yet. Now we have: ![Full spread](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/io6xo3wjhzha01gbzlz9.png) Let’s resize the browser: ![Resized Fan Deck](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ka50u4h8ahueoaa5wncc.png) I find this incredibly satisfying! --- ## Click-to-Focus with Exclusive `<details>` Here’s where the `<details>` element earns its place. By giving all color cards `name="deck"`, the browser enforces *exclusive accordion* behavior: - **Click a card’s summary** → it opens (gets the `[open]` attribute), any other open card closes automatically. - **Click the same summary again** → it closes, returning to the default fan. But the `<details>` element normally *hides* its content when closed. We want the color cards to always be visible — the open/closed state should only affect the card’s *rotation*, not its content visibility. This is where the new `::details-content` pseudo-element comes in: ```css details::details-content { content-visibility: visible; display: contents; } ``` The `::details-content` pseudo-element targets the content slot of a `<details>` — everything that isn’t the `<summary>`. By overriding `content-visibility` to `visible` and setting `display: contents`, the card’s color list is always rendered, regardless of the open state. Let’s see how it looks when we select a card: ![Selected Color Card](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jfi7l2fyh3x11uldxykv.png) --- ## CSS-Only State Detection When a card is open, we want three things to happen: 1. The **active card** rotates to 0° (straight up) 2. Cards **before** it collapse toward the start 3. Cards **after** it push toward the end We need boolean-like flags — `0` or `1` — that each card can use in its rotation formula. And we can set them entirely with CSS selectors: ```css /* Any card is active */ section:has(details[open]) > * { --has-active: 1; } /* Cards before the active one */ section > :has(~ details[open]) { --is-before: 1; } /* The active card itself */ details[open] { --is-active: 1; } /* Cards after the active one */ details[open] ~ * { --is-after: 1; } ``` Four selectors, four flags. Let’s unpack them: - `section:has(details[open])` matches the section *when any details child is open*, then sets `--has-active: 1` on all children. - `:has(~ details[open])` matches any element that has a *subsequent sibling* that is `details[open]` — i.e., it comes *before* the active card. - `details[open]` matches the active card directly. - `details[open] ~ *` matches all *subsequent siblings* — the cards *after* the active one. The defaults are all `0`, set on the base `section > *` rule. When no card is open, all flags are `0`, and the cards fan normally. ### The Full Rotation Formula With the flags in place, the rotation formula handles all states: ```css section > * { rotate: calc( (var(--start-degree) + (var(--end-degree) - var(--start-degree)) * (sibling-index() - 1) / (sibling-count() - 1)) * (1 - var(--is-active)) - var(--is-before) * (var(--end-degree) - var(--start-degree)) * (sibling-index() - 1) / (sibling-count() - 1) * 0.85 + var(--is-after) * (var(--end-degree) - var(--start-degree)) * (1 - (sibling-index() - 1) / (sibling-count() - 1)) * 0.85 ); transition: rotate .25s linear; } ``` So what’s going on? - **Line 1–2:** The normal fan rotation — the same formula from before. - **`* (1 - var(--is-active))`:** Multiplying by 0 when active zeroes out the rotation — the card snaps to 0°. - **Before cards:** Subtract a value that pushes them further toward the start. The `0.85` factor collapses them tightly but not completely. - **After cards:** Add a value that pushes them further toward the end, using the *inverse* position `(1 - progress)` so they fan toward the opposite edge. The `transition` gives it a smooth, satisfying swing. --- ## The New CSS Features — a Recap This component leans on several CSS features that are all relatively new — so use a modern browser. | Feature | What It Does Here | |---|---| | `progress()` | Returns 0–1 based on container width, driving the fan spread | | `sibling-index()` | Each card knows its position — used for rotation and z-index | | `sibling-count()` | Total number of cards — used to normalize position to 0–1 | | `<details name="">` | Exclusive accordion — click to open/close, only one active | | `::details-content` | Override content visibility so cards always show their colors | --- ## Final Thoughts I’m constantly blown away by how far CSS has progressed — and is progressing. What I find exciting is how these new features *compose*. None of them alone are revolutionary — but `progress()` feeding into `sibling-index()`-driven rotation, toggled by native `<details>` state detected via `:has()` selectors, all without a single line of JavaScript. Here’s a CodePen demo. I urge you to open it full-screen, resize, click etc.: {% codepen https://codepen.io/stoumann/pen/zxByRmP %}

    Tags

    htmlcsstutorialwebdev

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.