<?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: Tom Ricky</title>
    <description>The latest articles on DEV Community by Tom Ricky (@zhengguge06).</description>
    <link>https://dev.to/zhengguge06</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%2F4047738%2F40ecac4b-bb63-47b3-99c6-6c42c4fc01c6.png</url>
      <title>DEV Community: Tom Ricky</title>
      <link>https://dev.to/zhengguge06</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhengguge06"/>
    <language>en</language>
    <item>
      <title>Using Markdown as a Reviewed Runtime Dataset in TypeScript</title>
      <dc:creator>Tom Ricky</dc:creator>
      <pubDate>Fri, 07 Aug 2026 04:14:32 +0000</pubDate>
      <link>https://dev.to/zhengguge06/using-markdown-as-a-reviewed-runtime-dataset-in-typescript-3kmi</link>
      <guid>https://dev.to/zhengguge06/using-markdown-as-a-reviewed-runtime-dataset-in-typescript-3kmi</guid>
      <description>&lt;p&gt;Your application needs a content pool. Not configuration, not copy—structured data that drives runtime behavior. A quiz needs questions. A generator needs prompts. A flashcard deck needs cards. Each entry has typed fields, belongs to a category, and must pass editorial review before users see it.&lt;/p&gt;

&lt;p&gt;The obvious options are a database, a headless CMS, or a JSON file. But for a bounded dataset that ships with the code, is reviewed by humans, and changes at editorial pace rather than user pace, there is another option: treat a constrained Markdown file as both the human-readable review artifact and the runtime data source.&lt;/p&gt;

&lt;p&gt;This is not a pitch for Markdown as a universal data layer. It works under specific conditions: the dataset is small enough to bundle, reviews happen through version-controlled changes, and you control the schema. Here is how the pipeline works, what it actually enforces, and where it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: reviewable data that also has types
&lt;/h2&gt;

&lt;p&gt;JSON is straightforward for TypeScript to consume, but the type information is compile-time only unless you add runtime validation. Asking a reviewer to scan hundreds of entries in a JSON file for a misclassified difficulty rating or a missing license note is still a poor review experience. Fields blur together. Metadata disappears into nesting.&lt;/p&gt;

&lt;p&gt;A CMS can provide forms and workflows, but may also introduce authentication, hosting, synchronization, and a separate content dependency. For a client-only application with no server functions and infrequent editorial updates, that machinery may not be justified.&lt;/p&gt;

&lt;p&gt;What you want is a format where:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A reviewer can read a batch of entries, see their metadata, and inspect the change in a version-control diff.&lt;/li&gt;
&lt;li&gt;The application can parse that same file into typed objects when the data module is evaluated.&lt;/li&gt;
&lt;li&gt;Entries outside the approved status are excluded when the runtime pool is created.&lt;/li&gt;
&lt;li&gt;Explicit validation commands can catch format violations, duplicates, and missing fields before release.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A constrained Markdown contract
&lt;/h2&gt;

&lt;p&gt;The approach is not "parse arbitrary Markdown." It is closer to: define a rigid document structure using Markdown syntax, then write a parser that validates the recognized data-bearing structure against that contract.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ylnmrmofvgtkkrz0ygo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ylnmrmofvgtkkrz0ygo.png" alt="a clear left-to-right visual pipeline from a Markdown table to typed runtime objects" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A simplified example of what one section looks like (not production code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Batch: imaginary-objects-easy-v1&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Status: approved
&lt;span class="p"&gt;-&lt;/span&gt; Difficulty: easy
&lt;span class="p"&gt;-&lt;/span&gt; Primary category: objects
&lt;span class="p"&gt;-&lt;/span&gt; Source: Original editorial draft
&lt;span class="p"&gt;-&lt;/span&gt; License: Internal project content
&lt;span class="p"&gt;-&lt;/span&gt; Generated by: Editorial team
&lt;span class="p"&gt;-&lt;/span&gt; Imported on: 2026-07-31
&lt;span class="p"&gt;-&lt;/span&gt; Reviewed by: Project owner
&lt;span class="p"&gt;-&lt;/span&gt; Reviewed on: 2026-07-31
&lt;span class="p"&gt;-&lt;/span&gt; Review version: v1

| ID | Prompt | Drawability | Guessability | Aliases | Extra categories | Contexts | Notes |
|---|---|---:|---:|---|---|---|---|
| paper-moon | paper moon | 4 | 4 | — | concepts | — | Simplified example |
| clockwork-fish | clockwork fish | 4 | 3 | — | animals | — | Simplified example |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;## Batch:&lt;/code&gt; heading starts an independently reviewable section. The parser expects a defined set of required metadata fields and a fixed table column order. It treats contract violations such as wrong column counts, missing required metadata, unknown enum values, and duplicate IDs as hard errors. It does not attempt to validate unrelated Markdown outside the recognized batch structure.&lt;/p&gt;

&lt;p&gt;This is not a general-purpose Markdown parser. It splits lines, matches heading prefixes, extracts metadata key-value pairs, and walks table rows cell by cell. It knows nothing about bold text, links, or nested lists. The format contract is narrow enough that the parser can be strict.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgenwzj9j0235p5dmjvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgenwzj9j0235p5dmjvw.png" alt="Three content lanes showing only approved data entering the browser runtime pool" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The validation boundary
&lt;/h2&gt;

