<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yyj-dev</title>
    <description>The latest articles on DEV Community by yyj-dev (@yyj).</description>
    <link>https://dev.to/yyj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3997002%2F54f4f34f-d2a3-4bb1-bbb5-a3977912901d.png</url>
      <title>DEV Community: yyj-dev</title>
      <link>https://dev.to/yyj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yyj"/>
    <language>en</language>
    <item>
      <title>One React Component, Three Display Technologies — Flip Cards, Pixel LEDs, and Seven-Segment Digits in Pure CSS</title>
      <dc:creator>yyj-dev</dc:creator>
      <pubDate>Fri, 03 Jul 2026 07:28:26 +0000</pubDate>
      <link>https://dev.to/yyj/one-react-component-three-display-technologies-flip-cards-pixel-leds-and-seven-segment-digits-3dbn</link>
      <guid>https://dev.to/yyj/one-react-component-three-display-technologies-flip-cards-pixel-leds-and-seven-segment-digits-3dbn</guid>
      <description>&lt;p&gt;I recently shipped a fullscreen clock app, &lt;a href="https://digitalclock.xyz/" rel="noopener noreferrer"&gt;digitalclock.xyz&lt;/a&gt;, that offers eight themes. Four of them look like a mechanical flip clock, three like a retro pixel LED display, and one like a seven-segment alarm clock. Three genuinely different display technologies — and I wanted all of them rendered by the same component tree, with zero canvas, zero SVG, and zero image assets.&lt;/p&gt;

&lt;p&gt;That constraint turned out to be the most interesting engineering decision in the project. Here's how the architecture actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  A theme is just data
&lt;/h2&gt;

&lt;p&gt;The first rule I set: a theme is not a component, not a CSS file, not a variant prop scattered across the tree. It's a plain object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ThemeStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pixel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;segment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ThemeId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ThemeStyle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pageBg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cardBg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cardBgGradient&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;digitColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;divider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;glow&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;fontClassVar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;var(--font-flip)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;var(--font-flip-mono)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;var(--font-pixel)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;separatorColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subtleText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything a theme knows is color tokens, an optional glow, a font variable — and one 3-value enum, &lt;code&gt;style&lt;/code&gt;, which decides which of three rendering branches the digit component takes. All eight themes fit in a single array in &lt;code&gt;themes.ts&lt;/code&gt;. "Flip Ocean" and "Pixel Amber" differ only in the values of these fields.&lt;/p&gt;

&lt;p&gt;The important consequence: adding a ninth theme is a data change, not a code change. Adding a fourth &lt;em&gt;display technology&lt;/em&gt; is a code change — one new branch — but nothing else in the tree has to know about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only interface between JS and styling is custom properties
&lt;/h2&gt;

&lt;p&gt;React never writes real CSS declarations inline. The digit component's inline &lt;code&gt;style&lt;/code&gt; contains nothing but CSS custom properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--seg-on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digitColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--seg-off&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`color-mix(in srgb, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digitColor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 10%, transparent)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--seg-glow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;glow&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CSSProperties&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static CSS rules in &lt;code&gt;globals.css&lt;/code&gt; consume those variables. JS decides &lt;em&gt;what the values are&lt;/em&gt;; CSS decides &lt;em&gt;what they mean&lt;/em&gt;. That split is what keeps three unrelated display technologies from leaking into each other — the React side genuinely does not know how a segment gets its bevel or how a flip card folds.&lt;/p&gt;

&lt;p&gt;One detail I like a lot: the seven-segment theme only declares a single color, &lt;code&gt;digitColor&lt;/code&gt;. The dim "off" segments are derived in that snippet above with &lt;code&gt;color-mix(in srgb, &amp;lt;digitColor&amp;gt; 10%, transparent)&lt;/code&gt;, and the glow is another derivation of the same hue. One token in, an entire lit/unlit/halo palette out. When I added the theme picker, no theme ever shipped with mismatched on/off colors, because there's nothing to mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch 1: pixel — the trivial case
&lt;/h2&gt;

&lt;p&gt;The pixel LED branch is almost embarrassingly simple, which is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pixel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flip-pixel-digit"&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digitColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;textShadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;glow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pixel font does the heavy lifting for the dot-matrix look, and a two-layer &lt;code&gt;text-shadow&lt;/code&gt; (an 18px halo plus a wider 36px falloff) makes it bloom like a backlit display. The glyph &lt;em&gt;is&lt;/em&gt; text, so the glow technique that fits is &lt;code&gt;text-shadow&lt;/code&gt; — it hugs the letterforms for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch 2: seven-segment — the font is in the stylesheet
&lt;/h2&gt;

