<?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: Izak T</title>
    <description>The latest articles on DEV Community by Izak T (@kickbuttowski80).</description>
    <link>https://dev.to/kickbuttowski80</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%2F98383%2Ff6294d6b-373a-4e86-8694-ca306979a3eb.jpeg</url>
      <title>DEV Community: Izak T</title>
      <link>https://dev.to/kickbuttowski80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kickbuttowski80"/>
    <language>en</language>
    <item>
      <title>I Was Arrested for Guarding Too Much: A Trial About the Null Check That Never Fired</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/i-was-arrested-for-guarding-too-much-a-trial-about-the-null-check-that-never-fired-42hf</link>
      <guid>https://dev.to/kickbuttowski80/i-was-arrested-for-guarding-too-much-a-trial-about-the-null-check-that-never-fired-42hf</guid>
      <description>&lt;p&gt;&lt;em&gt;The charge: three counts of over-guarding. The evidence: one ternary that had never run. I was not the hero of the trial that followed.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; A guard should test what can actually happen &lt;em&gt;at that layer&lt;/em&gt;. If the view already guarantees a value is non-null, checking it again in the script adds no safety — it adds noise, hides the real contract, and quietly turns bugs into wrong output. The ruling: guard at the boundary, once. Let interior code trust it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Arrest
&lt;/h2&gt;

&lt;p&gt;It happened over a schedule board. Two guards came for me — guards, yes, for a man who guards too much.&lt;/p&gt;

&lt;p&gt;The charge: &lt;strong&gt;over-guarding&lt;/strong&gt;. Three counts — which three, the paperwork didn't say. The evidence bag held only one exhibit — a Vue computed property, three lines long, the ternary bagged separately:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pickTechName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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;pickTarget&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;trayTechName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pickTarget&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="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="c1"&gt;//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this part&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It names the technician shown in the click-to-place banner on our schedule board. It also, per the arresting officer, stands guard against a null that could never reach it — a doorman paid to open a door that never gets a visitor.&lt;/p&gt;

&lt;p&gt;The charge sheet was signed by the prosecutor — a colleague who had read the diff:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Why do we check for null here when the view already guarantees it?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had no answer ready. The prosecution cannot compel a defendant to testify — so taking the stand was my counsel's last card: &lt;em&gt;"The defense calls the defendant."&lt;/em&gt; What follows is the transcript of my cross-examination.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit A: The scene of the alleged crime
&lt;/h2&gt;

&lt;p&gt;The computed has exactly one reader in the entire codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"pickTarget"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-semibold"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Placing "{{ pickTarget.title }}"&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"pickTarget.techId"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    — click an open hour on {{ pickTechName }}'s row
  &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the two &lt;code&gt;v-if&lt;/code&gt;s already standing at the door. The banner exists only while &lt;code&gt;pickTarget&lt;/code&gt; does; the hint, only when that job actually has a technician — an unassigned job is a real state here, not an exception. The computed is only ever read inside that block — after both checks have already passed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Examination, Round One
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prosecutor:&lt;/strong&gt; Why is that ternary there?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Me:&lt;/strong&gt; Because &lt;code&gt;pickTarget&lt;/code&gt; starts as &lt;code&gt;null&lt;/code&gt;. &lt;code&gt;trayTechName(null)&lt;/code&gt; would throw — it reads &lt;code&gt;job.techId&lt;/code&gt; off the argument. The guard prevents a crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prosecutor:&lt;/strong&gt; When is the computed &lt;em&gt;evaluated&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Me:&lt;/strong&gt; When it's read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prosecutor:&lt;/strong&gt; And it's read where?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Me:&lt;/strong&gt; Inside the &lt;code&gt;v-if="pickTarget"&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prosecutor:&lt;/strong&gt; So at the moment of evaluation, can &lt;code&gt;pickTarget&lt;/code&gt; be null?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The court reporter stopped typing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My counsel rose: "Objection — the question assumes facts that haven't been established."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Overruled.&lt;/em&gt; The fact was about to be established anyway. &lt;a href="https://vuejs.org/guide/essentials/computed" rel="noopener noreferrer"&gt;&lt;code&gt;computed&lt;/code&gt; in Vue&lt;/a&gt; is &lt;strong&gt;lazy&lt;/strong&gt;: the getter does not run at setup. It runs on first read — and the only read is inside a subtree that requires &lt;code&gt;pickTarget&lt;/code&gt; to exist. By the time my guard could fire, the condition it tests is already guaranteed false.&lt;/p&gt;

&lt;p&gt;My counsel leaned over and whispered: &lt;em&gt;"They have a point. Sit tight — let me see what else we have."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Examination, Round Two
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prosecutor:&lt;/strong&gt; Very well. It's redundant &lt;em&gt;today&lt;/em&gt;. But isn't it cheap insurance for the future? Someone might read it unguarded later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Me:&lt;/strong&gt; It's one comparison. It costs nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prosecutor:&lt;/strong&gt; I am not asking what it costs. I am asking what it &lt;em&gt;communicates&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My counsel tried again: "Objection — counsel is testifying."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Overruled.&lt;/em&gt; The prosecutor's point stood.&lt;/p&gt;

&lt;p&gt;That is the question I had been avoiding. A guard is not a neutral act. It is a claim. It says: &lt;strong&gt;"this value can be null at this point."&lt;/strong&gt; A reader arriving later now has to hunt for the scenario where that's true — and there is none. I had invented a hypothetical future reader to justify a check with no failure mode today.&lt;/p&gt;

&lt;p&gt;"Cheap" was never the argument. "Honest" was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recess: The Three Kinds of Guards
&lt;/h2&gt;

&lt;p&gt;During the recess, my counsel and I put together a table to explain our defense. Every guard is one of three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Ask yourself&lt;/th&gt;
&lt;th&gt;Where it goes&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boundary guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Can this outside data be wrong?"&lt;/td&gt;
&lt;td&gt;Only where data comes in from outside&lt;/td&gt;
&lt;td&gt;Reading &lt;code&gt;localStorage&lt;/code&gt;, an API, a form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Invariant guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Can my guess be wrong?"&lt;/td&gt;
&lt;td&gt;Inside, where it must shout if wrong&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;assert(items.length &amp;gt; 0)&lt;/code&gt; before indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redundant guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Is it already known?"&lt;/td&gt;
&lt;td&gt;Nowhere — someone already answered&lt;/td&gt;
&lt;td&gt;Re-checking what the view already guarantees&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same question at every layer — and only three answers: &lt;em&gt;yes&lt;/em&gt;, &lt;em&gt;never&lt;/em&gt; (and say it loudly), or &lt;em&gt;already answered&lt;/em&gt;. Only the first two need a check.&lt;/p&gt;

&lt;p&gt;A guard should test what can actually happen &lt;strong&gt;at that layer&lt;/strong&gt; — not re-test what a neighbor already guarantees.&lt;/p&gt;

&lt;p&gt;The prosecutor rose to answer our table. &lt;em&gt;"This distinction is not just opinion. &lt;a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2053r1.pdf" rel="noopener noreferrer"&gt;P2053R1&lt;/a&gt;, a C++ committee proposal by Rostislav Khlebnikov and John Lakos, draws the same line: a check is 'defensive' when its assumption is expected to always hold. Human input and remote clients violate assumptions — those need validation at the boundary, not in trusted interior code."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;(The full proposal, plus &lt;a href="https://isocpp.org/blog/2015/05/cppcon-2014-defensive-programming-done-right-part-i-john-lakos" rel="noopener noreferrer"&gt;Lakos's CppCon talk summary&lt;/a&gt; in plain HTML, cover the same ground.)&lt;/p&gt;

&lt;p&gt;My counsel leaned toward me. &lt;em&gt;"But what about spacecraft? A bit flipped by radiation can turn an impossible state into a real one — that's why NASA votes outputs across &lt;a href="https://ntrs.nasa.gov/citations/20190002742" rel="noopener noreferrer"&gt;triple-redundant avionics&lt;/a&gt;."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The prosecutor nodded. &lt;em&gt;"Warranted, there — hardware itself can corrupt state. But this is a Vue component, not flight-control software. The template reads this computed only after &lt;code&gt;pickTarget&lt;/code&gt; passes its &lt;code&gt;v-if&lt;/code&gt;. This ternary doesn't guard against physics; it repeats a software guarantee."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prosecution's Case: Three Counts of Over-Guarding
&lt;/h2&gt;

&lt;p&gt;When court resumed, the paperwork finally became specific. The prosecutor read out the three counts — and none of them concern performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count one: it lies about the contract.&lt;/strong&gt; The guard advertises a possibility that doesn't exist, taxing every future reader with a search for a phantom — after validation, later inconsistency is an internal error and should hard-fail (&lt;a href="https://blog.michael.gr/post/2020-05-30-on-validation-vs-error-checking/" rel="noopener noreferrer"&gt;Michael Belivanakis&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count two: it masks real bugs.&lt;/strong&gt; If the invariant ever &lt;em&gt;does&lt;/em&gt; break, the guard substitutes &lt;code&gt;''&lt;/code&gt; and renders a slightly wrong UI — instead of throwing a stack trace that points straight at the culprit. John Lakos's &lt;a href="https://isocpp.org/blog/2015/05/cppcon-2014-defensive-programming-done-right-part-i-john-lakos" rel="noopener noreferrer"&gt;CppCon talk summary&lt;/a&gt; puts it as near-consensus: &lt;em&gt;"Most would agree that a well-implemented library should do something other than silently continue when a pre-condition violation is detected."&lt;/em&gt; The pithier form comes from a heavily upvoted &lt;a href="https://softwareengineering.stackexchange.com/questions/373486/if-functions-have-to-do-null-checks-before-doing-the-intended-behaviour-is-this" rel="noopener noreferrer"&gt;Software Engineering Stack Exchange answer&lt;/a&gt;: "isn't the null check, it's the silent fail" — programmer errors are best dealt with by &lt;strong&gt;failing immediately and loudly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count three: it breeds.&lt;/strong&gt; Once neither caller nor callee can say who owns the check, every layer starts checking "just in case" — and the codebase fills with guards that guard nothing. This one has the best citation, and it comes from the man who invented the alternative.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Evidence: Precedent from 1988
&lt;/h2&gt;

&lt;p&gt;My counsel rose once more — keeping his promise from Round One to see what else we had. &lt;em&gt;"I went looking for precedent that might excuse a 'just in case' guard, Your Honor. What I found is older than my client — and it is not on our side. The defense offers no opinions. Only precedent."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bertrand Meyer's *Design by Contract&lt;/strong&gt;* — the contract theory behind Eiffel — rests on a rule he calls &lt;strong&gt;non-redundancy&lt;/strong&gt;. In &lt;a href="https://se.inf.ethz.ch/~meyer/publications/old/dbc_chapter.pdf" rel="noopener noreferrer"&gt;his own chapter on the subject&lt;/a&gt;, Meyer states it as an absolute rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Either you have the condition in the &lt;code&gt;require&lt;/code&gt;, or you have it in an &lt;code&gt;if&lt;/code&gt; instruction in the body of the routine, but &lt;strong&gt;never in both&lt;/strong&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And he names the cost: redundancy "accumulated over a large system" buries "islands of useful processing in oceans of error-checking code."&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://www.eiffel.org/doc/eiffel/ET-_Design_by_Contract_(tm),_Assertions_and_Exceptions" rel="noopener noreferrer"&gt;Eiffel documentation&lt;/a&gt; states it operationally — "a routine body should never test for the precondition, since it is the client's responsibility" — and records the counterintuitive payoff: &lt;em&gt;"one can get more reliable software by having fewer explicit checks in the software text."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And in &lt;a href="https://se.inf.ethz.ch/~meyer/publications/computer/contract.pdf" rel="noopener noreferrer"&gt;"Applying Design by Contract"&lt;/a&gt; (IEEE Computer), Meyer dismantles the exact argument I made on the stand — the "just in case" guard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Adding possibly redundant code 'just in case' only contributes to the software's complexity... The result of such blind checking is simply to introduce more software, hence more sources of things that could go wrong at execution time, hence the need for more checks, and so on &lt;em&gt;ad infinitum&lt;/em&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There it is: my "cheap insurance" answer, refuted in 1992 by the man who defined the contract. The court accepts the precedent. I had been arguing against case law I didn't know existed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Defense's Best Argument: "But what if a user edits localStorage?"
&lt;/h2&gt;