&lt;p&gt;After parsing, every entry is a fully typed object. But the typing alone does not enforce content rules. The parser applies layered checks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural rules the parser rejects immediately:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A batch heading that is not a valid kebab-case slug&lt;/li&gt;
&lt;li&gt;A duplicate batch ID or prompt ID within the file&lt;/li&gt;
&lt;li&gt;A table row with the wrong number of columns&lt;/li&gt;
&lt;li&gt;A difficulty or category value not in the defined enum&lt;/li&gt;
&lt;li&gt;A numeric rating outside the 1–5 range&lt;/li&gt;
&lt;li&gt;A prompt longer than four words&lt;/li&gt;
&lt;li&gt;A display term that collides with another prompt's term or alias after normalization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Status-gated rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A batch marked &lt;code&gt;approved&lt;/code&gt; must have &lt;code&gt;Reviewed by&lt;/code&gt; and &lt;code&gt;Reviewed on&lt;/code&gt; fields. The parser throws if they are absent.&lt;/li&gt;
&lt;li&gt;Only &lt;code&gt;approved&lt;/code&gt; batches pass through the status filter into the runtime pool. The contract also supports &lt;code&gt;draft&lt;/code&gt; and &lt;code&gt;retired&lt;/code&gt;; if those statuses are present, their batches are parsed and validated, but their prompts do not reach the application. The current checked library snapshot contains only approved batches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What the parser does not verify:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the named reviewer is a real person&lt;/li&gt;
&lt;li&gt;Whether the review date is truthful&lt;/li&gt;
&lt;li&gt;Whether the source URL actually responds&lt;/li&gt;
&lt;li&gt;Whether the license claim is legally accurate&lt;/li&gt;
&lt;li&gt;Whether the difficulty assignment is editorially correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are human judgment calls. The parser enforces that required metadata exists and, where implemented, satisfies basic shape checks. Its semantic accuracy depends on the review process, not the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests as the second enforcement layer
&lt;/h2&gt;

&lt;p&gt;The parser throws when the data module is evaluated with structurally invalid Markdown. An explicit validation command is what turns that behavior into a pre-release gate in this project: repository policy requires &lt;code&gt;pnpm words:check&lt;/code&gt; before a commit. That is not a claim that hosted CI runs the check or that every &lt;code&gt;vite build&lt;/code&gt; necessarily evaluates the parser.&lt;/p&gt;

&lt;p&gt;A test suite adds checks that the parser alone cannot express:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The exact number of approved prompts matches an expected count. A mismatch makes an inventory change explicit instead of proving by itself that the change was unauthorized.&lt;/li&gt;
&lt;li&gt;Every difficulty-and-category combination has a minimum number of entries. This prevents shipping a filter combination that would return zero results at runtime.&lt;/li&gt;
&lt;li&gt;Prompt terms and aliases are globally unique after Unicode normalization. The parser enforces uniqueness while traversing the full file, and the test independently asserts the property across the resulting pool.&lt;/li&gt;
&lt;li&gt;Unit tests assert selected review versions, reviewer fields, source text, and license prefixes for imported batches. A separate Quick Draw check validates the pinned repository, source commit, recorded SHA-256, license identifier, row counts, and review decisions in the review artifact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An optional networked command, &lt;code&gt;words:quickdraw:verify-source&lt;/code&gt;, goes further: it re-fetches the pinned upstream files, verifies the categories file's SHA-256 hash, recomputes overlap with the existing library, and checks that the reviewed candidates still match the pinned snapshot. That network verification is separate from the default local check.&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;words:check&lt;/code&gt; command runs the local Quick Draw review validation and the prompt-library unit tests. It does not invoke the optional network verification. These are not integration or E2E tests; they operate on the review artifact and parsed Markdown data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approval filter in one line
&lt;/h2&gt;

&lt;p&gt;After all validation passes, the runtime export is brief:&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;const&lt;/span&gt; &lt;span class="nx"&gt;prompts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;batches&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&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;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a future library snapshot includes draft or retired batches, they can remain reviewable without entering the pool the application consumes. The current checked library snapshot contains only approved batches. The runtime boundary is still a one-line status check because the parser has already required the approved-batch metadata and constructed each prompt with the expected fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public claim consistency
&lt;/h2&gt;

&lt;p&gt;One subtle problem: the application displays a prompt count to users ("900+ reviewed prompts"). That number should reflect the approved pool, not a separate marketing estimate. A utility function converts the loaded count to a floored magnitude (&lt;code&gt;969&lt;/code&gt; becomes &lt;code&gt;"900+"&lt;/code&gt;, &lt;code&gt;330&lt;/code&gt; becomes &lt;code&gt;"300+"&lt;/code&gt;). Dedicated tests verify the formatter, and the current homepage derives this display from &lt;code&gt;prompts.length&lt;/code&gt; when it renders. For that surface, the floored magnitude does not exceed the loaded approved count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this works and where it does not
&lt;/h2&gt;

&lt;p&gt;This pipeline works when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The dataset is bounded (hundreds to low thousands of entries)&lt;/li&gt;
&lt;li&gt;Content changes at editorial pace, not user pace&lt;/li&gt;
&lt;li&gt;Reviews happen through version-controlled changes&lt;/li&gt;
&lt;li&gt;The data ships with the application bundle&lt;/li&gt;
&lt;li&gt;You control the schema and can enforce it with a custom parser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It starts to break when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The dataset exceeds what you want in a client bundle&lt;/li&gt;
&lt;li&gt;Non-technical editors need a form-based interface&lt;/li&gt;
&lt;li&gt;Content updates must deploy independently of code&lt;/li&gt;
&lt;li&gt;Multiple writers need concurrent editing with conflict resolution&lt;/li&gt;
&lt;li&gt;You need row-level permissions or approval workflows more complex than a batch status field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those cases, a database or CMS is the right tool. The Markdown approach is not a general CMS replacement—it is a specific solution for datasets that live at the intersection of "needs human review" and "ships as application code."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8y1sfhs5emphvgmzw33f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8y1sfhs5emphvgmzw33f.png" alt="Human editorial review working with automated validation on a structured dataset" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Version history is a workflow layer
&lt;/h2&gt;

&lt;p&gt;When content changes are committed, Git history can preserve who changed the file and when. Separate review documents can record reviewer decisions, exclusion reasons, and source attribution. Together they provide useful evidence, but only for changes the workflow actually records.&lt;/p&gt;