&lt;p&gt;This is the branch people don't believe until they open devtools. A seven-segment digit renders as seven empty spans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg-digit"&lt;/span&gt; &lt;span class="na"&gt;data-value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-h seg-a"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-v seg-b"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-v seg-c"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-h seg-d"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-v seg-e"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-v seg-f"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"seg seg-h seg-g"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No text content at all. The "font" — which segments light up for which digit — lives entirely in CSS as ten attribute-selector rules. It's literally the 0–9 truth table you'd find in a 7-segment decoder datasheet, written in selectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.seg-digit&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"2"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nd"&gt;:is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.seg-a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-b&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-d&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-e&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-g&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
&lt;span class="nc"&gt;.seg-digit&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"4"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nd"&gt;:is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.seg-b&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-c&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-f&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-g&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
&lt;span class="nc"&gt;.seg-digit&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"8"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nd"&gt;:is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.seg-a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-b&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-c&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-d&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-e&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-f&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.seg-g&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seg-on&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React's entire job here is setting &lt;code&gt;data-value="4"&lt;/code&gt;. The cascade does the decoding.&lt;/p&gt;

&lt;p&gt;The geometry is absolutely positioned in &lt;code&gt;em&lt;/code&gt; units — &lt;code&gt;seg-a&lt;/code&gt; at &lt;code&gt;top: 0&lt;/code&gt;, &lt;code&gt;seg-g&lt;/code&gt; at &lt;code&gt;top: 0.455em&lt;/code&gt;, &lt;code&gt;seg-d&lt;/code&gt; at &lt;code&gt;top: 0.91em&lt;/code&gt;, verticals hung at the corners — so the whole digit scales with &lt;code&gt;font-size&lt;/code&gt; like real text does. The classic tapered segment ends are one &lt;code&gt;clip-path: polygon(...)&lt;/code&gt; per orientation, hexagons instead of rectangles.&lt;/p&gt;

&lt;p&gt;Two branch-specific details worth stealing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glow:&lt;/strong&gt; &lt;code&gt;text-shadow&lt;/code&gt; is useless here because there's no text. The segment branch uses &lt;code&gt;filter: drop-shadow(var(--seg-glow))&lt;/code&gt; on the container, which follows the clipped hexagon shapes exactly — including the tapered tips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility:&lt;/strong&gt; a pile of empty spans is meaningless to a screen reader, so the container carries &lt;code&gt;aria-label={value}&lt;/code&gt;. Cheap insurance for a fully decorative DOM structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Branch 3: flip — the only branch that needs memory
&lt;/h2&gt;

&lt;p&gt;Flip is the one technology that can't be stateless, because a flip has a &lt;em&gt;before&lt;/em&gt; and an &lt;em&gt;after&lt;/em&gt;. The component keeps the previous value in state and runs a two-phase animation — the top half folds down over 300ms, then the bottom half unfolds for another 300ms — before committing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPrev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// pixel/segment themes: no animation, just sync&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;setFlipping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;timerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPrev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setFlipping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the second early return: when the active theme is pixel or segment, the same component silently degrades into "just track the value." The flip machinery — the extra state, the timeout, the transient fold elements rendered during &lt;code&gt;flipping&lt;/code&gt; — costs nothing on the branches that don't use it. That's what lets one &lt;code&gt;FlipDigit&lt;/code&gt; component serve all three technologies instead of three sibling components duplicating the value-diffing logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one SSR wrinkle
&lt;/h2&gt;

&lt;p&gt;A clock is the canonical hydration-mismatch generator: the server has no idea what time it is in the client's timezone. My &lt;code&gt;useNow&lt;/code&gt; hook returns &lt;code&gt;Date | null&lt;/code&gt;, starts as &lt;code&gt;null&lt;/code&gt;, and only picks up a real &lt;code&gt;Date&lt;/code&gt; inside &lt;code&gt;useEffect&lt;/code&gt; — so the server render and first client render agree on "no time yet," and the 250ms interval is purely a refresh rate after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this bought me
&lt;/h2&gt;

&lt;p&gt;The final tally for eight themes across three display technologies: one digit component with three branches, one theme array, and a few hundred lines of static CSS. No canvas contexts to manage, no SVG sprites to generate, nothing to preload. The DOM stays inspectable — you can watch digit &lt;code&gt;4&lt;/code&gt; light up &lt;code&gt;b, c, f, g&lt;/code&gt; in the elements panel, which also made every rendering bug a devtools problem instead of a bitmap-debugging problem.&lt;/p&gt;