&lt;p&gt;My counsel played his last card — our strongest argument. &lt;em&gt;"What if a user opens devtools and hand-edits the stored JSON? Then the data genuinely can be wrong, and the guard is justified."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It deserved a real answer rather than a shrug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The threat model is wrong.&lt;/strong&gt; A dispatcher at a small business is not going to open devtools and hand-edit a JSON blob. If your safety depends on that being unlikely, you have larger problems than a null check — &lt;a href="https://instil.co/blog/defensive-checks" rel="noopener noreferrer"&gt;Instil's write-up&lt;/a&gt; is blunt that below the public boundary these checks are overhead meant for tests and review, not runtime guards.&lt;/p&gt;

&lt;p&gt;And this is the part I got wrong: the real reasons storage needs care have nothing to do with attackers. They are mundane — an earlier version of the app wrote a different shape, the storage filled and the write silently died, two browser tabs raced to write the same key, a user cleared their browsing data. None of that justifies guards sprinkled through the app. All of it justifies &lt;strong&gt;one&lt;/strong&gt; guard, at the read boundary, where bytes become objects.&lt;/p&gt;

&lt;p&gt;So the objection doesn't defeat the rule — it confirms it. Reading &lt;code&gt;localStorage&lt;/code&gt; is a boundary: data coming in from outside the app's control. And boundaries are guarded once, at the edge.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Court's Finding: Where the Guard Belongs in Vue
&lt;/h2&gt;

&lt;p&gt;Before the jury retired, the judge settled the one question the case actually turned on: in a Vue component, who owns the guarantee?&lt;/p&gt;

&lt;p&gt;There is a twist specific to Vue worth entering into the record: &lt;strong&gt;the template is a boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is not just convention — it is design. Vue's docs describe the single-file component as "colocating the view, logic and styling of a component" — one component, one shared contract. Which is why they live in one file: split them, and neither side sees the whole guarantee — how duplicate checks get born.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://thecodeforge.io/frontend/vue-undefined-in-template/" rel="noopener noreferrer"&gt;TheCodeForge's guide to the classic Vue template crash&lt;/a&gt; explains why: "Templates don't wait. That first render is where nearly every &lt;em&gt;Cannot read properties of undefined&lt;/em&gt; is born." Their fix is the same instinct: "fix at the data boundary in &lt;code&gt;setup()&lt;/code&gt;, not with scattered guards in every template."&lt;/p&gt;

&lt;p&gt;And the idiomatic Vue form is a &lt;strong&gt;container &lt;code&gt;v-if&lt;/code&gt;&lt;/strong&gt; — the pattern you already saw in Exhibit A. &lt;a href="https://soundscode.com/vue/vue-conditionals" rel="noopener noreferrer"&gt;SoundsCode's lesson&lt;/a&gt; says it plainly: the condition narrows the type for everything in that subtree.&lt;/p&gt;

&lt;p&gt;One condition. One owner. Compare that to sprinkling &lt;code&gt;pickTarget?.title&lt;/code&gt;, &lt;code&gt;pickTarget?.techId&lt;/code&gt;, and a ternary inside the computed — three checks, three places to drift, and no single line that states what is actually guaranteed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest footnote&lt;/strong&gt;, because a sharp reader will spot it: &lt;code&gt;pickTarget&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; read a second time outside that subtree — &lt;code&gt;:selected-job-id="pickTarget?.id || null"&lt;/code&gt;, handed to the unscheduled tray, which is always mounted. That optional chain is not the same species of check, and it should stay. There, &lt;code&gt;null&lt;/code&gt; is a &lt;strong&gt;legitimate value&lt;/strong&gt; — the tray renders whether or not anything is picked, and the child's prop is typed to receive &lt;code&gt;String | null&lt;/code&gt;. That is a boundary handoff, not a redundant guard.&lt;/p&gt;

&lt;p&gt;The rule was never "never use &lt;code&gt;?.&lt;/code&gt;." It is "use it where the value can actually be absent" — and the taxonomy above discriminating between two reads of the same ref is exactly the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Guard at the boundary, once. Let interior code trust it. Where you can, structure the code so the invalid state is impossible; where you cannot, fail loudly rather than substituting a value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The template's &lt;code&gt;v-if="pickTarget"&lt;/code&gt; is the owner. The computed should not re-litigate it. The corrected exhibit:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pickTechName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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;trayTechName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pickTarget&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If someone later reads it unguarded, it throws — loudly, at the exact line that is wrong. That is not a bug. That is the contract doing its job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fair objection:&lt;/strong&gt; a throw during render can blank the page in production. It is still the better trade — a silent &lt;code&gt;''&lt;/code&gt; hides the defect &lt;em&gt;forever&lt;/em&gt;; a throw surfaces it. "Fail loudly" was never "crash the customer." It means &lt;strong&gt;fail where you can see it&lt;/strong&gt;: the console in development, and an error boundary in production — Vue's &lt;a href="https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured" rel="noopener noreferrer"&gt;&lt;code&gt;onErrorCaptured&lt;/code&gt;&lt;/a&gt; or &lt;code&gt;app.config.errorHandler&lt;/code&gt;, which reports the bug and renders a real error state instead of a card full of blanks.&lt;/p&gt;

&lt;p&gt;The rule is not "never guard." It is "never &lt;em&gt;silently&lt;/em&gt; substitute."&lt;/p&gt;

&lt;p&gt;The jury was out for twenty minutes. When they filed back in, the foreman read the verdict to the judge.&lt;/p&gt;

&lt;p&gt;Guilty on all three counts. The guard is released from duty — it never worked a day in its life.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;The lesson was never about null checks. It is that my first two answers were &lt;em&gt;shaped&lt;/em&gt; like reasoning while being motivated reasoning — an instinct, then a hunt for justifications instead of facts. The prosecutor was right the entire time; the record only changed when I read the sources and noticed the check I had been defending had never once executed.&lt;/p&gt;

&lt;p&gt;The sentence was lighter than I deserved: one deleted ternary, and a paragraph in the project's decision log.&lt;/p&gt;

&lt;p&gt;A guard that guards nothing is cheap.&lt;/p&gt;

&lt;p&gt;Defending it in court is not.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>cleancode</category>
      <category>frontend</category>
    </item>
    <item>
      <title>The Case of the Missing `rem`: A Sherlock Holmes Guide to CSS Units in JavaScript</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Fri, 02 Oct 2026 02:19:48 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/the-case-of-the-missing-rem-a-sherlock-holmes-guide-to-css-units-in-javascript-3ij4</link>
      <guid>https://dev.to/kickbuttowski80/the-case-of-the-missing-rem-a-sherlock-holmes-guide-to-css-units-in-javascript-3ij4</guid>
      <description>&lt;p&gt;&lt;em&gt;Being the record of an investigation into CSS units, JavaScript geometry, and a very small missing value — as written by Dr. Watson.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Matter of No Consequence
&lt;/h2&gt;

&lt;p&gt;It began, as many of my friend's cases did, with a problem I thought too small to matter.&lt;/p&gt;

&lt;p&gt;"A dropdown," I said, handing Holmes the ticket. "It should sit &lt;code&gt;0.25rem&lt;/code&gt; below its button. An hour's work, no more."&lt;/p&gt;

&lt;p&gt;Holmes did not look up.&lt;/p&gt;

&lt;p&gt;"You find it trivial, Watson. Yet &lt;strong&gt;it has long been an axiom of mine that the little things are infinitely the most important&lt;/strong&gt;."&lt;/p&gt;

&lt;p&gt;He was right. He usually was — and he never let me forget it. That &lt;code&gt;0.25rem&lt;/code&gt; would lead us through the CSS value pipeline, the geometry of the browser, and to the border of JavaScript itself, where a unit had gone missing and nobody had reported it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Impossible Request
&lt;/h2&gt;

&lt;p&gt;The dropdown already used Floating UI — a small library that calculates where a floating panel should sit relative to its button. The positioning worked. We only needed to nudge the panel a little below its button.&lt;/p&gt;

&lt;p&gt;I wrote the natural thing:&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="nf"&gt;offset&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.25rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library refused it. Its &lt;a href="https://floating-ui.com/docs/offset" rel="noopener noreferrer"&gt;&lt;code&gt;offset()&lt;/code&gt; middleware&lt;/a&gt; expects a numeric distance:&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="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Ha!" I said. "The library is simply rude. JavaScript cannot understand relative units. Case closed, Holmes."&lt;/p&gt;

&lt;p&gt;Holmes looked up at last.&lt;/p&gt;

&lt;p&gt;"The documentation plainly says a number — that part you read correctly. But the design says &lt;code&gt;0.25rem&lt;/code&gt;. Knowing the API wants a number does not tell us &lt;em&gt;which&lt;/em&gt; number. That question is the whole case."&lt;/p&gt;

&lt;p&gt;He tapped the code with one finger.&lt;/p&gt;

&lt;p&gt;"Look closer. The number it wants is bare — no unit attached. Its geometry runs in CSS pixels, the same unit &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect" rel="noopener noreferrer"&gt;&lt;code&gt;getBoundingClientRect()&lt;/code&gt;&lt;/a&gt; reports. Every number it takes is understood to mean px. No &lt;code&gt;"4px"&lt;/code&gt; string ever crosses the boundary."&lt;/p&gt;

&lt;p&gt;"Then why does CSS accept the very same value?" I asked, and showed him:&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;.dropdown&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&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;Holmes smiled — the smile that meant the real work was starting.&lt;/p&gt;

&lt;p&gt;"One door accepts the visitor. The other refuses him. The mystery is not at either door, Watson. It is at the border between them."&lt;/p&gt;

&lt;h2&gt;
  
  
  Clue the First: &lt;code&gt;rem&lt;/code&gt; Is Not a Fixed Number
&lt;/h2&gt;

&lt;p&gt;"The name tells us the first fact," said Holmes, writing on the hearth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rem = root em
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"One &lt;code&gt;rem&lt;/code&gt; is based on the font size of the root element — usually &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;. If that size is 16px:"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1rem     = 16px
0.25rem  = 4px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"There," I said. "So it &lt;em&gt;is&lt;/em&gt; 4px. Fixed. Done."&lt;/p&gt;

&lt;p&gt;"You assume every visitor's browser agrees with your arithmetic, Watson. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/length" rel="noopener noreferrer"&gt;MDN describes &lt;code&gt;rem&lt;/code&gt; as a root-relative length&lt;/a&gt;: the common default is 16px, but user preferences may change it. A reader with weak eyesight may set the root font size to 20px. A media query may change it. The application itself may change it."&lt;/p&gt;

&lt;p&gt;He underlined his chalk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.25 × current root font size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"&lt;code&gt;0.25rem&lt;/code&gt; is not a number, Watson. It is a &lt;em&gt;recipe&lt;/em&gt;. And a recipe must be baked before it can be eaten."&lt;/p&gt;

&lt;h2&gt;
  
  
  Clue the Second: CSS Understands the Recipe
&lt;/h2&gt;

&lt;p&gt;"Then the browser is the culprit," I said. "It cannot process relative units."&lt;/p&gt;

&lt;p&gt;Holmes shook his head.&lt;/p&gt;