&lt;p&gt;Project policy requires approved-content changes to remain auditable. Batch metadata can carry a &lt;code&gt;Revisions&lt;/code&gt; entry recording who changed what, when, and why. The parser checks that revision entries are well-formed when present, but it does not require one on every edit. Git history and review discipline remain workflow controls rather than parser guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live case
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://pictionarywordgenerator.io/" rel="noopener noreferrer"&gt;Pictionary Word Generator&lt;/a&gt;, a free browser-based prompt generator. The content pipeline described here is how its 900+ reviewed prompts—across three difficulty levels and seven categories—are maintained, reviewed, and delivered to the client without a database or CMS. The production repository is private due to licensing constraints; the examples above are simplified and independently written.&lt;/p&gt;

&lt;p&gt;Two companion guides cover different parts of the system: one on &lt;a href="https://github.com/zhengguge06-code/pictionary-word-generator/blob/main/docs/filtered-no-repeat-random-picker.md" rel="noopener noreferrer"&gt;building a filtered no-repeat random picker&lt;/a&gt;, and another on &lt;a href="https://github.com/zhengguge06-code/pictionary-word-generator/blob/main/docs/no-repeat-round-engine-state-machine.md" rel="noopener noreferrer"&gt;designing the round state machine&lt;/a&gt; that coordinates filters, history, and timer. This article covers what happens before the picker runs: how content enters the system and earns the right to be selected.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I built the Pictionary Word Generator used as the live case. This article was drafted with AI assistance and independently fact-checked against the current source code, tests, and live pages.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Build a React Countdown Timer That Doesn't Drift in Background Tabs</title>
      <dc:creator>Tom Ricky</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:39:11 +0000</pubDate>
      <link>https://dev.to/zhengguge06/build-a-react-countdown-timer-that-doesnt-drift-in-background-tabs-14e1</link>
      <guid>https://dev.to/zhengguge06/build-a-react-countdown-timer-that-doesnt-drift-in-background-tabs-14e1</guid>
      <description>&lt;p&gt;Most countdown timer tutorials accumulate ticks: subtract one second every time &lt;code&gt;setInterval&lt;/code&gt; fires. That works until the browser throttles your tab. This article builds a timer that derives remaining time from a wall-clock deadline, so backgrounding the tab for thirty seconds doesn't leave the display thirty seconds behind reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the drift problem
&lt;/h2&gt;

&lt;p&gt;Here's the minimal broken version:&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="c1"&gt;// Don't ship this.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useBrokenTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRemaining&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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;setRemaining&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;remaining&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;Open a tab running this timer, switch to another tab, wait 20 seconds, switch back. The display might show 15 seconds have passed because the browser reduced the interval frequency while the tab was inactive. Chrome, Firefox, and Safari all throttle background timers — the exact behavior varies across browsers and versions, so you can't predict how much drift you'll get.&lt;/p&gt;

&lt;p&gt;The root cause: the timer's state is the accumulated count of callbacks, but callbacks don't fire at guaranteed intervals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the timer state
&lt;/h2&gt;

&lt;p&gt;Replace the single number with a state machine:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TimerPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&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="s1"&gt;running&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="s1"&gt;paused&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="s1"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TimerState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TimerPhase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// wall-clock deadline while running&lt;/span&gt;
  &lt;span class="nl"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// frozen value while idle, paused, or finished&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four phases, each with a clear meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;idle&lt;/strong&gt;: not started yet. &lt;code&gt;remainingMs&lt;/code&gt; holds the full duration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;running&lt;/strong&gt;: counting down. &lt;code&gt;endsAt&lt;/code&gt; holds the deadline; remaining is computed live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;paused&lt;/strong&gt;: stopped mid-count. &lt;code&gt;remainingMs&lt;/code&gt; holds the frozen remainder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;finished&lt;/strong&gt;: hit zero. &lt;code&gt;remainingMs&lt;/code&gt; is 0.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Map transitions before writing callbacks
&lt;/h2&gt;

&lt;p&gt;Treat the timer as a small state machine rather than a collection of unrelated buttons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle ---- start ----&amp;gt; running ---- reaches zero ----&amp;gt; finished
  ^                     |   ^                           |
  |                     |   |                           |
  +------ reset --------+   +------ start -------------+
                        |
                      pause
                        |
                        v
                      paused ---- start ----&amp;gt; running
                        |
                        +-------- reset -----&amp;gt; idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This map prevents ambiguous behavior. &lt;code&gt;Start&lt;/code&gt; while running is a no-op. &lt;code&gt;Start&lt;/code&gt; while paused means resume. &lt;code&gt;Start&lt;/code&gt; after completion begins a new full round. &lt;code&gt;Reset&lt;/code&gt; always returns to an idle full-duration timer and never starts it. Writing these rules down first also gives you a compact test plan before React state or browser scheduling enters the picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store a deadline while running
&lt;/h3&gt;

&lt;p&gt;When the timer is running, the source of truth is &lt;code&gt;endsAt&lt;/code&gt;. Remaining time at any moment is:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remainingMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endsAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No matter how many ticks the interval missed, this expression reflects elapsed wall-clock time at the moment of rendering, subject to the system-clock boundary discussed below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store a duration while paused
&lt;/h3&gt;

&lt;p&gt;Pause captures the current difference:&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="c1"&gt;// Pause handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frozen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endsAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frozen&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resume creates a fresh deadline from the frozen duration:&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="c1"&gt;// Resume handler&lt;/span&gt;
&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remainingMs&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Let the interval repaint, not count
&lt;/h2&gt;