&lt;p&gt;If you want to poke at the result (devtools open, ideally), it's live at &lt;a href="https://digitalclock.xyz/" rel="noopener noreferrer"&gt;digitalclock.xyz&lt;/a&gt;. And if you've pushed attribute selectors somewhere equally questionable, I'd honestly love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>webdev</category>
      <category>css</category>
      <category>react</category>
    </item>
    <item>
      <title>What Western Devs Need to Know Before Visiting China in 2026: Alipay, WeChat Pay &amp; the Mobile Web</title>
      <dc:creator>yyj-dev</dc:creator>
      <pubDate>Tue, 23 Jun 2026 15:42:03 +0000</pubDate>
      <link>https://dev.to/yyj/what-western-devs-need-to-know-before-visiting-china-in-2026-alipay-wechat-pay-the-mobile-web-1bi0</link>
      <guid>https://dev.to/yyj/what-western-devs-need-to-know-before-visiting-china-in-2026-alipay-wechat-pay-the-mobile-web-1bi0</guid>
      <description>&lt;p&gt;If you write software for a living and you're considering a trip to China in 2026, the friction you'll hit is not what you expect. The Great Firewall is the headline, but it's rarely what trips up a first-time visitor. What actually breaks your week is the small stuff: a QR code at a noodle shop, a metro turnstile that won't take your foreign card, a hotel Wi-Fi that quietly drops every request to Google.&lt;/p&gt;

&lt;p&gt;This is a brief survival guide written from a developer's mindset: what's actually changed in 2026, what you can fix before you leave, and what you should just accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Visa-free entry now covers most Western devs
&lt;/h2&gt;

&lt;p&gt;As of late 2025, China extended its 30-day visa-free transit policy to passport holders from 38 countries, including the US, UK, Germany, France, Australia, the Netherlands, and most of the EU. If you're flying in for a vacation, a conference, or even a short remote-work stretch, you may not need to apply for a visa at all — you just need an onward ticket within 30 days.&lt;/p&gt;

&lt;p&gt;The catch: the rules per nationality drift quarterly, and the official guidance is scattered across embassy pages. I keep a more current breakdown here: &lt;a href="https://firsttripchina.com/" rel="noopener noreferrer"&gt;FirstTripChina visa-free guide&lt;/a&gt; — worth checking the week you book your ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The payment problem is the real "API" you need to integrate
&lt;/h2&gt;

&lt;p&gt;China runs on two payment rails: Alipay and WeChat Pay. Cash is technically legal but vendors below the level of a 4-star hotel will look at you like you handed them a stone tablet. Foreign credit cards work at airports and big chains; they do not work at the dumpling place you actually want to eat at.&lt;/p&gt;

&lt;p&gt;The fix that exists in 2026 — and that did not exist three years ago — is "Tour Card" inside Alipay and "International" mode inside WeChat Pay. Both let you link a Visa/Mastercard issued outside China and pay via the same QR system locals use. Setup steps (roughly):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Alipay (App Store / Play Store, US/EU regions both work).&lt;/li&gt;
&lt;li&gt;Verify with passport + selfie (KYC takes about 3 minutes).&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Tour Card&lt;/strong&gt; → add your foreign card → top up an in-app balance, OR enable real-time charges.&lt;/li&gt;
&lt;li&gt;Repeat the equivalent flow in WeChat → Me → Services → Wallet → International.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two tips that took me embarrassingly long to figure out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-ups under ¥200 (~$28) waive fees&lt;/strong&gt;; above that, you pay ~3%. Re-charge often instead of once.&lt;/li&gt;
&lt;li&gt;WeChat Pay's &lt;code&gt;International&lt;/code&gt; mode has a daily transaction cap that resets at midnight Beijing time, &lt;em&gt;not&lt;/em&gt; midnight local time. Plan accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote up the full setup with screenshots: &lt;a href="https://firsttripchina.com/" rel="noopener noreferrer"&gt;Alipay &amp;amp; WeChat Pay setup for foreigners&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your phone is a router, not just a phone
&lt;/h2&gt;

&lt;p&gt;If you're traveling from a country with a "free roaming in China" plan (T-Mobile US, Three UK, some EU carriers), the bandwidth is throttled but the &lt;strong&gt;route is unfiltered&lt;/strong&gt; — your traffic exits in Hong Kong or Singapore. Google, GitHub, Slack, your work VPN: all just work.&lt;/p&gt;