&lt;p&gt;"Watch what the engine does with &lt;code&gt;padding: 0.25rem&lt;/code&gt;. It knows four things:"&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The number is &lt;code&gt;0.25&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The unit is &lt;code&gt;rem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rem&lt;/code&gt; points to the root font size.&lt;/li&gt;
&lt;li&gt;So the final layout distance can be calculated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Relative units are not strangers in CSS — they are citizens. The browser resolves them during value processing: first into computed values, then into the final pixel geometry used for layout. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascade/Property_value_processing" rel="noopener noreferrer"&gt;MDN walks through this pipeline&lt;/a&gt;."&lt;/p&gt;

&lt;p&gt;"So the browser is innocent," I said.&lt;/p&gt;

&lt;p&gt;"Eliminated. And that is progress, Watson. &lt;strong&gt;When you have eliminated the impossible, whatever remains, however improbable, must be the truth.&lt;/strong&gt; What remains is the strangest suspect of all: JavaScript itself."&lt;/p&gt;

&lt;h2&gt;
  
  
  Clue the Third: JavaScript Numbers Carry No Units
&lt;/h2&gt;

&lt;p&gt;"In JavaScript," said Holmes, "this is a number:"&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="mf"&gt;0.25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"It has no unit attached. And this is a string:"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.25rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"JavaScript can hold that string — but arithmetic does not know what it means:"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.25rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="c1"&gt;// NaN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"There!" I said. "JavaScript cannot do the math. My own theory, Holmes — it cannot understand relative units."&lt;/p&gt;

&lt;p&gt;Holmes looked at me with something between pity and patience.&lt;/p&gt;

&lt;p&gt;"JavaScript is not bad at arithmetic, Watson. It multiplies perfectly — the moment it is given numbers. The problem is that an ordinary string carries no CSS meaning into a geometry calculation. The suspect was never arithmetic. The suspect is the &lt;em&gt;border&lt;/em&gt; — the crossing between two realms: CSS, where values carry units, and JavaScript, where geometry speaks only in numbers."&lt;/p&gt;

&lt;p&gt;He was quiet for a moment.&lt;/p&gt;

&lt;p&gt;"JavaScript &lt;em&gt;can&lt;/em&gt; ask the browser to resolve CSS values — through &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle" rel="noopener noreferrer"&gt;&lt;code&gt;getComputedStyle()&lt;/code&gt;&lt;/a&gt;, or the more structured &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/computedStyleMap" rel="noopener noreferrer"&gt;CSS Typed OM&lt;/a&gt;. The translation is possible. It is simply not automatic. And that, Watson, is where our criminal has been hiding all along: in the gap between what CSS says and what JavaScript can measure."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Translation
&lt;/h2&gt;

&lt;p&gt;When Holmes finally wrote the fix, it was almost too small to believe:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remToPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rem&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;rem&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&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="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;remToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"If the root font size is 16px:"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.25 × 16 = 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"If it is 20px:"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.25 × 20 = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"The result is a bare number — &lt;code&gt;4&lt;/code&gt;, never &lt;code&gt;"4px"&lt;/code&gt;. The unit lives in the meaning, not in the value."&lt;/p&gt;

&lt;p&gt;I studied the little function. "So this fixes JavaScript's weakness with units."&lt;/p&gt;

&lt;p&gt;"No, Watson. That theory died two clues ago. Nothing is wrong with JavaScript's arithmetic. This function &lt;em&gt;translates&lt;/em&gt;: a CSS-relative length on one side, an absolute pixel number on the other. Every border needs a translator who speaks both languages."&lt;/p&gt;

&lt;p&gt;"And this border has many crossings, Watson — canvas, scroll offsets, element measurements. All of them speak in bare numbers."&lt;/p&gt;

&lt;h2&gt;
  
  
  A Loose Thread
&lt;/h2&gt;

&lt;p&gt;The case looked closed. Holmes, of course, did not think so.&lt;/p&gt;

&lt;p&gt;"One thread remains, and it is the one a careless detective never pulls. Is this translation &lt;em&gt;responsive&lt;/em&gt;? The function reads the root font size at the moment it runs:"&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="nf"&gt;getComputedStyle&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Call it again after a font-size change, and it picks up the new value. But nothing calls it again on its own. If the result is cached — computed once at startup, or baked into a value created when the page loads — a later font-size change will be ignored until the code runs the conversion again."&lt;/p&gt;

&lt;p&gt;"And our own dropdown, Holmes — which of its values are baked, and which stay fresh?"&lt;/p&gt;

&lt;p&gt;"In the dropdown that began this case, the panel's maximum height is recalculated on every position update, so it always uses the current root font size. The offset and shift values are created once, when the positioning is set up, so they would only see a font-size change after a reload."&lt;/p&gt;

&lt;p&gt;"Is there no way past this last hurdle, Holmes?"&lt;/p&gt;

&lt;p&gt;"Floating UI's authors were not blind to the problem, Watson. The &lt;code&gt;offset()&lt;/code&gt; middleware accepts not only a bare number, but a function that returns one:"&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="nf"&gt;offset&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;remToPx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Pass the recipe instead of the baked dish, and Floating UI runs our translator on every position update. The gap now follows any font-size change while the page is open — no need to rebuild the middleware. The same trick works for &lt;code&gt;shift()&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;He wrote the rule beneath it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSS resolves rem dynamically.
JavaScript resolves it only when your code runs the conversion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"And &lt;em&gt;that&lt;/em&gt;, Watson, is why a matter you called trivial led through CSS value processing, browser geometry, and the internals of a positioning library."&lt;/p&gt;

&lt;p&gt;"The &lt;code&gt;rem&lt;/code&gt; was never missing at all, then," I said.&lt;/p&gt;

&lt;p&gt;"It was never missing, Watson. It simply could not cross the border into JavaScript unescorted."&lt;/p&gt;

&lt;p&gt;Case closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Smallest Clues
&lt;/h2&gt;

&lt;p&gt;I confess I had expected to waste the afternoon on a four-pixel gap.&lt;/p&gt;

&lt;p&gt;Instead, I learned the lesson Holmes had been teaching me all afternoon: the tools you reach for tell you what you do not understand yet. I reached for a string because CSS had taught me that spacing carries units. The library refused it because geometry is spoken in numbers. Between those two truths stood a one-line translator — and behind it, a whole system I had never once thought to inspect.&lt;/p&gt;

&lt;p&gt;The smallest clues sometimes open the largest cases.&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>The Dropdown Behind the Curtain: A Wizard of Oz Guide to CSS Overflow Clipping</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Wed, 16 Sep 2026 19:52:00 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/the-dropdown-behind-the-curtain-a-wizard-of-oz-guide-to-css-overflow-clipping-18di</link>
      <guid>https://dev.to/kickbuttowski80/the-dropdown-behind-the-curtain-a-wizard-of-oz-guide-to-css-overflow-clipping-18di</guid>
      <description>&lt;p&gt;&lt;em&gt;Somewhere over the rainbow, dropdowns render outside their containers. Down here in Kansas, they get cut in half.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; An &lt;code&gt;absolute&lt;/code&gt;-positioned dropdown inside a scrollable modal gets cut: &lt;code&gt;position: absolute&lt;/code&gt; carries the panel past the modal body's padding-box edge, and &lt;code&gt;overflow-y: auto&lt;/code&gt; clips everything painted past it. Neither rule causes the bug alone. The fix: teleport the panel out of the scroll container and float it. This post does it in Vue (&lt;code&gt;&amp;lt;Teleport&amp;gt;&lt;/code&gt; + Floating UI) — every framework has the same move (e.g. React: &lt;code&gt;createPortal&lt;/code&gt;). The Popover API and CSS anchor positioning are the platform's version of it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Kansas
&lt;/h2&gt;

&lt;p&gt;Dorothy had a four-step setup form. On the second step, the team roster, adding a new team member opens a focused modal — and there their role is picked from a dropdown: &lt;strong&gt;Scarecrow&lt;/strong&gt;, &lt;strong&gt;Tin Man&lt;/strong&gt;, or &lt;strong&gt;Lion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It was a plain &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;. It worked. It was also &lt;em&gt;ugly&lt;/em&gt; — gray, boxy, and the dropdown list looked like Windows 95 no matter what she did to it. MDN puts it politely: the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select#styling_with_css" rel="noopener noreferrer"&gt;noted for being hard to style consistently across browsers&lt;/a&gt;. A fully styleable &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; is finally landing — Chromium and Safari 27 ship it, Firefox does not yet — but &lt;em&gt;landing&lt;/em&gt; and &lt;em&gt;landed in every browser your users run&lt;/em&gt; are different things — and Dorothy had a deadline.&lt;/p&gt;