&lt;p&gt;The interval's only job is forcing React to re-render so the display updates. It doesn't carry state:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;forceTick&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;forceTick&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;200&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;200ms gives smooth-looking seconds without excessive renders. The display uses &lt;code&gt;Math.ceil(remainingMs / 1000)&lt;/code&gt; so it reads &lt;code&gt;01:00&lt;/code&gt; at the start and ticks down to &lt;code&gt;00:01&lt;/code&gt; before finishing — never shows a misleading &lt;code&gt;00:00&lt;/code&gt; while time is still technically remaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid three React lifecycle traps
&lt;/h2&gt;

&lt;p&gt;The deadline solves elapsed-time accuracy, but the hook can still misbehave if its effects and callbacks are careless.&lt;/p&gt;

&lt;p&gt;First, create the repaint interval only while the timer is running. The effect above depends on &lt;code&gt;state.phase&lt;/code&gt;, so React clears the old interval whenever the phase changes and again when the component unmounts. This cleanup is especially important during development with React Strict Mode, where effects may be mounted, cleaned up, and mounted again to expose unsafe behavior. A missing cleanup can leave two repaint loops active even though the deadline math itself is correct.&lt;/p&gt;

&lt;p&gt;Second, use functional state updates for transitions that depend on the previous state. &lt;code&gt;setState((current) =&amp;gt; ...)&lt;/code&gt; gives rapid Start or Pause interactions the most recent queued value. Reading a captured &lt;code&gt;state&lt;/code&gt; inside a memoized callback can apply a transition to an older render and produce surprising pause/resume behavior.&lt;/p&gt;

&lt;p&gt;Third, keep the deadline out of the effect dependency list. The interval is not responsible for creating or changing the deadline; it only requests another render. Recreating the interval whenever &lt;code&gt;endsAt&lt;/code&gt; changes adds work without improving correctness. The dependency should describe the resource's lifetime: running means the repaint loop exists, every other phase means it does not.&lt;/p&gt;

&lt;p&gt;These rules also make the hook easier to inspect. There is one piece of persistent timer state, one short-lived repaint resource, and explicit transition callbacks. If the displayed time is wrong, you can ask whether the deadline is wrong, whether a transition stored the wrong frozen duration, or whether the view simply has not repainted yet. Those are much clearer failure categories than “the interval got weird.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Recompute when visibility changes
&lt;/h2&gt;

&lt;p&gt;When the user returns to the tab, you want the display to snap to the correct time immediately rather than waiting up to 200ms for the next interval:&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onVisible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;forceTick&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onVisible&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onVisible&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single forced render on &lt;code&gt;visibilitychange&lt;/code&gt; does the job. The computation is already &lt;code&gt;endsAt - Date.now()&lt;/code&gt;, so no additional logic is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement start, pause, reset, and finish
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DURATION_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&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;setState&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;current&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;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;DURATION_MS&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remainingMs&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="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;duration&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="p"&gt;[]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&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;setState&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;current&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;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;current&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="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endsAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&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;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DURATION_MS&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transition from &lt;code&gt;running&lt;/code&gt; to &lt;code&gt;finished&lt;/code&gt; happens in an effect that watches &lt;code&gt;remainingMs&lt;/code&gt;:&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="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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;remainingMs&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starting from &lt;code&gt;finished&lt;/code&gt; resets to the full duration automatically.&lt;/li&gt;
&lt;li&gt;Starting from &lt;code&gt;paused&lt;/code&gt; resumes from the frozen remainder.&lt;/li&gt;
&lt;li&gt;Reset always returns to &lt;code&gt;idle&lt;/code&gt; at the full duration without starting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Integrate the timer with a round flow
&lt;/h2&gt;

&lt;p&gt;In the Pictionary tool where I use this hook, generating a new word calls &lt;code&gt;reset()&lt;/code&gt; — the clock returns to 01:00 but does not auto-start. The host presses Start when the room is ready.&lt;/p&gt;

&lt;p&gt;This is a product decision, not a React limitation. Auto-starting would mean every accidental tap on "next word" immediately begins a countdown. Keeping Start manual gives the host control over pacing.&lt;/p&gt;

&lt;p&gt;Similarly, changing the difficulty or category filter resets the timer. The host is adjusting the round setup, so the previous countdown is no longer relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the edge cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Background tab for 45s during a 60s countdown&lt;/td&gt;
&lt;td&gt;Returning shows ~15s remaining, not ~55s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rapid pause/resume&lt;/td&gt;
&lt;td&gt;Frozen duration follows the latest deadline within a small timing tolerance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start after finished&lt;/td&gt;
&lt;td&gt;Restores full duration, doesn't resume from 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmount during running&lt;/td&gt;
&lt;td&gt;Interval is cleared, no state updates on unmounted component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System clock jump (e.g., laptop sleep/wake)&lt;/td&gt;
&lt;td&gt;Timer may show unexpected values — this is a known boundary, not a solved case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple rapid Start clicks&lt;/td&gt;
&lt;td&gt;Idempotent — second click is a no-op because phase is already &lt;code&gt;running&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note on the clock-jump case: &lt;code&gt;Date.now()&lt;/code&gt; reflects the system clock, so a laptop sleeping for 10 minutes and waking will show the timer as finished (since &lt;code&gt;endsAt&lt;/code&gt; is now far in the past). This is generally the correct behavior for a game timer — if you left mid-round, the round is over. But it's worth documenting as a boundary rather than claiming "zero drift under all conditions."&lt;/p&gt;

&lt;h2&gt;
  
  
  Be precise about what “doesn't drift” means
&lt;/h2&gt;

&lt;p&gt;This design corrects &lt;strong&gt;callback-scheduling drift&lt;/strong&gt;. If the browser delays ten interval callbacks, the next render still calculates against the original deadline instead of pretending only one second passed. It does not make &lt;code&gt;setInterval&lt;/code&gt; precise, guarantee a repaint at an exact millisecond, or protect against a user manually changing the system clock.&lt;/p&gt;