&lt;p&gt;If you're not on one of those plans, you have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Buy a physical or eSIM tourist plan&lt;/strong&gt; from China Unicom HK or 3HK — these route through Hong Kong, so still unfiltered. ~$25 for 20 GB / 30 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-install a paid VPN with a stealth protocol&lt;/strong&gt; (WireGuard over obfuscation works in mid-2026; pure OpenVPN is detected). Test it &lt;em&gt;before&lt;/em&gt; you land — VPN provider websites are blocked inside the wall, so you can't sign up after arrival.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do nothing&lt;/strong&gt; and accept that for one week, your Slack will live on Telegram, your Gmail on Outlook, and your Google Maps on Apple Maps. (Yes, Apple Maps works in China. It's surprisingly good. Baidu Maps is better.)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. High-speed rail beats everything else for inter-city
&lt;/h2&gt;

&lt;p&gt;The high-speed rail (HSR) network in 2026 is the single best argument for traveling in mainland China. Beijing → Shanghai (1,300 km / 800 mi) is 4h 18m at 350 km/h, costs ¥553 (~$77) in second class, and runs every 12 minutes. There is no airport security line, no boarding pass, no luggage check — you tap your passport at the gate and walk on.&lt;/p&gt;

&lt;p&gt;Book through &lt;strong&gt;Trip.com&lt;/strong&gt; or &lt;strong&gt;12306 English&lt;/strong&gt; in your Apple/Play store. The native 12306 app is more reliable but the English UI is rough. Buy your ticket the day before; popular Beijing-Shanghai trains do sell out on weekends.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The three cities most worth your first week
&lt;/h2&gt;

&lt;p&gt;If this is your first trip and you have 7–10 days, the strongest itinerary I've seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beijing (3 days)&lt;/strong&gt; — Forbidden City, Mutianyu Great Wall, 798 art district. The history is the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Xi'an (2 days)&lt;/strong&gt; — Terracotta Army, the Muslim Quarter night market. Worth the detour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shanghai (3 days)&lt;/strong&gt; — Modern China. The Bund, Pudong skyline, plus the food scene most worth flying for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HSR connects all three in &amp;lt;6 hours each leg. Full city-by-city breakdown: &lt;a href="https://firsttripchina.com/" rel="noopener noreferrer"&gt;FirstTripChina city guides&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check visa-free eligibility before you book — for most Western passports in 2026, you don't need a visa for ≤30 days.&lt;/li&gt;
&lt;li&gt;Install Alipay + WeChat Pay &lt;em&gt;before&lt;/em&gt; you fly. Their KYC requires a phone number you can receive SMS on; do it from home.&lt;/li&gt;
&lt;li&gt;Get an HK-routed eSIM if your home carrier doesn't include China roaming.&lt;/li&gt;
&lt;li&gt;Beijing → Xi'an → Shanghai by HSR is the optimal first-trip loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The country is friendlier to foreign travelers than its reputation suggests, but the friction is concentrated in the first 24 hours after you land. Front-load the prep, and the rest of the trip is a breeze.&lt;/p&gt;

</description>
      <category>travel</category>
      <category>lifestyle</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built PDF Dark — convert any PDF to true dark mode, 100% in your browser</title>
      <dc:creator>yyj-dev</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:12:29 +0000</pubDate>
      <link>https://dev.to/yyj/i-built-pdf-dark-convert-any-pdf-to-true-dark-mode-100-in-your-browser-5985</link>
      <guid>https://dev.to/yyj/i-built-pdf-dark-convert-any-pdf-to-true-dark-mode-100-in-your-browser-5985</guid>
      <description>&lt;h2&gt;
  
  
  Why another PDF tool?
&lt;/h2&gt;

&lt;p&gt;I read a lot of PDFs at night — research papers, ebooks, technical manuals. Every PDF reader I tried had the same problem: their "dark mode" was just a viewer toggle. The moment I forwarded the PDF to my Kindle or iPad, the white background blasted me again.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://pdfdark.org" rel="noopener noreferrer"&gt;&lt;strong&gt;PDF Dark&lt;/strong&gt;&lt;/a&gt; — a tool that converts any PDF into a &lt;strong&gt;real dark-mode PDF file&lt;/strong&gt; you can download, share, and reread anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100% browser-side conversion.&lt;/strong&gt; Your file never touches a server. Verifiable in DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real dark output PDF&lt;/strong&gt;, not a viewer trick. Works on Kindle, iPad, Acrobat, anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturation-aware algorithm&lt;/strong&gt; — photos and charts keep their original colors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free, MIT-licensed, no signup, no ads.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;The conversion runs entirely in the browser using &lt;strong&gt;PDF.js&lt;/strong&gt; to parse the original PDF page by page, then re-renders each page through a Canvas pipeline. The algorithm walks pixel data and inverts only low-saturation regions (text, line art, white background), while preserving high-saturation regions (photos, color charts, logos).&lt;/p&gt;

&lt;p&gt;The output is then re-encoded as a standard PDF using &lt;strong&gt;pdf-lib&lt;/strong&gt;. No server, no upload — your file never leaves your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://pdfdark.org" rel="noopener noreferrer"&gt;https://pdfdark.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub (MIT): &lt;a href="https://github.com/1436941541/pdf-dark" rel="noopener noreferrer"&gt;https://github.com/1436941541/pdf-dark&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;This is an early version. PRs, bug reports, and feature requests are very welcome — especially edge cases (complex form PDFs, scanned PDFs with dense images, RTL languages). Drop a comment below or open an issue on GitHub.&lt;/p&gt;

&lt;p&gt;If you read PDFs at night, give it a try and let me know what breaks.&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
  </channel>
</rss>