&lt;p&gt;The native &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; has one big advantage, though: the browser draws its dropdown as its own layer, outside your boxes — no ancestor's &lt;code&gt;overflow&lt;/code&gt; can clip it. That is also why it is hard to restyle: the parts developers want to touch live mostly outside CSS's reach (&lt;a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select" rel="noopener noreferrer"&gt;MDN: Customizable select elements&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So Dorothy did what any developer with a design system and a deadline would do. She swapped the native &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; for a &lt;a href="https://headlessui.com" rel="noopener noreferrer"&gt;Headless UI&lt;/a&gt; component — a &lt;code&gt;Listbox&lt;/code&gt; with a styled button and a beautiful, dark, rounded options panel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Listbox&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ListboxButton&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"select w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ role || 'Pick a role' }}&lt;span class="nt"&gt;&amp;lt;/ListboxButton&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ListboxOptions&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute z-50 w-full mt-1 rounded-lg shadow-lg max-h-60 overflow-auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ListboxOption&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"r in roles"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"r"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"r"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ r }}&lt;span class="nt"&gt;&amp;lt;/ListboxOption&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ListboxOptions&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Listbox&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;relative&lt;/code&gt; wrapper anchors the options to the button — the standard pattern.&lt;/p&gt;

&lt;p&gt;She opened it. It was gorgeous.&lt;/p&gt;

&lt;p&gt;Then the tornado hit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tornado
&lt;/h2&gt;

&lt;p&gt;The role dropdown sat near the bottom of the modal. When it opened, only the first option was fully visible. &lt;strong&gt;Scarecrow&lt;/strong&gt; showed. &lt;strong&gt;Tin Man&lt;/strong&gt; was cut halfway. &lt;strong&gt;Lion&lt;/strong&gt; was gone — not hidden behind something, just &lt;em&gt;not there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Dorothy checked &lt;code&gt;z-index&lt;/code&gt;. She raised it to &lt;code&gt;9999&lt;/code&gt;. Nothing. She checked the DOM — all three options were rendered, sitting right where they should be. The browser just wasn't drawing them.&lt;/p&gt;

&lt;p&gt;Think of it like a stage. The DOM is everything backstage — all three actors exist, standing in their spots. Painting is what the audience sees through the curtain. Lion was on stage — just past the curtain's edge, where nothing gets through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ modal ───────────────────────────────┐
│  Add Team Member                      │
│ ┌─ modal body (overflow-y: auto) ───┐ │
│ │  Name  [ Ada Lovelace          ]  │ │
│ │  Role  [ Scarecrow           ▾ ]  │ │
│ │        ┌─────────────────────┐    │ │
│ │        │ ✔ Scarecrow         │    │ │ ← painted
│ │        │   Tin Man ▓▓▓▓▓▓▓▓▓ │    │ │ ← half-drawn
│ │════════╪═════════════════════│════│ │ ← the clipping edge
│ │        │   Lion              │    │ │ ← in the DOM, never painted
│ │        └─────────────────────┘    │ │
│ └───────────────────────────────────┘ │
│  [ Cancel ]            [ Add Member ] │
└───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The modal body had this:&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;.modal-body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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;That &lt;code&gt;overflow-y: auto&lt;/code&gt; was the last ingredient of the tornado. (The &lt;code&gt;max-height&lt;/code&gt; beside it is innocent — it limits the box's height but draws no clipping edge.) She didn't know it yet. But back in Kansas, Uncle Henry — who was a farmer — had a saying about the spring storms: no tornado is ever one thing; the storm is what happens when the factors line up. She had been searching for the one line to blame; the saying told her there was no one line to blame — only factors that lined up: the &lt;code&gt;absolute&lt;/code&gt; positioning written back in Kansas, and the &lt;code&gt;overflow-y: auto&lt;/code&gt; on the modal body. Each one alone was harmless. Together, they lined up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Yellow Brick Road
&lt;/h2&gt;

&lt;p&gt;Remembering Uncle Henry's saying, Dorothy set off to find the Wizard — the one fix that would make the dropdown appear. Along the way she met three companions — the same three names she'd been fighting to see in that dropdown, now walking beside her — each with one piece of the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Scarecrow: "If I only had a brain (that understood overflow)"
&lt;/h3&gt;

&lt;p&gt;The Scarecrow's problem was a mental model. Dorothy's too. She believed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"&lt;code&gt;overflow: auto&lt;/code&gt; lets the user scroll to see what doesn't fit. It reveals content. It doesn't hide anything."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Half right. Here is what the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/overflow#values" rel="noopener noreferrer"&gt;CSS spec via MDN&lt;/a&gt; actually says about &lt;code&gt;auto&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Overflow content is &lt;strong&gt;clipped at the element's padding box&lt;/strong&gt;. When overflowing, the element box is a scroll container displaying scroll bars."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is doing more work than it sounds like. &lt;code&gt;overflow: auto&lt;/code&gt; is two behaviors in one value:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It draws an invisible clipping edge around the element's &lt;strong&gt;padding box&lt;/strong&gt;. Anything painted outside that edge is not drawn at all.&lt;/li&gt;
&lt;li&gt;If the element's &lt;em&gt;in-flow&lt;/em&gt; content is taller than the box, it adds a scrollbar so that content can be slid into view.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first one is what people miss. &lt;code&gt;auto&lt;/code&gt; &lt;strong&gt;clips&lt;/strong&gt;. So do &lt;code&gt;scroll&lt;/code&gt;, &lt;code&gt;hidden&lt;/code&gt;, and the newer &lt;code&gt;clip&lt;/code&gt;; only &lt;code&gt;visible&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;The scrollbar is a consolation prize. And the fine print — &lt;em&gt;in-flow content&lt;/em&gt; — is the whole key, which is why the Tin Man shows up next.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tin Man: "If I only had a heart (in the document flow)"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In-flow&lt;/strong&gt; just means the element takes up space in the normal layout; an absolutely positioned element is &lt;strong&gt;out of flow&lt;/strong&gt;. MDN's definition of &lt;code&gt;position: absolute&lt;/code&gt; puts it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The element is &lt;strong&gt;removed from the normal document flow&lt;/strong&gt;, and no space is created for the element in the page layout. The element is positioned relative to its closest positioned ancestor."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position#values" rel="noopener noreferrer"&gt;MDN: position&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Read that carefully. The Witch sends two monkeys to make this clip, and each has the job the Winged Monkeys had in the book:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The first monkey is &lt;code&gt;position: absolute&lt;/code&gt; — the carrier.&lt;/strong&gt; In the book, one squad's job was hauling prisoners to the Witch's castle — Toto and the Lion included. Our first monkey's job is the dropdown: he grabs it, pins it to the closest positioned ancestor directly below the button, and lifts it out of the flow — no room reserved, nothing pushed down. Nothing stops him — he can carry it anywhere, even past an edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The second monkey is &lt;code&gt;overflow-y: auto&lt;/code&gt; on the modal body — the wrecker.&lt;/strong&gt; The other squad's job was leaving wreckage: the Scarecrow's straw in a tree, the Tin Woodman dented on rocks. Our second monkey's job is the clipping: he draws the scroll area with its invisible edge, and anything painted past it is cut — dented at the edge, or gone from the painting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reread the Tornado and you can watch them work: the Scarecrow showed, still inside the edge; the Tin Man was dented — half-drawn at the clip line; the Lion was carried off past the edge, in the DOM and never painted. &lt;strong&gt;Absolute positioning places it there; overflow does the clipping. Neither one causes the problem alone — the bug needs both monkeys.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(One bug, two faces: past the edge, the browser paints nothing — the Tin Man dented, the Lion gone. And the dropdown never left: scroll the modal body and the missing half appears, because the browser counts it as part of the container's scrollable area. Same bug wearing a scrollbar — not a fix.)&lt;/p&gt;

&lt;p&gt;But the two monkeys don't strike every time. The modal hugs its content, so when the dropdown opens as the last field, it always sits at the curtain's edge and gets cut — every resolution, every monitor. Move it up the form, with fields below it, and the same two rules cut nothing. The cut doesn't care which screen you're on; it cares where in the form the dropdown lives.&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://www.smashingmagazine.com/2026/03/dropdowns-scrollable-containers-why-break-how-fix/" rel="noopener noreferrer"&gt;Godstime Aburu writes in Smashing Magazine&lt;/a&gt;: &lt;em&gt;"Clipping and positioning are separate systems. They just happen to collide in ways that look completely random until you understand both."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lion: "If I only had the courage (to stop guessing)"
&lt;/h3&gt;

&lt;p&gt;Dorothy tried fixes. Each one felt brave. Each one turned out to be another Winged Monkey — reinforcements the Witch sent after her first two, dressed up as fixes: dramatic, helpful-looking, and still working for the Wicked Witch, not her.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1 — &lt;code&gt;overflow: visible&lt;/code&gt; on the modal body.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dropdown escaped! And then the modal grew taller than the viewport and the footer buttons disappeared off the bottom of the screen. &lt;code&gt;visible&lt;/code&gt; does not clip, but it also does not scroll. She had traded one clipping problem for a different one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2 — &lt;code&gt;position: fixed&lt;/code&gt; on the dropdown.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position#values" rel="noopener noreferrer"&gt;MDN says&lt;/a&gt; &lt;code&gt;position: fixed&lt;/code&gt; is positioned "relative to its initial containing block, which is the viewport." The viewport is not inside the modal body, so nothing clips it. It worked — except &lt;code&gt;top: 100%&lt;/code&gt; now meant "100% of the viewport," and the dropdown rendered at the bottom of the &lt;em&gt;screen&lt;/em&gt;, nowhere near its button. Doing it by hand is a real job — code like this, re-run on every scroll and resize:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buttonEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;dropdownStyle&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;top&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="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&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="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&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="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also has a footgun she dodged: a transformed, filtered, or contained ancestor can hijack the containing block for &lt;code&gt;fixed&lt;/code&gt; descendants (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Display/Containing_block#identifying_the_containing_block" rel="noopener noreferrer"&gt;MDN: containing block&lt;/a&gt;) — and the modal's open/close animation used &lt;code&gt;transform: scale()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Back in the Tornado, her first move was &lt;code&gt;z-index: 9999&lt;/code&gt; — and this is why it was always going to do nothing: &lt;code&gt;z-index&lt;/code&gt; only wins fights &lt;em&gt;inside&lt;/em&gt; a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Positioned_layout/Stacking_context" rel="noopener noreferrer"&gt;stacking context&lt;/a&gt;. Trapped in a context that already lost, the dropdown can carry the biggest number on the page and still paint underneath everything. The fix is not a bigger number; it is finding which ancestor built the walls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3 — A positioning library.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Teleporting the panel out of the modal solves the clipping — and creates a new job. A panel in &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; is nobody's child: someone must measure the button, follow every scroll and resize, and cap the height underneath. Doing that by hand is Attempt 2's math all over again, so Dorothy reached for &lt;a href="https://floating-ui.com" rel="noopener noreferrer"&gt;Floating UI&lt;/a&gt; and wrapped it, with the Teleport, in one small component. A package, a composable, and a wrapper later, the role dropdown worked.&lt;/p&gt;

&lt;p&gt;The Lion's courage is not fighting; it is out-thinking the Witch instead of swinging at every flying monkey.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Emerald City
&lt;/h2&gt;

&lt;p&gt;Dorothy reached the Wizard. Pulled back the curtain. And found:&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;.modal-body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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;The same two rules from the beginning. There was no great and powerful fix. There was a scroll container with a clipping edge, and an absolutely positioned dropdown that had been painting past it.&lt;/p&gt;

&lt;p&gt;"Pay no attention to the man behind the curtain" is exactly what a scroll container says. The container &lt;em&gt;is&lt;/em&gt; the curtain. Everything behind its padding-box edge is calculated, positioned, real — and never drawn.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ruby Slippers
&lt;/h2&gt;

&lt;p&gt;Glinda's line is the whole lesson: &lt;em&gt;you had the power all along&lt;/em&gt; — understand what each tool actually does before you reach for the next one.&lt;/p&gt;

&lt;p&gt;For a plain text-only dropdown, the ruby slippers really were the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; Dorothy started with: the browser draws it outside your boxes — the freedom she ended up rebuilding by hand. Teams still swap it for real reasons (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select#styling_with_css" rel="noopener noreferrer"&gt;MDN: hard to style consistently&lt;/a&gt;), and Headless UI's &lt;a href="https://headlessui.com/v1/vue/listbox" rel="noopener noreferrer"&gt;Listbox&lt;/a&gt; rebuilds the accessibility for free while adding the styling she needed. The swap was real, not a mistake.&lt;/p&gt;

&lt;p&gt;Two more things she learned the hard way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you need custom styling, move the positioned ancestor.&lt;/strong&gt; The oldest trick in the book (&lt;a href="https://css-tricks.com/popping-hidden-overflow/" rel="noopener noreferrer"&gt;CSS-Tricks: Popping Hidden Overflow&lt;/a&gt;): the closest positioned ancestor must live &lt;em&gt;outside&lt;/em&gt; the overflow container while still being an ancestor of the dropdown. Absolute only clips against the scroll container if the container sits between the dropdown and that ancestor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check whether it is &lt;em&gt;actually&lt;/em&gt; clipping today.&lt;/strong&gt; The dropdown got cut because it opened &lt;em&gt;below&lt;/em&gt; the modal body's visible edge; add fields beneath it and the same &lt;code&gt;absolute&lt;/code&gt; code stops clipping. Nothing fixed itself — the geometry did. And "it fits on my screen" proves nothing: the cut cares where in the form the dropdown lives, not which screen it's on. That is why Dorothy's team wrote the rule structurally instead of hoping the form layout stayed lucky.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Dorothy Actually Did
&lt;/h2&gt;

&lt;p&gt;The clip is structural, and the only reliable, no-surprises answer is to take the dropdown &lt;strong&gt;out of the scroll container's DOM tree entirely&lt;/strong&gt;. In a Vue + Headless UI app, that means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the &lt;code&gt;&amp;lt;ListboxButton&amp;gt;&lt;/code&gt; inside the form so focus, &lt;code&gt;v-model&lt;/code&gt;, and ARIA relationships stay intact.&lt;/li&gt;
&lt;li&gt;Render the &lt;code&gt;&amp;lt;ListboxOptions&amp;gt;&lt;/code&gt; through a &lt;code&gt;&amp;lt;Teleport to="body"&amp;gt;&lt;/code&gt; while the dropdown is open.&lt;/li&gt;
&lt;li&gt;Anchor the teleported panel with &lt;code&gt;@floating-ui/vue&lt;/code&gt;, using &lt;code&gt;strategy: 'fixed'&lt;/code&gt; — safe because the panel lives in &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, where no transformed ancestor can hijack its containing block, and necessary because Headless UI (the 1.7.x line Dorothy's team ships) calls &lt;code&gt;scrollIntoView()&lt;/code&gt; on the active option when the panel opens; with &lt;code&gt;absolute&lt;/code&gt; the panel briefly renders at the document's origin, and that call drags the whole document through the modal's &lt;code&gt;overflow: hidden&lt;/code&gt; scroll lock.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concretely, the shared wrapper looks like this — trigger and panel stay in their Headless UI context; only the panel's mounting point changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Listbox&lt;/span&gt; &lt;span class="na"&gt;v-slot=&lt;/span&gt;&lt;span class="s"&gt;"{ open }"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;FloatingDropdown&lt;/span&gt; &lt;span class="na"&gt;:open=&lt;/span&gt;&lt;span class="s"&gt;"open"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;#trigger&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ListboxButton&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"select w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ role || 'Pick a role' }}&lt;span class="nt"&gt;&amp;lt;/ListboxButton&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;#panel&lt;/span&gt;&lt;span class="err"&gt;="{&lt;/span&gt; &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;panelStyle&lt;/span&gt; &lt;span class="err"&gt;}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ListboxOptions&lt;/span&gt; &lt;span class="na"&gt;static&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"panelClass"&lt;/span&gt; &lt;span class="na"&gt;:style=&lt;/span&gt;&lt;span class="s"&gt;"panelStyle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ListboxOption&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"r in roles"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"r"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"r"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ r }}&lt;span class="nt"&gt;&amp;lt;/ListboxOption&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ListboxOptions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/FloatingDropdown&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Listbox&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library hands you the mechanics; it does not hand you the product rules. Dorothy's team wrote three down, and each became a middleware choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never flip.&lt;/strong&gt; A panel that stays attached to its button beats one that repositions itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never shift vertically&lt;/strong&gt; — horizontally only, or the panel detaches from the trigger while the modal body scrolls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the height to the space underneath&lt;/strong&gt; with &lt;code&gt;size()&lt;/code&gt;, which publishes that space as a CSS variable the panel folds into its own &lt;code&gt;max-height&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Order matters — &lt;code&gt;offset&lt;/code&gt; before &lt;code&gt;shift&lt;/code&gt; — and the trigger needs its own visibility check, or the panel lingers, orphaned, after the button scrolls away.&lt;/p&gt;

&lt;p&gt;The rule: &lt;em&gt;if a dropdown opens inside a scrollable modal, teleport it and float it. Do not negotiate with &lt;code&gt;overflow&lt;/code&gt;.&lt;/em&gt; Page-level dropdowns, outside any scroll container, are fine with plain &lt;code&gt;absolute&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Since Dorothy Built Her Form
&lt;/h2&gt;

&lt;p&gt;The platform is catching up. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API" rel="noopener noreferrer"&gt;Popover API&lt;/a&gt; is broadly supported, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning" rel="noopener noreferrer"&gt;CSS anchor positioning&lt;/a&gt; crossed Baseline in January 2026 when Firefox 147 supplied the last engine. Together they do natively, in CSS, what Dorothy's stack did in JavaScript: the panel renders in the top layer above every clip, opens below its trigger and never flips, caps its height to the space underneath, hides when the trigger scrolls away, and gets light-dismiss and &lt;code&gt;Esc&lt;/code&gt; for free.&lt;/p&gt;

&lt;p&gt;But every one of those features is &lt;em&gt;positioning&lt;/em&gt;. A dropdown is two problems wearing one component, and on behavior — listbox role, arrow keys, typeahead, focus management — the platform is silent. That is the work Headless UI was doing all along, and it does not migrate to CSS: the W3C's &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/" rel="noopener noreferrer"&gt;Select-Only Combobox pattern&lt;/a&gt; and its listbox siblings describe what a custom select owes its users, and hand-rolling it inherits the pattern's own warning — &lt;em&gt;no ARIA is better than bad ARIA&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  There's No Place Like Home
&lt;/h2&gt;

&lt;p&gt;And one non-CSS fact: the tools you reach for tell you what you do not understand yet. None of Dorothy's guesses was "read what &lt;code&gt;overflow: auto&lt;/code&gt; does."&lt;/p&gt;

&lt;p&gt;When she finally did, the answer stopped being a guess. The companions' gifts were the ruby slippers — the understanding had been there all along.&lt;/p&gt;

&lt;p&gt;The curtain was two rules of CSS. Often, it is.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>vue</category>
    </item>
    <item>
      <title>Perry Mason in: The Case of the Drifting Timer</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Thu, 20 Aug 2026 00:47:41 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/perry-mason-in-the-case-of-the-drifting-timer-inj</link>
      <guid>https://dev.to/kickbuttowski80/perry-mason-in-the-case-of-the-drifting-timer-inj</guid>
      <description>&lt;h1&gt;
  
  
  Perry Mason in: The Case of the Drifting Timer
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Opening Statement
&lt;/h2&gt;

&lt;p&gt;You need a reactive "current time" in your Vue 3 app. A schedule grid with a red line showing "now." A live clock. A dashboard that updates every minute.&lt;/p&gt;

&lt;p&gt;Every Vue developer reaches for &lt;code&gt;setInterval&lt;/code&gt; first. It works. But "works" and "works well" are different things.&lt;/p&gt;

&lt;p&gt;This is the story of taking a naive timer from "it ticks" to production-grade — and the four iterations it took to get there. The prosecution calls four exhibits. Let's begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exhibit A: The Memory Leak
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;onMounted&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;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="nx"&gt;currentTime&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60000&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;It works. Sort of. The defense rests — but the prosecution is just getting started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibits of negligence:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The interval is never cleared. When the component unmounts, the timer keeps firing every 60 seconds forever — updating a ref nothing reads anymore, and holding its closure (and everything the ref references) in memory for the lifetime of the page.&lt;/li&gt;
&lt;li&gt;Silent. Invisible. The kind of leak that shows up in production after a user navigates around your app for 20 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exhibit B: Component-Only Cleanup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="nf"&gt;onMounted&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="nx"&gt;currentTime&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;timeInterval&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="nx"&gt;currentTime&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;onUnmounted&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;timeInterval&lt;/span&gt;&lt;span class="p"&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;timeInterval&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;Now we clean up. The interval is stored in a variable, cleared on unmount. A step forward. But &lt;code&gt;onUnmounted&lt;/code&gt; has a scope limitation worth understanding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The limitation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;onUnmounted&lt;/code&gt; only works inside components. If someone calls this logic from a Pinia store or outside a component's &lt;code&gt;setup()&lt;/code&gt; context, &lt;code&gt;onUnmounted&lt;/code&gt; never fires. The timer leaks silently. (Composables called &lt;em&gt;synchronously during &lt;code&gt;setup()&lt;/code&gt;&lt;/em&gt; are fine — &lt;a href="https://vuejs.org/guide/reusability/composables.html" rel="noopener noreferrer"&gt;Vue's docs&lt;/a&gt; recommend exactly that. The problem is when there's no component instance at all.)&lt;/li&gt;
&lt;li&gt;The timer fires 60 seconds after &lt;em&gt;load&lt;/em&gt;, not at the top of the minute. If a user opens your app at 10:00:58, the clock won't update until 10:01:58. Your app looks broken — the system tray says 10:01 but your UI still shows 10:00.&lt;/li&gt;
&lt;li&gt;The timer keeps running when the tab is hidden. Wasting CPU on a clock nobody is looking at.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exhibit C: Scope-Safe Cleanup
&lt;/h2&gt;

&lt;p&gt;Vue has a method for scope-safe cleanup: &lt;a href="https://vuejs.org/api/reactivity-advanced.html#onscopedispose" rel="noopener noreferrer"&gt;&lt;code&gt;onScopeDispose&lt;/code&gt;&lt;/a&gt;. From the documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This method can be used as a non-component-coupled replacement of&lt;br&gt;
&lt;code&gt;onUnmounted&lt;/code&gt; in reusable composition functions, since each Vue component's&lt;br&gt;
&lt;code&gt;setup()&lt;/code&gt; function is also invoked in an effect scope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The implementation is just a push onto the currently active scope's cleanup list. From the Vue core source (roughly — &lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/effectScope.ts" rel="noopener noreferrer"&gt;&lt;code&gt;packages/reactivity/src/effectScope.ts&lt;/code&gt;&lt;/a&gt;):&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onScopeDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failSilently&lt;/span&gt; &lt;span class="o"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cleanups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;__DEV__&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;failSilently&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`onScopeDispose() is called when there is no active effect scope...`&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;"Currently active" is the key phrase. Every component's &lt;code&gt;setup()&lt;/code&gt; runs inside an effect scope that Vue creates and activates for you, so inside a component the cleanup is automatic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;onScopeDispose()&lt;/code&gt; cleans up automatically when &lt;code&gt;useNow()&lt;/code&gt; is called synchronously within an active Vue effect scope — such as component setup or setup-store creation. Outside one, callers must retain and call the returned &lt;code&gt;stop()&lt;/code&gt; or create their own scope.&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="c1"&gt;// In a component — Vue creates the scope, onScopeDispose just works&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pinia setup stores&lt;/strong&gt; also run inside their own effect scope. &lt;a href="https://pinia.vuejs.org/api/pinia/interfaces/StoreWithState.html#-dispose-" rel="noopener noreferrer"&gt;Pinia's &lt;code&gt;$dispose()&lt;/code&gt; method&lt;/a&gt; stops the store's associated effect scope (its &lt;a href="https://github.com/vuejs/pinia/blob/main/packages/pinia/src/store.ts" rel="noopener noreferrer"&gt;source&lt;/a&gt; calls &lt;code&gt;scope.stop()&lt;/code&gt;), which invokes any &lt;code&gt;onScopeDispose&lt;/code&gt; callbacks registered during setup. Note that unlike component unmount, &lt;code&gt;$dispose()&lt;/code&gt; must be called intentionally — it is not automatic.&lt;/p&gt;

&lt;p&gt;The only time you reach for &lt;code&gt;effectScope()&lt;/code&gt; yourself is when there's genuinely &lt;strong&gt;no&lt;/strong&gt; scope to hook into — app-level singletons, plugins, plain modules. In that case, either create a scope and remember to stop it:&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="c1"&gt;// No component, no store — create a scope, and remember to stop it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;effectScope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// triggers cleanup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…or skip the scope and call &lt;code&gt;stop()&lt;/code&gt; manually:&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="c1"&gt;// No scope at all — caller disposes manually&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// ...later, when the owner is torn down:&lt;/span&gt;
&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat, straight from the &lt;a href="https://vuejs.org/api/composition-api-lifecycle.html#onscopedispose" rel="noopener noreferrer"&gt;Vue docs&lt;/a&gt;: if &lt;code&gt;onScopeDispose&lt;/code&gt; is called with &lt;strong&gt;no&lt;/strong&gt; active scope, Vue logs a warning and the callback is never registered. Vue 3.5+ accepts a second argument, &lt;code&gt;onScopeDispose(cleanup, true)&lt;/code&gt;, which suppresses the warning — but the callback is still not registered. That's why the final composable uses &lt;code&gt;failSilently = true&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; exposes &lt;code&gt;stop()&lt;/code&gt; for manual disposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exhibit D: Clock Alignment + Visibility Pause
&lt;/h2&gt;

&lt;p&gt;The final composable (shown at the end of this article) addresses the human-perception problem and the battery-drain problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clock Alignment
&lt;/h3&gt;

&lt;p&gt;If you load at 10:00:58, you don't want to wait 60 seconds for the first tick. You want the clock to update at 10:01:00 — aligned to the next interval boundary (minute boundaries with the default 60,000 ms interval).&lt;/p&gt;

&lt;p&gt;The composable reads the current time immediately on start, then schedules every subsequent tick at the next interval boundary. So the clock is correct from the moment it loads, and stays aligned on every tick.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msUntilNextTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;-&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;interval&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="nx"&gt;now&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;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainTimeISO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;timer&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="nx"&gt;now&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;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainTimeISO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;msUntilNextTick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the default 60,000 ms interval, &lt;code&gt;msUntilNextTick&lt;/code&gt; is the milliseconds remaining until the next minute boundary. The initial &lt;code&gt;setTimeout&lt;/code&gt; fires at that boundary, then hands off to a recurring timer for all subsequent ticks. The clock feels synchronized with the system tray.&lt;/p&gt;

&lt;p&gt;But there's a subtlety: &lt;code&gt;setInterval&lt;/code&gt; drifts. Modern browsers under system load or low-power modes can slip a few milliseconds per tick. Over hours, the timer ends up firing 5–10 seconds past the top of the minute. The fix is a &lt;strong&gt;self-correcting recursive &lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt; that recalculates &lt;code&gt;interval - (Date.now() % interval)&lt;/code&gt; on &lt;em&gt;every&lt;/em&gt; tick — re-aligning to the real interval boundary each time. Delay does not accumulate from one tick to the next — if the browser runs a callback late, the next callback is scheduled toward the next clock boundary rather than inheriting the previous lateness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visibility Pause
&lt;/h3&gt;

&lt;p&gt;When the user switches tabs, &lt;code&gt;document.hidden&lt;/code&gt; becomes &lt;code&gt;true&lt;/code&gt;. We stop the timer. When they come back, we immediately sync the time and restart the interval.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onVisibilityChange&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="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// immediately reads the current time, then starts ticking&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;No wasted CPU when nobody is looking. No stale time when they return.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSR Safety
&lt;/h3&gt;

&lt;p&gt;If this composable runs during Server-Side Rendering (Nuxt, Vite SSG), &lt;code&gt;document&lt;/code&gt; doesn't exist. Calling &lt;code&gt;document.addEventListener&lt;/code&gt; at the top level would crash with &lt;code&gt;ReferenceError: document is not defined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: wrap all DOM-dependent code in a &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt; guard. The DOM code is SSR-safe. Separately, if you use Temporal, the server needs it too — natively or through a polyfill imported at your app's entry point. These are two independent requirements: no DOM on the server, and Temporal availability on the server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hydration note:&lt;/strong&gt; Rendering live time on the server can cause hydration&lt;br&gt;
mismatches because the server and browser render at different moments — and&lt;br&gt;
potentially in different time zones. The cleanest pattern for a live clock is&lt;br&gt;
to render a neutral placeholder server-side and start the actual clock after&lt;br&gt;
client mount.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Temporal Precision
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; defaults to nanosecond precision (e.g., &lt;code&gt;14:30:15.123456789&lt;/code&gt;). When bound directly to a template, those sub-second digits render as noise. The fix: round to seconds with &lt;code&gt;.round({ smallestUnit: 'second' })&lt;/code&gt; so &lt;code&gt;now.value&lt;/code&gt; formats cleanly out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict: The Final Composable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shallowRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onScopeDispose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isBrowser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&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="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interval must be a positive finite number of milliseconds&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="c1"&gt;// Round to seconds so default template rendering is clean (e.g., "14:30:15")&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getNow&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="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainTimeISO&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;smallestUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// shallowRef: Temporal.PlainTime is immutable — no need for Vue to proxy&lt;/span&gt;
  &lt;span class="c1"&gt;// its internals. We only ever replace .value, never mutate it.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shallowRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getNow&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stop&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="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;timer&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Self-correcting recursive timeout: recalculates remaining ms to next&lt;/span&gt;
  &lt;span class="c1"&gt;// boundary on every tick — delay does not accumulate across ticks&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scheduleNextUpdate&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stop&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;msUntilNextTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;-&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;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&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="nx"&gt;now&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="nf"&gt;getNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;scheduleNextUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;msUntilNextTick&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;start&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Don't schedule if the page is already hidden (e.g. loaded in a&lt;/span&gt;
    &lt;span class="c1"&gt;// background tab) — visibilitychange only fires on state changes&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;isBrowser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;now&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="nf"&gt;getNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;scheduleNextUpdate&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;onVisibilityChange&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="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;start&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;dispose&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stop&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;isBrowser&lt;/span&gt;&lt;span class="p"&gt;)&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;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;onVisibilityChange&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="c1"&gt;// SSR Safe: only attach DOM listeners in the browser environment&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;isBrowser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;start&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;onVisibilityChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// failSilently=true: if no active scope, caller must call stop() manually&lt;/span&gt;
  &lt;span class="nf"&gt;onScopeDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dispose&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dispose&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;Usage in a component:&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="c1"&gt;// Vue creates the scope — onScopeDispose handles cleanup automatically&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage outside a scope (plugins, plain modules):&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="c1"&gt;// No active scope — caller disposes manually&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// ...later, when the owner is torn down:&lt;/span&gt;
&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composable handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clock alignment to interval boundaries (minute boundaries with the default 60,000 ms interval; self-correcting, non-accumulating delay)&lt;/li&gt;
&lt;li&gt;Tab visibility pausing (including when the page loads already hidden)&lt;/li&gt;
&lt;li&gt;Scope-safe cleanup when called inside an active scope (components, Pinia setup stores, effectScope)&lt;/li&gt;
&lt;li&gt;Manual disposal via &lt;code&gt;stop()&lt;/code&gt; when called outside a scope&lt;/li&gt;
&lt;li&gt;SSR safety for DOM code (if you use Temporal, the server needs it too — import a polyfill at your app's entry point if needed)&lt;/li&gt;
&lt;li&gt;Clean Temporal formatting (rounded to seconds)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shallowRef&lt;/code&gt; for immutable Temporal objects (no unnecessary proxying)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Many Clocks: Share One Instance
&lt;/h3&gt;

&lt;p&gt;Each &lt;code&gt;useNow()&lt;/code&gt; call creates its own timer and &lt;code&gt;visibilitychange&lt;/code&gt; listener. That's fine for one or a few clocks, but if a page renders many clock components, consider sharing a single app-level &lt;code&gt;useNow()&lt;/code&gt; instance via &lt;code&gt;effectScope()&lt;/code&gt; and passing the ref down via props or provide/inject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Arguments: Why Recursive &lt;code&gt;setTimeout&lt;/code&gt; Instead of &lt;code&gt;setInterval&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;setInterval&lt;/code&gt;:&lt;/strong&gt; Fires on a fixed interval, but modern browsers drift under load or in low-power modes. A few milliseconds per tick compounds over hours — your clock ends up 5–10 seconds off the real interval boundary. Since we're building a &lt;em&gt;clock UI&lt;/em&gt;, that drift is visible to users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;setTimeout&lt;/code&gt; (recursive):&lt;/strong&gt; Each callback recalculates &lt;code&gt;interval - (Date.now() % interval)&lt;/code&gt; before scheduling the next tick. This re-aligns to the real interval boundary on &lt;em&gt;every&lt;/em&gt; tick. Delay does not accumulate — if the browser runs a callback late, the next callback targets the next clock boundary rather than inheriting the previous lateness. The callback reads &lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; — instantaneous — so there's no risk of overlapping calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;:&lt;/strong&gt; Fires ~60x/sec, synced to screen refresh. Designed for smooth animations. Way too frequent for a 60-second clock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;nextTick&lt;/code&gt;:&lt;/strong&gt; Not a timer at all. Fires once, after Vue's DOM update cycle. No concept of time or repetition.&lt;/p&gt;

&lt;p&gt;For a clock that needs to stay in sync with the system tray over long sessions, self-correcting recursive &lt;code&gt;setTimeout&lt;/code&gt; is the right tool. Browser timer scheduling is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#timeout_value_clamping" rel="noopener noreferrer"&gt;not exact&lt;/a&gt; — the event loop can be busy, and background tabs are commonly throttled — but self-correction ensures that lateness doesn't compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expert Testimony: Why Temporal API Instead of &lt;code&gt;Date&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Date&lt;/code&gt; is perfectly reasonable for a simple display clock — especially if your app doesn't already depend on Temporal. But if your app already uses Temporal for its date/time semantics, it's the natural choice here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Date&lt;/code&gt; object is mutable — &lt;code&gt;setHours()&lt;/code&gt;, &lt;code&gt;setDate()&lt;/code&gt;, &lt;code&gt;setMinutes()&lt;/code&gt; all modify the instance in place. This leads to subtle bugs when the same instance is shared across reactive state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Temporal.PlainTime&lt;/code&gt; and &lt;code&gt;Temporal.PlainDate&lt;/code&gt; are &lt;strong&gt;immutable&lt;/strong&gt;. Every operation returns a new instance. No mutation bugs possible. &lt;code&gt;PlainTime&lt;/code&gt; represents wall-clock time without a date or time zone — exactly what a display clock needs.&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="c1"&gt;// Date — mutable, error-prone&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&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="c1"&gt;// tomorrow, but d is mutated in place&lt;/span&gt;

&lt;span class="c1"&gt;// Temporal — immutable, safe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainDateISO&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;tomorrow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// new instance, today unchanged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As of mid-2026 it's formally standardized as Stage 4 and being merged into the &lt;a href="https://github.com/tc39/ecma262/pull/3759" rel="noopener noreferrer"&gt;ES2027 specification&lt;/a&gt; (the TC39 finished-proposals list updated the target publication year from 2026 to 2027). It's available in current Chromium and Firefox, but full cross-browser support still requires a fallback or polyfill — Safari and iOS Safari remain unsupported. The proposal's implementation status and &lt;a href="https://caniuse.com/temporal" rel="noopener noreferrer"&gt;caniuse&lt;/a&gt; agree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 144+&lt;/strong&gt; — shipped January 13, 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox 139+&lt;/strong&gt; — shipped May 27, 2025&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge 144+&lt;/strong&gt; — Chromium-aligned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari&lt;/strong&gt; — Technology Preview only, disabled by default; the &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=223166" rel="noopener noreferrer"&gt;WebKit implementation bug&lt;/a&gt; is still open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 26+&lt;/strong&gt; — shipped May 5, 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to support browsers without native Temporal, import a Temporal polyfill at your app's entry point. Two options are &lt;a href="https://www.npmjs.com/package/@js-temporal/polyfill" rel="noopener noreferrer"&gt;&lt;code&gt;@js-temporal/polyfill&lt;/code&gt;&lt;/a&gt; (closest to the reference implementation, currently alpha) or &lt;a href="https://www.npmjs.com/package/temporal-polyfill" rel="noopener noreferrer"&gt;&lt;code&gt;temporal-polyfill&lt;/code&gt;&lt;/a&gt; (by FullCalendar, stable release). Pick the one that fits your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Works" is not "works well."&lt;/strong&gt; The naive &lt;code&gt;setInterval&lt;/code&gt; ticks. But it leaks, drifts from real time, and wastes CPU on hidden tabs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prefer &lt;code&gt;onScopeDispose&lt;/code&gt; for reusable composables.&lt;/strong&gt; It works inside any active Vue effect scope (components, Pinia setup stores, &lt;code&gt;effectScope()&lt;/code&gt;) — and inside components it's literally equivalent to &lt;code&gt;onUnmounted&lt;/code&gt;, per RFC #0041. But it's not magic: if there's no active scope, the callback is never registered. Always expose a manual &lt;code&gt;stop()&lt;/code&gt; for unmanaged usage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clock alignment is a human-perception problem.&lt;/strong&gt; Users don't know what "drift" means, but they know your clock looks wrong when it doesn't match their system tray.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Self-correcting beats fixed intervals.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; drifts under load. A recursive &lt;code&gt;setTimeout&lt;/code&gt; that recalculates alignment on every tick ensures delay doesn't accumulate — the next tick always targets the next real clock boundary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Guard against SSR.&lt;/strong&gt; If your composable touches &lt;code&gt;document&lt;/code&gt; or &lt;code&gt;window&lt;/code&gt;, wrap it in &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt;. And if you use Temporal, make sure the server has it too — natively or via polyfill.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;shallowRef&lt;/code&gt; for immutable objects.&lt;/strong&gt; &lt;code&gt;Temporal.PlainTime&lt;/code&gt; is&lt;br&gt;
immutable — you only ever replace &lt;code&gt;.value&lt;/code&gt;, never mutate its internals. &lt;code&gt;shallowRef&lt;/code&gt; avoids unnecessary proxy conversion and is the appropriate model for external class instances.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The court finds &lt;code&gt;setInterval&lt;/code&gt; guilty of negligence. The composable is released into production. Case closed. ⚖️&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Case of the Lying Clock: 5 Vue Mysteries Solved</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:34:40 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/the-case-of-the-lying-clock-5-vue-mysteries-solved-1e99</link>
      <guid>https://dev.to/kickbuttowski80/the-case-of-the-lying-clock-5-vue-mysteries-solved-1e99</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%2Fu0atups87dowf8w853je.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%2Fu0atups87dowf8w853je.png" alt="A desk with a vintage clock and a magnifying glass on a wooden surface" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every detective has their cold cases. These are mine — five Vue concepts that&lt;br&gt;
confused me until I investigated them properly. Grab your magnifying glass.&lt;/p&gt;


&lt;h2&gt;
  
  
  Case #1: The Lying Clock
&lt;/h2&gt;

&lt;p&gt;Imagine you set an alarm to go off every 60 seconds. You press start at&lt;br&gt;
10:00:00.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First alarm: 10:01:00 — perfect&lt;/li&gt;
&lt;li&gt;Second alarm: 10:02:00 — still good&lt;/li&gt;
&lt;li&gt;But your phone is also doing other things: checking email, refreshing
weather, running background tasks. Sometimes it fires the alarm a tiny bit
late.&lt;/li&gt;
&lt;li&gt;Third alarm: 10:03:01 (1 second late)&lt;/li&gt;
&lt;li&gt;Fourth alarm: 10:04:02 (2 seconds off now)&lt;/li&gt;
&lt;li&gt;Five hours later: your alarm fires at 15:05:12 when it should fire at
15:05:00&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap growing bigger over time — that's &lt;strong&gt;drift&lt;/strong&gt;. The timer slowly slides&lt;br&gt;
away from where it should be.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Does This Happen?
&lt;/h3&gt;

&lt;p&gt;JavaScript runs on a single thread — it can only do one thing at a time. When a&lt;br&gt;
timer is supposed to fire, the browser puts it in a queue. But if the thread is&lt;br&gt;
busy doing something else, the timer waits. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#reasons_for_delays_longer_than_specified" rel="noopener noreferrer"&gt;MDN documentation for&lt;br&gt;
&lt;code&gt;setTimeout&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
lists several reasons timers fire late:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested timeouts&lt;/strong&gt; are throttled to a minimum of 4ms after 5 levels of
nesting (per the &lt;a href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers" rel="noopener noreferrer"&gt;HTML5 spec&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background tabs&lt;/strong&gt; are throttled to a maximum of once per second
(&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#timeouts_in_inactive_tabs_throttled_to_=1000ms" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;:
"timeouts are throttled to firing no more often than once per second (1000 ms)
in inactive tabs")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 88+&lt;/strong&gt; introduced &lt;a href="https://developer.chrome.com/blog/timer-throttling-in-chrome-88" rel="noopener noreferrer"&gt;intensive throttling&lt;/a&gt;
for hidden pages: timers that have been hidden for more than 5 minutes are
checked only once per minute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking scripts&lt;/strong&gt; in Firefox get even more aggressive throttling: 10
second minimum in background tabs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Does This Happen on New Devices Too?
&lt;/h3&gt;

&lt;p&gt;Yes, but less. Modern devices are faster, so the delay per tick is smaller —&lt;br&gt;
maybe 1-2 milliseconds instead of 10-20. But over hours, even 1ms per tick&lt;br&gt;
adds up. And background tab throttling happens on &lt;em&gt;every&lt;/em&gt; device, no matter how&lt;br&gt;
fast — it's a browser policy, not a hardware limitation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is This Common Knowledge?
&lt;/h3&gt;

&lt;p&gt;It's the kind of thing you learn when your boss says "why does the clock on&lt;br&gt;
our dashboard show the wrong time after lunch?" and you Google it. Many&lt;br&gt;
developers use &lt;code&gt;setInterval&lt;/code&gt; and never notice because their app doesn't stay&lt;br&gt;
open long enough for the drift to matter.&lt;/p&gt;


&lt;h2&gt;
  
  
  Case #1 Continued: How Recursive &lt;code&gt;setTimeout&lt;/code&gt; Fixes the Drift
&lt;/h2&gt;

&lt;p&gt;A fixed interval can fall behind its intended clock boundary when callbacks&lt;br&gt;
run late — the lateness piles up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start at 10:00:00
→ fire at 10:01:00.002 (2ms late)
→ fire at 10:02:00.005 (5ms late)
→ fire at 10:03:00.009 (getting worse)
→ ...hours later, you're 10+ seconds off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recursive &lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt; recalculates the next boundary from wall-clock time&lt;br&gt;
each tick, so delay doesn't accumulate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start at 10:00:00
→ "next minute is 10:01:00, that's 60000ms away" → set timeout for 60000ms
→ fire at 10:01:00.002 (2ms late — same as before)
→ BUT NOW: check clock: "it's 10:01:00.002, next minute is 10:02:00, that's 59998ms"
→ set timeout for 59998ms
→ fire at 10:02:00.001 (only 1ms late! it corrected itself!)
→ check clock: "it's 10:02:00.001, next minute is 10:03:00, that's 59999ms"
→ fire at 10:03:00.000 — right on time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's called "recursive" because the function calls itself — inside the&lt;br&gt;
&lt;code&gt;setTimeout&lt;/code&gt; callback, we call &lt;code&gt;scheduleNextTick()&lt;/code&gt; again, which sets up the&lt;br&gt;
next &lt;code&gt;setTimeout&lt;/code&gt;. It's a loop that builds itself one step at a time.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setInterval&lt;/code&gt; = a runner who counts their steps and hopes they're on pace&lt;/li&gt;
&lt;li&gt;Recursive &lt;code&gt;setTimeout&lt;/code&gt; = a runner who checks their watch before every step&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Case #2: The Box That Didn't Need Tracking — &lt;code&gt;shallowRef&lt;/code&gt; vs &lt;code&gt;ref&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When you put an object inside &lt;code&gt;ref()&lt;/code&gt;, Vue wraps every single property in a&lt;br&gt;
reactive proxy. If your object has &lt;code&gt;{ hour: 14, minute: 30, second: 15 }&lt;/code&gt;, Vue&lt;br&gt;
tracks all three properties — plus every method, plus any nested objects. This&lt;br&gt;
is great when you need to change one property and have the UI update.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;Temporal.PlainTime&lt;/code&gt; is &lt;strong&gt;immutable&lt;/strong&gt; — you can never change its properties.&lt;br&gt;
You can only throw the whole thing away and make a new one. So Vue's deep&lt;br&gt;
tracking is wasted effort.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shallowRef&lt;/code&gt; says: "only track &lt;code&gt;.value&lt;/code&gt; itself, don't go inside." The &lt;a href="https://vuejs.org/api/reactivity-advanced.html#shallowref" rel="noopener noreferrer"&gt;Vue&lt;br&gt;
documentation&lt;/a&gt; puts&lt;br&gt;
it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unlike &lt;code&gt;ref()&lt;/code&gt;, the inner value of a shallow ref is stored and exposed as-is,&lt;br&gt;
and will not be made deeply reactive. Only the &lt;code&gt;.value&lt;/code&gt; access is reactive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the docs even tell you when to use it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;shallowRef()&lt;/code&gt; is typically used for performance optimizations of large data&lt;br&gt;
structures, or integration with external state management systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since we only ever do &lt;code&gt;now.value = getNow()&lt;/code&gt; (replace the whole object) and&lt;br&gt;
never mutate internal properties, &lt;code&gt;shallowRef&lt;/code&gt; does exactly what we need — and&lt;br&gt;
nothing we don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; &lt;code&gt;ref&lt;/code&gt; is like putting a GPS tracker on every item inside a box.&lt;br&gt;
&lt;code&gt;shallowRef&lt;/code&gt; is like putting a tracker only on the box. If you never open the&lt;br&gt;
box and change items inside — you only swap the whole box — the single tracker&lt;br&gt;
is enough.&lt;/p&gt;


&lt;h2&gt;
  
  
  Case #3: The Cleanup That Worked Everywhere — Effect Scopes and &lt;code&gt;onScopeDispose&lt;/code&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;When a component unmounts, Vue needs to clean up — stop timers, remove event&lt;br&gt;
listeners, cancel watchers. &lt;code&gt;onUnmounted&lt;/code&gt; is the hook for this. But it only&lt;br&gt;
works inside components. If you use a composable from a Pinia store or a&lt;br&gt;
standalone module, &lt;code&gt;onUnmounted&lt;/code&gt; never fires — there's no component to unmount.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;Vue 3.2 introduced &lt;code&gt;effectScope&lt;/code&gt; and &lt;code&gt;onScopeDispose&lt;/code&gt; via&lt;br&gt;
&lt;a href="https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md" rel="noopener noreferrer"&gt;RFC #0041&lt;/a&gt;.&lt;br&gt;
The RFC explains the motivation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In Vue's component &lt;code&gt;setup()&lt;/code&gt;, effects will be collected and bound to the&lt;br&gt;
current instance. When the instance gets unmounted, effects will be disposed&lt;br&gt;
automatically. This RFC is trying to abstract the component's &lt;code&gt;setup()&lt;/code&gt;&lt;br&gt;
effect collecting and disposing feature into a more reusable API that can be&lt;br&gt;
used outside of the component model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In plain English: Vue already had this cleanup mechanism inside components. They&lt;br&gt;
just made it available outside components too.&lt;/p&gt;
&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Every component's &lt;code&gt;setup()&lt;/code&gt; runs inside an &lt;strong&gt;effect scope&lt;/strong&gt; — Vue creates it&lt;br&gt;
automatically. &lt;code&gt;onScopeDispose&lt;/code&gt; registers a cleanup callback on whatever scope&lt;br&gt;
is currently active. The &lt;a href="https://vuejs.org/api/reactivity-advanced.html#onscopedispose" rel="noopener noreferrer"&gt;Vue docs&lt;/a&gt;&lt;br&gt;
describe it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Registers a dispose callback on the current active effect scope. This method&lt;br&gt;
can be used as a non-component-coupled replacement of &lt;code&gt;onUnmounted&lt;/code&gt; in&lt;br&gt;
reusable composition functions, since each Vue component's &lt;code&gt;setup()&lt;/code&gt; function&lt;br&gt;
is also invoked in an effect scope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So inside a component, &lt;code&gt;onScopeDispose&lt;/code&gt; and &lt;code&gt;onUnmounted&lt;/code&gt; do the same thing.&lt;br&gt;
But &lt;code&gt;onScopeDispose&lt;/code&gt; also works in Pinia stores and standalone scopes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Will &lt;code&gt;onMounted&lt;/code&gt; and &lt;code&gt;onUnmounted&lt;/code&gt; Go Away?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onMounted&lt;/code&gt;&lt;/strong&gt; — stays forever. It's about the DOM being ready, not about
cleanup. Neither &lt;code&gt;effectScope&lt;/code&gt; nor &lt;code&gt;onScopeDispose&lt;/code&gt; can replace it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onUnmounted&lt;/code&gt;&lt;/strong&gt; — mostly replaced by &lt;code&gt;onScopeDispose&lt;/code&gt; for cleanup in
composables. It still works, but &lt;code&gt;onScopeDispose&lt;/code&gt; is more versatile.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What Happens If You Use Both Inside a Component?
&lt;/h3&gt;

&lt;p&gt;Nothing bad — they both fire during teardown. No conflict, no error.&lt;br&gt;
It's just redundant — using both for the same cleanup is unnecessary.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Catch: No Active Scope
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;onScopeDispose&lt;/code&gt; is called with no active scope (like from a plain module),&lt;br&gt;
Vue logs a warning and the callback is &lt;strong&gt;never registered&lt;/strong&gt;. The&lt;br&gt;
&lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/effectScope.ts" rel="noopener noreferrer"&gt;Vue source&lt;/a&gt;&lt;br&gt;
shows exactly why:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onScopeDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failSilently&lt;/span&gt; &lt;span class="o"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cleanups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;failSilently&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`onScopeDispose() is called when there is no active effect scope...`&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;No &lt;code&gt;activeEffectScope&lt;/code&gt; → no push → no cleanup. That's why our composable uses&lt;br&gt;
&lt;code&gt;failSilently = true&lt;/code&gt; (to suppress the warning) &lt;strong&gt;and&lt;/strong&gt; exposes a &lt;code&gt;stop()&lt;/code&gt;&lt;br&gt;
function (so the caller can clean up manually).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; &lt;code&gt;onUnmounted&lt;/code&gt; is like "tell me when the building is demolished."&lt;br&gt;
&lt;code&gt;onScopeDispose&lt;/code&gt; is like "tell me when my &lt;em&gt;room&lt;/em&gt; is demolished" — works whether&lt;br&gt;
your room is in a building, a store, or a standalone structure. But if you're&lt;br&gt;
not in any room at all, nobody will tell you anything — so keep a fire&lt;br&gt;
extinguisher (&lt;code&gt;stop()&lt;/code&gt;) handy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case #4: The Server That Knew Too Much — Hydration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Short Version
&lt;/h3&gt;

&lt;p&gt;When using SSR (Server-Side Rendering, like Nuxt), the server generates complete&lt;br&gt;
HTML and sends it to the browser. The user sees a full page immediately. Then&lt;br&gt;
Vue loads in the browser and "takes over" that HTML — attaching event listeners,&lt;br&gt;
making it interactive. That takeover is &lt;strong&gt;hydration&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Matters for Clocks
&lt;/h3&gt;

&lt;p&gt;The server renders at one moment — say 10:00:00. That HTML travels across the&lt;br&gt;
internet for 500ms. The browser hydrates at 10:00:01. Vue compares the server&lt;br&gt;
HTML to what it wants to render and finds a mismatch: the server wrote 10:00:00&lt;br&gt;
but the browser wants 10:00:01.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nuxt.com/docs/3.x/guide/best-practices/hydration" rel="noopener noreferrer"&gt;Nuxt hydration guide&lt;/a&gt;&lt;br&gt;
lists this as a common problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Content that changes based on current time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And &lt;a href="https://www.nazarboyko.com/articles/hydration-issues-in-vue-and-nuxt" rel="noopener noreferrer"&gt;Nazar Boyko's deep dive on hydration&lt;/a&gt;&lt;br&gt;
explains why it matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The server runs this code at, say, 14:32:01. The client runs it at 14:32:03.&lt;br&gt;
The text nodes do not match, and Vue complains.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It gets worse: the server might be in a different time zone than the user. A&lt;br&gt;
server in New York rendering 10:00:00 EST is 15:00:00 for a user in Tokyo.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;The cleanest pattern is to render a placeholder on the server and start the real&lt;br&gt;
clock only after the browser takes over. As the&lt;br&gt;
&lt;a href="https://masteringnuxt.com/blog/fixing-hydration-errors-in-nuxt-a-practical-guide" rel="noopener noreferrer"&gt;Mastering Nuxt guide&lt;/a&gt;&lt;br&gt;
recommends:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Render time only on the client.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or use Nuxt's &lt;a href="https://nuxt.com/docs/api/components/client-only" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;ClientOnly&amp;gt;&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
component, or the&lt;br&gt;
&lt;a href="https://github.com/danielroe/nuxt-time" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;NuxtTime&amp;gt;&lt;/code&gt;&lt;/a&gt; component which is&lt;br&gt;
specifically designed for SSR-safe time rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; It's like a restaurant menu printed yesterday with "Today's Special:&lt;br&gt;
Soup." When you read it today, it's wrong. Better to leave the special blank on&lt;br&gt;
the printed menu and write it in by hand when the customer sits down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does This Apply to Every Project?
&lt;/h3&gt;

&lt;p&gt;No — if you're using plain Vite + Vue, not SSR, there's no hydration. But a&lt;br&gt;
good composable handles it anyway with the &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt;&lt;br&gt;
guard, which is good practice for reusable code that someone might use in Nuxt&lt;br&gt;
someday.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case #5: The Warning Nobody Needed — &lt;code&gt;failSilently&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;onScopeDispose&lt;/code&gt; is called with no active scope, Vue logs a warning:&lt;br&gt;
"Hey, there's no scope to attach this to!" That warning can be noisy if you&lt;br&gt;
&lt;em&gt;intentionally&lt;/em&gt; call the composable outside a scope.&lt;/p&gt;

&lt;p&gt;Passing &lt;code&gt;true&lt;/code&gt; as the second argument tells Vue: "I know there might not be a&lt;br&gt;
scope. Don't warn me. I'll handle cleanup myself with &lt;code&gt;stop()&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;failSilently = true&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; create cleanup. It just hides&lt;br&gt;
the warning. The callback is still not registered. You still need &lt;code&gt;stop()&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mini-note: Temporal Rounding&lt;/strong&gt; — &lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; gives you&lt;br&gt;
nanosecond precision (&lt;code&gt;14:30:15.123456789&lt;/code&gt;). That's nine digits after the&lt;br&gt;
seconds. &lt;code&gt;.round({ smallestUnit: 'second' })&lt;/code&gt; trims it to &lt;code&gt;14:30:15&lt;/code&gt;. Clean.&lt;br&gt;
Like showing "$14.30" instead of "$14.300000001."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Case Closed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timer drift&lt;/strong&gt; — A fixed interval can fall behind its intended clock boundary; recursive &lt;code&gt;setTimeout&lt;/code&gt; recalculates from wall-clock time each tick&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shallowRef&lt;/code&gt;&lt;/strong&gt; — Only tracks &lt;code&gt;.value&lt;/code&gt; replacement, not internal property changes — perfect for immutable objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effect scopes + &lt;code&gt;onScopeDispose&lt;/code&gt;&lt;/strong&gt; — Scopes group reactive effects for disposal; &lt;code&gt;onScopeDispose&lt;/code&gt; registers cleanup on the active scope — works in components, setup-store creation, and manual scopes, but not with no scope at all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration&lt;/strong&gt; — Server sends HTML, browser "takes over" — time-based content can mismatch between the two&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;failSilently&lt;/code&gt;&lt;/strong&gt; — Suppresses the "no active scope" warning; does NOT create cleanup — you still need &lt;code&gt;stop()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vue3</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Stop Using Props + Watchers Just to Trigger Actions in Child Components (Vue 3.5 useTemplateRef + defineExpose)</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:41:36 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/stop-using-props-watchers-just-to-trigger-actions-in-child-components-vue-35-usetemplateref--5gdk</link>
      <guid>https://dev.to/kickbuttowski80/stop-using-props-watchers-just-to-trigger-actions-in-child-components-vue-35-usetemplateref--5gdk</guid>
      <description>&lt;p&gt;How many times have you seen (or written) code where a parent component passes a boolean prop to a child component solely to trigger a method inside that child?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ Parent passing a trigger flag --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ChildComponent&lt;/span&gt; &lt;span class="na"&gt;:shouldReset=&lt;/span&gt;&lt;span class="s"&gt;"isResetting"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the child component, you end up writing an artificial watcher just to catch that state flip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ Child watching a prop just to run a method --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shouldReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;watch&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shouldReset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&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;newVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;resetForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Running internal logic based on prop change&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a classic Vue code smell. Passing state down when you actually mean to send a &lt;strong&gt;command&lt;/strong&gt; creates awkward state-toggling hacks, extra reactivity cycles, and unnecessary watcher logic.&lt;/p&gt;

&lt;p&gt;Here is how to clean up this anti-pattern using &lt;code&gt;defineExpose&lt;/code&gt; and Vue 3.5’s &lt;code&gt;useTemplateRef()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "Props + Watchers" as Action Triggers is a Smell
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Artificial State Pollution:&lt;/strong&gt; You have to manage boolean flags in the parent (&lt;code&gt;isResetting = true&lt;/code&gt;, then back to &lt;code&gt;false&lt;/code&gt;) just to fire a one-time event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reactivity Overhead:&lt;/strong&gt; Vue has to track the prop dependency, queue the watcher, evaluate the callback, and schedule component re-renders—all to invoke a simple function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluttered Child Logic:&lt;/strong&gt; The child component needs boilerplate code to watch a prop, handle boolean states, and reset the flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vue is primarily declarative, but &lt;strong&gt;UI actions are inherently imperative&lt;/strong&gt;. Focusing an input, resetting a form, playing an animation, or stepping through a wizard are actions, not state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Better Pattern: &lt;code&gt;defineExpose&lt;/code&gt; + &lt;code&gt;useTemplateRef&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of forcing declarative props onto an imperative action, expose the child’s internal function directly to the parent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Explicitly Expose the Method in the Child
&lt;/h3&gt;

&lt;p&gt;By default, components using &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; are &lt;strong&gt;closed&lt;/strong&gt;. External components cannot access internal variables or functions unless you explicitly expose them using &lt;code&gt;defineExpose&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ChildComponent.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// The internal action&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resetForm&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;formState&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Form reset executed!&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="c1"&gt;// Explicitly grant access to external parents&lt;/span&gt;
&lt;span class="nf"&gt;defineExpose&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;resetForm&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"formState.name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"formState.email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Call the Exposed Method in the Parent (Vue 3.5)
&lt;/h3&gt;

&lt;p&gt;In Vue 3.5+, use &lt;code&gt;useTemplateRef()&lt;/code&gt; to obtain a strongly-typed reference to the child component instance, then call the method directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ParentComponent.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTemplateRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ChildComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ChildComponent.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Obtain the reference using Vue 3.5's useTemplateRef&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;childRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTemplateRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;childRef&lt;/span&gt;&lt;span class="dl"&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;handleResetButtonClick&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Directly invoke the exposed method on the child instance&lt;/span&gt;
  &lt;span class="nx"&gt;childRef&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;resetForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ChildComponent&lt;/span&gt; &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"childRef"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"handleResetButtonClick"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset Child Form&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Architectural Balance: When to Use What
&lt;/h2&gt;

&lt;p&gt;A common rule in Vue is &lt;em&gt;"Props Down, Events Up"&lt;/em&gt;. Reaching into child components with refs should not replace standard data flow, but knowing when to use each pattern keeps your architecture clean:&lt;/p&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;Recommended Pattern&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passing data into a child&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Props&lt;/strong&gt; (&lt;code&gt;:data="..."&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Declarative state rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notifying a parent about child actions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Emits&lt;/strong&gt; (&lt;code&gt;@submit="..."&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Decoupled event notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triggering imperative child UI actions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;defineExpose&lt;/code&gt; + `useTemplateRef&lt;/strong&gt;`&lt;/td&gt;
&lt;td&gt;Direct command execution without state hacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If your component requires a prop and a &lt;code&gt;watch&lt;/code&gt; solely to trigger an internal function, you are using reactivity as a workaround for command execution.&lt;/p&gt;

&lt;p&gt;Leverage &lt;code&gt;defineExpose&lt;/code&gt; alongside Vue 3.5's &lt;code&gt;useTemplateRef()&lt;/code&gt; to keep component logic clean, imperative, and free of artificial state flags.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