&lt;p&gt;The distinction matters in tests. Avoid asserting that a callback fires at exactly 200ms; that is controlled by the runtime. Assert that, given an &lt;code&gt;endsAt&lt;/code&gt; value and a later clock reading, the displayed remainder is derived from their difference. For end-to-end coverage, background the page long enough to trigger throttling, return, and allow a small tolerance around the expected number of seconds. That tests the behavior users care about without turning browser scheduling into a brittle benchmark.&lt;/p&gt;

&lt;p&gt;If your application needs monotonic elapsed-time measurement rather than a wall-clock deadline, investigate &lt;code&gt;performance.now()&lt;/code&gt;. It has different semantics around navigation, sleep, and persistence, so it is not a drop-in improvement for every countdown. For this one-session game timer, &lt;code&gt;Date.now()&lt;/code&gt; makes the intended “the round kept elapsing while you were away” behavior explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format the display
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatClock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainingMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalSeconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalSeconds&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;&lt;code&gt;Math.ceil&lt;/code&gt; ensures the display shows &lt;code&gt;00:01&lt;/code&gt; for the final partial second and transitions to &lt;code&gt;00:00&lt;/code&gt; only when truly finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the real interaction
&lt;/h2&gt;

&lt;p&gt;I built a Pictionary round tool that uses this exact timer pattern. You can &lt;a href="https://pictionarywordgenerator.io/" rel="noopener noreferrer"&gt;try the 60-second round timer in context&lt;/a&gt; — generate a word, start the timer, background the tab, and return to see the corrected display.&lt;/p&gt;

&lt;p&gt;The timer is fixed at 60 seconds. It does not offer configurable duration — that's a deliberate constraint for this particular tool, not a limitation of the pattern described here.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>tutorial</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Was Tired of My Discord Server Asking “What Time Is That for Me?” — Here’s What Fixed It</title>
      <dc:creator>Tom Ricky</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:06:14 +0000</pubDate>
      <link>https://dev.to/zhengguge06/i-was-tired-of-my-discord-server-asking-what-time-is-that-for-me-heres-what-fixed-it-105d</link>
      <guid>https://dev.to/zhengguge06/i-was-tired-of-my-discord-server-asking-what-time-is-that-for-me-heres-what-fixed-it-105d</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7zva1zik7bbh58vn96h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7zva1zik7bbh58vn96h.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It happened again.&lt;/p&gt;

&lt;p&gt;I posted the raid time for our 500-member gaming Discord. Within minutes, the replies flooded in:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What time is that in UK?”&lt;br&gt;
“Is that Saturday or Sunday for me?”&lt;br&gt;
“Wait, is EST the same as EDT right now?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’d included three timezone conversions in the original post. Didn’t matter. Half the team still got it wrong, and we started the raid with six people missing.&lt;/p&gt;

&lt;p&gt;There had to be a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution Hiding in Plain Sight
&lt;/h2&gt;

&lt;p&gt;Turns out, Discord has a feature most people don’t know about: &lt;strong&gt;timestamps that automatically convert to each viewer’s timezone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You type this once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event starts &amp;lt;t:1722729600:F&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Discord shows everyone &lt;strong&gt;their&lt;/strong&gt; local time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New York sees: &lt;em&gt;Saturday, August 3, 2024 at 9:00 PM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Tokyo sees: &lt;em&gt;Sunday, August 4, 2024 at 10:00 AM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;London sees: &lt;em&gt;Sunday, August 4, 2024 at 2:00 AM&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same moment. Zero confusion. No more “what time is that for me?”&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed After We Started Using This
&lt;/h2&gt;

&lt;p&gt;The difference was immediate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before timestamps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15+ timezone questions per event announcement&lt;/li&gt;
&lt;li&gt;20% of people missed events due to conversion mistakes&lt;/li&gt;
&lt;li&gt;Arguments every daylight saving time change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After timestamps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maybe 1 question per month&lt;/li&gt;
&lt;li&gt;Event attendance up 35%&lt;/li&gt;
&lt;li&gt;Zero DST drama&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? It takes less time to create a timestamp than to type out three timezone conversions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Use Discord Timestamps
&lt;/h2&gt;

&lt;p&gt;There are two ways to do this. I’ll show you both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: Online Converter (My Go-To)
&lt;/h3&gt;

&lt;p&gt;This works on desktop and mobile, and you can see all nine timestamp styles at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Open a timestamp converter&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://timestamp.rocks/" rel="noopener noreferrer"&gt;Timestamp Rocks&lt;/a&gt; because it’s free, has no ads, and works on phones. (Full disclosure: I built it because I got tired of the alternatives.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frurfvf8kptzup0qehcsb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frurfvf8kptzup0qehcsb.png" alt=" " width="799" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Enter your event details&lt;/p&gt;

&lt;p&gt;Pick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The date&lt;/li&gt;
&lt;li&gt;The time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The timezone where the event happens&lt;/strong&gt; (not the viewer’s timezone!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your event is at 9 PM in New York, choose &lt;code&gt;America/New_York&lt;/code&gt;. Don’t guess between EST and EDT — the timezone handles that automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Copy the code&lt;/p&gt;

&lt;p&gt;The converter generates all nine Discord timestamp styles. For most events, I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;F&lt;/code&gt; — Full date with weekday (&lt;em&gt;Sunday, August 3, 2024 at 9:00 PM&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R&lt;/code&gt; — Countdown (&lt;em&gt;in 7 days&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; — Short time (&lt;em&gt;9:00 PM&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Copy&lt;/strong&gt; next to the format you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Paste into Discord&lt;/p&gt;

&lt;p&gt;Paste the code as normal message text — no special formatting needed.&lt;/p&gt;

&lt;p&gt;The code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;t:1722729600:F&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86vbax5k57g1rkbp4gh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86vbax5k57g1rkbp4gh9.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep the angle brackets, colons, and style letter. Discord needs the exact syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Discord’s Built-In Command (Desktop Only)
&lt;/h3&gt;

&lt;p&gt;Discord desktop has a shortcut:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;@time&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enter something like &lt;code&gt;tomorrow at 7:30 PM&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pick a format&lt;/li&gt;
&lt;li&gt;Send&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiw7eborn1eh6apl5d02b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiw7eborn1eh6apl5d02b.png" alt=" " width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only works on desktop (not mobile)&lt;/li&gt;
&lt;li&gt;Doesn’t let you specify a timezone&lt;/li&gt;
&lt;li&gt;Can’t compare all nine styles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still use the converter most of the time because I schedule events weeks in advance and need precise timezone control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nine Timestamp Styles (And When to Use Each)
&lt;/h2&gt;

&lt;p&gt;Discord gives you nine ways to display the same moment. Here’s when I use each:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For event announcements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;F&lt;/code&gt; — &lt;em&gt;Sunday, August 3, 2024 at 9:00 PM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Includes everything: weekday, date, time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For countdowns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;R&lt;/code&gt; — &lt;em&gt;in 7 days&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Updates automatically as time passes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For short reminders:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; — &lt;em&gt;9:00 PM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;When the date is already clear from context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For logs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;S&lt;/code&gt; — &lt;em&gt;8/3/24, 9:00:00 PM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Compact with seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write on Medium&lt;br&gt;
The full list:&lt;/p&gt;

&lt;p&gt;| Style | Display | When to Use |&lt;br&gt;
| — — — -| — — — — -| — — — — — — -|&lt;br&gt;
| &lt;code&gt;t&lt;/code&gt; | 9:00 PM | Short time |&lt;br&gt;
| &lt;code&gt;T&lt;/code&gt; | 9:00:00 PM | Time with seconds |&lt;br&gt;
| &lt;code&gt;d&lt;/code&gt; | 08/03/2024 | Short date |&lt;br&gt;
| &lt;code&gt;D&lt;/code&gt; | August 3, 2024 | Long date |&lt;br&gt;
| &lt;code&gt;f&lt;/code&gt; | August 3, 2024 at 9:00 PM | Date and time (default) |&lt;br&gt;
| &lt;code&gt;F&lt;/code&gt; | Sunday, August 3, 2024 at 9:00 PM | Full |&lt;br&gt;
| &lt;code&gt;R&lt;/code&gt; | in 7 days | Relative countdown |&lt;/p&gt;
&lt;h2&gt;
  
  
  Real Examples from My Server
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Game Raid Announcement
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🏰 Mythic Raid Night

📅 When: &amp;lt;t:1722729600:F&amp;gt;
⏰ Invites: &amp;lt;t:1722729600:R&amp;gt;

Be online 15 minutes early!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Everyone sees their local time. No one asks “what time is that for me?” anymore.&lt;/p&gt;
&lt;h3&gt;
  
  
  Streaming Schedule
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🎮 Going live for a 24-hour charity stream!

Starts: &amp;lt;t:1722729600:F&amp;gt; (&amp;lt;t:1722729600:R&amp;gt;)

Set a reminder — see you &amp;lt;t:1722729600:t&amp;gt;!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The countdown updates automatically. Viewers in any timezone know exactly when to tune in.&lt;/p&gt;
&lt;h3&gt;
  
  
  Moderation Log
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TIMEOUT] User @SpamBot timed out
• Action: &amp;lt;t:1722729600:S&amp;gt;
• Expires: &amp;lt;t:1722815999:S&amp;gt;
• Reason: Spam after warning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;S&lt;/code&gt; style keeps logs compact while preserving seconds for debugging.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mistakes I Made (So You Don’t Have To)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Mistake #1: Using milliseconds instead of seconds
&lt;/h3&gt;

&lt;p&gt;JavaScript’s &lt;code&gt;Date.now()&lt;/code&gt; returns milliseconds. Discord needs &lt;strong&gt;seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re generating timestamps in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Correct&lt;/span&gt;
&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Wrong — shows year 2286&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake #2: Putting timestamps in backticks
&lt;/h3&gt;

&lt;p&gt;Backticks make Discord show raw code. Send timestamps as normal text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Event at &amp;lt;t:1722729600:F&amp;gt;
❌ Event at `&amp;lt;t:1722729600:F&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake #3: Choosing the viewer’s timezone
&lt;/h3&gt;

&lt;p&gt;When you generate the code, choose &lt;strong&gt;where the event happens&lt;/strong&gt;, not where people are watching from.&lt;/p&gt;

&lt;p&gt;If your event is at 9 PM in New York, use &lt;code&gt;America/New_York&lt;/code&gt;. Discord handles everyone else’s timezone automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake #4: Forgetting about mobile
&lt;/h3&gt;

&lt;p&gt;Discord’s &lt;code&gt;@time&lt;/code&gt; command doesn’t work on phones. If your community uses mobile, teach them the converter method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reuse timestamps across messages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generate the code once, then paste it into multiple announcements, reminders, and pinned posts. They all stay synchronized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combine formats for clarity&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event: &amp;lt;t:1722729600:D&amp;gt; at &amp;lt;t:1722729600:t&amp;gt;
Countdown: &amp;lt;t:1722729600:R&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gives both a calendar reference and a countdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use in bot commands&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most Discord bots accept &lt;code&gt;&amp;lt;t:…&amp;gt;&lt;/code&gt; syntax directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/schedule post channel:#events time:&amp;lt;t:1722729600&amp;gt; message:Event starts!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;Timezone confusion isn’t just annoying — it costs you attendance.&lt;/p&gt;

&lt;p&gt;Before we used timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20% of people missed events&lt;/li&gt;
&lt;li&gt;Most said they “got the time wrong”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event attendance up 35%&lt;/li&gt;
&lt;li&gt;Zero timezone-related no-shows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run a global Discord community — gaming clan, open-source project, study group, anything — this one change will save you hours of explaining timezones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Next time you schedule a Discord event:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://timestamp.rocks/" rel="noopener noreferrer"&gt;Timestamp Rocks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Enter the event time and timezone&lt;/li&gt;
&lt;li&gt;Copy the &lt;code&gt;F&lt;/code&gt; format&lt;/li&gt;
&lt;li&gt;Paste it into your announcement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watch the timezone questions disappear.&lt;/p&gt;

&lt;p&gt;— -&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to see all nine timestamp styles at once?&lt;/strong&gt; Try the &lt;a href="https://timestamp.rocks/" rel="noopener noreferrer"&gt;Discord Timestamp Converter&lt;/a&gt; — it’s free, works on mobile, and shows you exactly what each format looks like before you paste it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://timestamp.rocks/blog/how-to-use-discord-timestamps" rel="noopener noreferrer"&gt;timestamp.rocks&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Use Discord Timestamps (and Stop Confusing Your Global Team)</title>
      <dc:creator>Tom Ricky</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:21:52 +0000</pubDate>
      <link>https://dev.to/zhengguge06/how-to-use-discord-timestamps-and-stop-confusing-your-global-team-46n4</link>
      <guid>https://dev.to/zhengguge06/how-to-use-discord-timestamps-and-stop-confusing-your-global-team-46n4</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: "What time is 9 PM EST for me?"
&lt;/h2&gt;

&lt;p&gt;I manage a Discord server with 500+ members across six continents. Every single event announcement used to trigger the same flood of questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What time is that in UK?"&lt;br&gt;
"Is EST the same as EDT right now?"&lt;br&gt;
"Wait, is that Saturday for me or Sunday?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried typing out timezone conversions. I tried pinning a world clock link. Nothing worked—until I discovered Discord timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Discord Timestamps?
&lt;/h2&gt;

&lt;p&gt;Discord timestamps are special codes that automatically convert to each viewer's local time. You paste something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event starts &amp;lt;t:1722729600:F&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And everyone sees &lt;strong&gt;their&lt;/strong&gt; timezone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone in New York sees: &lt;em&gt;Saturday, August 3, 2024 at 9:00 PM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Someone in Tokyo sees: &lt;em&gt;Sunday, August 4, 2024 at 10:00 AM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Someone in London sees: &lt;em&gt;Sunday, August 4, 2024 at 2:00 AM&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same moment. Zero confusion. No more "what time is that for me?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Changed Everything for My Server
&lt;/h2&gt;

&lt;p&gt;After switching to timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;95% drop&lt;/strong&gt; in timezone questions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event attendance up 35%&lt;/strong&gt; (people stopped missing events due to conversion mistakes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero drama&lt;/strong&gt; about daylight saving time changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a real example from our raid announcements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📢 Raid Saturday 9 PM EST
(That's 2 AM Sunday GMT, 11 AM Sunday AEST)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Half the team still got it wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📢 Raid &amp;lt;t:1722729600:F&amp;gt; (&amp;lt;t:1722729600:R&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Everyone shows up on time.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Create Discord Timestamps
&lt;/h2&gt;

&lt;p&gt;There are two methods. Pick whichever fits your workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: Online Converter (Works on Mobile!)
&lt;/h3&gt;

&lt;p&gt;This is what I use 90% of the time because it works on phones and lets you compare all nine timestamp styles.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open a timestamp converter&lt;/strong&gt; (I built &lt;a href="https://timestamp.rocks/" rel="noopener noreferrer"&gt;Timestamp Rocks&lt;/a&gt; for this—totally free, no ads)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Select your event details:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;li&gt;Time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The timezone where the event is scheduled&lt;/strong&gt; (not the viewer's timezone!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Copy the format you need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;F&lt;/code&gt; = Full date with weekday (&lt;em&gt;Sunday, August 3, 2024 at 9:00 PM&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R&lt;/code&gt; = Countdown (&lt;em&gt;in 7 days&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; = Short time (&lt;em&gt;9:00 PM&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Paste into Discord&lt;/strong&gt; as normal text and send&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfeyg4hlg7s9v48sro1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfeyg4hlg7s9v48sro1o.png" alt="Timestamp converter showing date selector and generated codes" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;t:1722729600:F&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, colons, and style letter—Discord needs the exact syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Discord's &lt;code&gt;@time&lt;/code&gt; Command (Desktop Only)
&lt;/h3&gt;

&lt;p&gt;Discord desktop has a built-in timestamp creator:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;@time&lt;/code&gt; in any message box&lt;/li&gt;
&lt;li&gt;Enter natural language like &lt;code&gt;tomorrow at 7:30 PM&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pick a format from Discord's suggestions&lt;/li&gt;
&lt;li&gt;Send&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa7a9b2cqyh38rf8i0fev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa7a9b2cqyh38rf8i0fev.png" alt="Discord desktop showing @time command" width="799" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop only (doesn't work on mobile)&lt;/li&gt;
&lt;li&gt;Limited timezone control&lt;/li&gt;
&lt;li&gt;Can't compare all nine styles side-by-side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still use the converter method most of the time, especially when I need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create timestamps on my phone&lt;/li&gt;
&lt;li&gt;Specify a precise timezone (like &lt;code&gt;America/New_York&lt;/code&gt; for automatic DST handling)&lt;/li&gt;
&lt;li&gt;Copy the raw code into a bot command&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Examples from My Server
&lt;/h2&gt;

&lt;p&gt;Here's how I actually use timestamps:&lt;/p&gt;

&lt;h3&gt;
  
  
  🎮 Weekly Game Night
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Game Night! &amp;lt;t:1722729600:F&amp;gt;

Starting &amp;lt;t:1722729600:R&amp;gt; with viewer challenges.
Drop in any time after &amp;lt;t:1722729600:t&amp;gt;!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;F&lt;/code&gt; gives the full context, &lt;code&gt;R&lt;/code&gt; creates urgency, and &lt;code&gt;t&lt;/code&gt; keeps the closing line clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  📅 Project Deadline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔒 Feature freeze: &amp;lt;t:1723248000:F&amp;gt;
Code review opens: &amp;lt;t:1723334400:F&amp;gt;
Release: &amp;lt;t:1723507200:F&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our open-source contributors span 12 timezones. Since switching to timestamps, we've had &lt;strong&gt;zero&lt;/strong&gt; late submissions from timezone confusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤖 Bot Logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TIMEOUT] User @SpamBot silenced
• Timeout applied: &amp;lt;t:1722470400:S&amp;gt;
• Expires: &amp;lt;t:1722556800:S&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;S&lt;/code&gt; style keeps logs compact while preserving seconds—crucial when debugging timing issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Tips I Wish I'd Known Sooner
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Combine Multiple Styles
&lt;/h3&gt;

&lt;p&gt;Don't just use one timestamp format. Layer them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📅 Date: &amp;lt;t:1722729600:D&amp;gt;
⏰ Time: &amp;lt;t:1722729600:t&amp;gt;
⏳ Countdown: &amp;lt;t:1722729600:R&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gives everyone the info they need at a glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use Timestamps in Bot Commands
&lt;/h3&gt;

&lt;p&gt;Most bots accept the &lt;code&gt;&amp;lt;t:...&amp;gt;&lt;/code&gt; syntax directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/schedule post channel:#announcements &lt;span class="nb"&gt;time&lt;/span&gt;:&amp;lt;t:1722729600&amp;gt; message:Event starting now!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bot reads the Unix timestamp and posts at the exact right moment for every timezone.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test Before Posting
&lt;/h3&gt;

&lt;p&gt;I always send timestamps to myself in a DM first. Catches mistakes before 500 people see them.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Don't Reuse Old Codes
&lt;/h3&gt;

&lt;p&gt;Generate a fresh timestamp for each new event. Reusing last week's code points to... last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes (and How to Fix Them)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Timestamp shows as raw text
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; &lt;code&gt;&amp;lt;t:1722729600:F&amp;gt;&lt;/code&gt; displays literally instead of rendering&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Check you didn't put it inside backticks or code blocks. It needs to be plain text.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Shows a date in 1970 or 2038+
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; You used milliseconds instead of seconds&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Discord needs Unix &lt;strong&gt;seconds&lt;/strong&gt; (10 digits). If you're using &lt;code&gt;Date.now()&lt;/code&gt; in JavaScript, divide by 1000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ❌ Everyone sees the wrong time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; You selected your viewer's timezone instead of the event's timezone&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; When creating the timestamp, pick the timezone where the event was &lt;strong&gt;scheduled&lt;/strong&gt;, not where you (or your viewers) are located.&lt;/p&gt;

&lt;h2&gt;
  
  
  All Nine Discord Timestamp Styles
&lt;/h2&gt;

&lt;p&gt;Quick reference for when you need a specific format:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9:34 PM&lt;/td&gt;
&lt;td&gt;Time only, date is obvious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9:34:00 PM&lt;/td&gt;
&lt;td&gt;Time with seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;08/03/2024&lt;/td&gt;
&lt;td&gt;Short numeric date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;August 3, 2024&lt;/td&gt;
&lt;td&gt;Long date, no weekday&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;August 3, 2024 9:34 PM&lt;/td&gt;
&lt;td&gt;Default (if you omit style)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Saturday, August 3, 2024 9:34 PM&lt;/td&gt;
&lt;td&gt;Full context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8/3/2024 9:34 PM&lt;/td&gt;
&lt;td&gt;Compact date + time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8/3/2024 9:34:00 PM&lt;/td&gt;
&lt;td&gt;Compact with seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;in 7 days&lt;/td&gt;
&lt;td&gt;Countdown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I use &lt;code&gt;F&lt;/code&gt; + &lt;code&gt;R&lt;/code&gt; for 80% of announcements. The combo gives both exact date and urgency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built a Timestamp Converter
&lt;/h2&gt;

&lt;p&gt;After manually assembling &lt;code&gt;&amp;lt;t:...&amp;gt;&lt;/code&gt; codes for months, I got tired of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Looking up Unix timestamp converters&lt;/li&gt;
&lt;li&gt;Manually typing style letters&lt;/li&gt;
&lt;li&gt;Not knowing which format would look best&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;a href="https://timestamp.rocks/" rel="noopener noreferrer"&gt;Timestamp Rocks&lt;/a&gt;—generates all nine Discord formats instantly, works on mobile, and handles timezones properly (including DST).&lt;/p&gt;

&lt;p&gt;It's free and open source. Built it for my team; sharing it with yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Next time you schedule a Discord event:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don't type "Saturday 9 PM EST"&lt;/li&gt;
&lt;li&gt;Generate a timestamp: &lt;code&gt;&amp;lt;t:1722729600:F&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Watch zero people ask "what time is that for me?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your community will thank you. Mine did.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you run into timezone chaos in your Discord server?&lt;/strong&gt; Drop your war stories in the comments—I'd love to hear how other server admins handle global scheduling.&lt;/p&gt;

&lt;p&gt;And if you try Discord timestamps, let me know which style format you end up using most!&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Try the timestamp converter:&lt;/strong&gt; &lt;a href="https://timestamp.rocks/" rel="noopener noreferrer"&gt;timestamp.rocks&lt;/a&gt;&lt;br&gt;
📖 &lt;strong&gt;Full technical docs:&lt;/strong&gt; &lt;a href="https://timestamp.rocks/blog/how-to-use-discord-timestamps" rel="noopener noreferrer"&gt;timestamp.rocks/blog/how-to-use-discord-timestamps&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>tutorial</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
