<?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: Antonio Bonet</title>
    <description>The latest articles on DEV Community by Antonio Bonet (@tonyblu331).</description>
    <link>https://dev.to/tonyblu331</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%2F3965262%2Fee8b6fc8-3b1b-4ac3-8831-5e0791978b82.png</url>
      <title>DEV Community: Antonio Bonet</title>
      <link>https://dev.to/tonyblu331</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tonyblu331"/>
    <language>en</language>
    <item>
      <title>I Missed Engine Debug Views in Three.js, So I Built a WebGPU Helper</title>
      <dc:creator>Antonio Bonet</dc:creator>
      <pubDate>Mon, 08 Jun 2026 16:52:32 +0000</pubDate>
      <link>https://dev.to/tonyblu331/i-missed-engine-debug-views-in-threejs-so-i-built-a-webgpu-helper-3o32</link>
      <guid>https://dev.to/tonyblu331/i-missed-engine-debug-views-in-threejs-so-i-built-a-webgpu-helper-3o32</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.amazonaws.com%2Fuploads%2Farticles%2Fcz1ogjb8m5gowszcnfri.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.amazonaws.com%2Fuploads%2Farticles%2Fcz1ogjb8m5gowszcnfri.png" alt=" " width="800" height="501"&gt;&lt;/a&gt;I come from an environment art and tech art background.&lt;/p&gt;

&lt;p&gt;For years, my main tools were Unreal Engine 5 and the usual real-time engine stack. When you work inside engines, you get used to seeing more than the final frame.&lt;/p&gt;

&lt;p&gt;You check normals.&lt;br&gt;
You check depth.&lt;br&gt;
You isolate lighting.&lt;br&gt;
You inspect albedo, roughness, wireframe, buffers, masks, and whatever else helps explain why the frame looks the way it does.&lt;/p&gt;

&lt;p&gt;That workflow sticks.&lt;/p&gt;

&lt;p&gt;Lately, while doing more rendering work in Three.js, especially around WebGPU and TSL, I started missing that layer.&lt;/p&gt;

&lt;p&gt;Three.js gives you a lot of freedom, but once the scene grows, the final image is not enough.&lt;/p&gt;

&lt;p&gt;A material looks wrong.&lt;/p&gt;

&lt;p&gt;Is it the normal map?&lt;br&gt;
Is roughness packed wrong?&lt;br&gt;
Is depth doing something weird?&lt;br&gt;
Is lighting hiding the issue?&lt;br&gt;
Is the shader fine but the input wrong?&lt;/p&gt;

&lt;p&gt;Without debug views, you start guessing.&lt;/p&gt;

&lt;p&gt;So I built threejs-debug-view.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/threejs-debug-compose" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/threejs-debug-compose&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/threejs-debug-compose#readme" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/threejs-debug-compose#readme&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add threejs-debug-view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it is&lt;/p&gt;

&lt;p&gt;threejs-debug-view is a WebGPU-first helper for inspecting render debug views in Three.js + TSL.&lt;/p&gt;

&lt;p&gt;It gives you a way to see what your render pipeline is producing while you build.&lt;/p&gt;

&lt;p&gt;Not a full scene inspector.&lt;br&gt;
Not an EffectComposer helper.&lt;br&gt;
Not a WebGL fallback.&lt;/p&gt;

&lt;p&gt;Just a dev component for render debugging.&lt;/p&gt;

&lt;p&gt;What it shows&lt;/p&gt;

&lt;p&gt;Built-in views include:&lt;/p&gt;

&lt;p&gt;beauty&lt;br&gt;
normals&lt;br&gt;
depth&lt;br&gt;
albedo&lt;br&gt;
roughness&lt;br&gt;
metallic&lt;br&gt;
AO&lt;br&gt;
opacity&lt;br&gt;
emissive&lt;br&gt;
material normals / normal maps&lt;br&gt;
wireframe&lt;br&gt;
lighting-only&lt;br&gt;
reflection-only&lt;br&gt;
estimated shader complexity&lt;/p&gt;

&lt;p&gt;The goal is not to expose every possible render state.&lt;/p&gt;

&lt;p&gt;The goal is to make the useful layers easy to reach.&lt;/p&gt;

&lt;p&gt;Beauty tells you what the user sees.&lt;/p&gt;

&lt;p&gt;Debug views tell you why it looks like that.&lt;/p&gt;

&lt;p&gt;Layouts&lt;/p&gt;

&lt;p&gt;You can display views as:&lt;/p&gt;

&lt;p&gt;single&lt;br&gt;
overlay&lt;br&gt;
split&lt;br&gt;
row&lt;br&gt;
column&lt;br&gt;
quad&lt;br&gt;
grid&lt;/p&gt;

&lt;p&gt;So you can compare the frame against the data behind it.&lt;/p&gt;

&lt;p&gt;Beauty next to normals.&lt;br&gt;
Depth next to roughness.&lt;br&gt;
Lighting-only next to reflection-only.&lt;br&gt;
Shader cost next to the final output.&lt;/p&gt;

&lt;p&gt;That is the part I wanted from engine workflows.&lt;/p&gt;

&lt;p&gt;Less guessing.&lt;br&gt;
More looking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DebugViews&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="s2"&gt;threejs-debug-view/react&lt;/span&gt;&lt;span class="dl"&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;DEFAULT_DEBUG_VIEWS&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="s2"&gt;threejs-debug-view&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DebugViews&lt;/span&gt;
  &lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_DEBUG_VIEWS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&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;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;showLabels&lt;/span&gt;
&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R3F adapter&lt;/p&gt;

&lt;p&gt;The package includes an R3F adapter and Leva controls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DebugViews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useDebugViewsControls&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="s2"&gt;threejs-debug-view/react&lt;/span&gt;&lt;span class="dl"&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;DEFAULT_DEBUG_VIEWS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getDebugViewLabels&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="s2"&gt;threejs-debug-view&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DebugLayer&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;controls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebugViewsControls&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;viewLabels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getDebugViewLabels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_DEBUG_VIEWS&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DebugViews&lt;/span&gt; &lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_DEBUG_VIEWS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easier to switch views and layouts at runtime without rewriting the scene every time you want to inspect another layer.&lt;/p&gt;

&lt;p&gt;Custom TSL layers&lt;/p&gt;

&lt;p&gt;The built-in views are only the base.&lt;/p&gt;

&lt;p&gt;You can also add your own TSL debug layers.&lt;/p&gt;

&lt;p&gt;That matters once the useful view becomes project-specific.&lt;/p&gt;

&lt;p&gt;A mask.&lt;br&gt;
A falloff.&lt;br&gt;
A weight.&lt;br&gt;
A confidence value.&lt;br&gt;
A reconstruction term.&lt;br&gt;
A material signal.&lt;br&gt;
Some temporary output that only makes sense for the shader you are building.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vec4&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="s2"&gt;three/tsl&lt;/span&gt;&lt;span class="dl"&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;createCustomDebugView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_DEBUG_VIEWS&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="s2"&gt;threejs-debug-view&lt;/span&gt;&lt;span class="dl"&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;DebugViews&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="s2"&gt;threejs-debug-view/react&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;customLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCustomDebugView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug:custom-layer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Custom Layer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;float&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DebugViews&lt;/span&gt; &lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_DEBUG_VIEWS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customLayer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a stable id when the node can be recreated between React renders.&lt;/p&gt;

&lt;p&gt;That helps the render graph dedupe equivalent views instead of treating them as new work every render.&lt;/p&gt;

&lt;p&gt;WebGPU-first&lt;/p&gt;

&lt;p&gt;This is WebGPU-first.&lt;/p&gt;

&lt;p&gt;No WebGL fallback yet.&lt;/p&gt;

&lt;p&gt;It is built around Three.js WebGPU, TSL, MRT passes, and a fullscreen render pipeline.&lt;/p&gt;

&lt;p&gt;So the scope is clear:&lt;/p&gt;

&lt;p&gt;WebGPU-first.&lt;br&gt;
TSL-first.&lt;br&gt;
R3F-friendly.&lt;/p&gt;

&lt;p&gt;Why I made it&lt;/p&gt;

&lt;p&gt;I did not want a full editor inside Three.js.&lt;/p&gt;

&lt;p&gt;I just missed the speed of engine debug views.&lt;/p&gt;

&lt;p&gt;The ability to stop looking only at the final image and quickly check the layers underneath.&lt;/p&gt;

&lt;p&gt;That is how I am used to debugging render work.&lt;/p&gt;

&lt;p&gt;So I packaged the helper I wanted while building.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/threejs-debug-compose" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/threejs-debug-compose&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/threejs-debug-compose#readme" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/threejs-debug-compose#readme&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;pnpm add threejs-debug-view&lt;/p&gt;

&lt;p&gt;Feedback welcome, especially from people working with Three.js WebGPU, TSL, R3F, custom materials, or engine-style rendering workflows on the web.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>react</category>
      <category>webgpu</category>
      <category>frontend</category>
    </item>
    <item>
      <title>CSS ellipsis is great. Until the string itself matters.</title>
      <dc:creator>Antonio Bonet</dc:creator>
      <pubDate>Tue, 02 Jun 2026 19:50:37 +0000</pubDate>
      <link>https://dev.to/tonyblu331/css-ellipsis-is-great-until-the-string-itself-matters-1ijl</link>
      <guid>https://dev.to/tonyblu331/css-ellipsis-is-great-until-the-string-itself-matters-1ijl</guid>
      <description>&lt;p&gt;CSS ellipsis is great.&lt;/p&gt;

&lt;p&gt;Until the string itself matters.&lt;/p&gt;

&lt;p&gt;It works when you only need to visually hide overflow.&lt;/p&gt;

&lt;p&gt;A long label.&lt;/p&gt;

&lt;p&gt;A table cell.&lt;/p&gt;

&lt;p&gt;A title inside a card.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;But sometimes your app needs more than a clipped box.&lt;/p&gt;

&lt;p&gt;Sometimes it needs the actual truncated string.&lt;/p&gt;

&lt;p&gt;Sometimes it needs to preserve a match.&lt;/p&gt;

&lt;p&gt;Sometimes it needs to cut from the middle.&lt;/p&gt;

&lt;p&gt;Sometimes it needs to prepare the text before the UI renders.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;strong&gt;Truncate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A small Pretext powered library for all those cases where CSS ellipsis is not enough.&lt;/p&gt;

&lt;p&gt;Even the weird ones.&lt;/p&gt;

&lt;p&gt;Try it here:&lt;/p&gt;

&lt;p&gt;Playground:&lt;br&gt;
&lt;a href="https://tonyblu331.github.io/truncate/" rel="noopener noreferrer"&gt;https://tonyblu331.github.io/truncate/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/truncate" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/truncate&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most truncation starts here:&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;.label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ellipsis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&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;And honestly, that is still the right answer for a lot of UI.&lt;/p&gt;

&lt;p&gt;CSS ellipsis does not fail.&lt;/p&gt;

&lt;p&gt;It does exactly what it is meant to do.&lt;/p&gt;

&lt;p&gt;It clips text visually.&lt;/p&gt;

&lt;p&gt;But it does not give you the final string.&lt;/p&gt;

&lt;p&gt;It does not know which part of the text matters.&lt;/p&gt;

&lt;p&gt;It does not know that a file extension should stay visible.&lt;/p&gt;

&lt;p&gt;It does not know that a search result should keep the match in view.&lt;/p&gt;

&lt;p&gt;It does not help when you need the output before rendering.&lt;/p&gt;

&lt;p&gt;It just clips.&lt;/p&gt;

&lt;p&gt;That is the gap Truncate is trying to cover.&lt;/p&gt;

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

&lt;p&gt;Truncate gives you small utilities for text truncation before it reaches the UI.&lt;/p&gt;

&lt;p&gt;It supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;width based truncation&lt;/li&gt;
&lt;li&gt;multiline truncation&lt;/li&gt;
&lt;li&gt;middle cuts&lt;/li&gt;
&lt;li&gt;start cuts&lt;/li&gt;
&lt;li&gt;search aware truncation&lt;/li&gt;
&lt;li&gt;range aware truncation&lt;/li&gt;
&lt;li&gt;custom markers&lt;/li&gt;
&lt;li&gt;height aware truncation&lt;/li&gt;
&lt;li&gt;grapheme safe text&lt;/li&gt;
&lt;li&gt;DOM free usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to replace CSS.&lt;/p&gt;

&lt;p&gt;CSS is still the right tool when the browser only needs to hide overflow.&lt;/p&gt;

&lt;p&gt;Truncate is for the cases where your app needs to know what the final text should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;truncate&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="s2"&gt;@tonybonet/truncate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A very long string&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="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;16px Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;Simple.&lt;/p&gt;

&lt;p&gt;But the useful part is when the case gets more specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middle truncation
&lt;/h2&gt;

&lt;p&gt;Some strings should not only be cut from the end.&lt;/p&gt;

&lt;p&gt;Filenames.&lt;/p&gt;

&lt;p&gt;URLs.&lt;/p&gt;

&lt;p&gt;Emails.&lt;/p&gt;

&lt;p&gt;Hashes.&lt;/p&gt;

&lt;p&gt;IDs.&lt;/p&gt;

&lt;p&gt;Anything where the start and end both matter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;truncateMiddle&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="s2"&gt;@tonybonet/truncate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;truncateMiddle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;really-long-file-name-final-final-v12.pdf&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="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;14px Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&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;Because this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;really-long-file-name...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not always as useful as this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;really-long...v12.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small detail.&lt;/p&gt;

&lt;p&gt;Big difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search aware truncation
&lt;/h2&gt;

&lt;p&gt;Search results should not blindly cut text and hope the useful part survives.&lt;/p&gt;

&lt;p&gt;If someone is searching inside logs, errors, docs, or long records, the preview should try to keep the match visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;truncateAround&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="s2"&gt;@tonybonet/truncate&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;truncateAround&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;14px Geist Mono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cannot read properties of undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&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;Because this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-06-03 14:22:10] production-api-worker...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not as useful as this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...TypeError: Cannot read properties of undefined...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That small difference makes search results, command palettes, logs, support tools, and debugging UIs feel much more intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Range aware truncation
&lt;/h2&gt;

&lt;p&gt;Sometimes you already know the exact part of the text that matters.&lt;/p&gt;

&lt;p&gt;Maybe it came from search.&lt;/p&gt;

&lt;p&gt;Maybe it came from a parser.&lt;/p&gt;

&lt;p&gt;Maybe it came from your own indexing layer.&lt;/p&gt;

&lt;p&gt;So you can preserve that range directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;truncateRange&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="s2"&gt;@tonybonet/truncate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;truncateRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;16px Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&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 API does not only return the text.&lt;/p&gt;

&lt;p&gt;It also gives you metrics.&lt;/p&gt;

&lt;p&gt;So if the range was preserved, you know.&lt;/p&gt;

&lt;p&gt;If it could not fit, you know.&lt;/p&gt;

&lt;p&gt;No fake confidence.&lt;/p&gt;

&lt;p&gt;No silent weirdness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom markers
&lt;/h2&gt;

&lt;p&gt;You are not locked into &lt;code&gt;...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes you want something more explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;truncateByWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;articleIntro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;16px Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ellipsis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; READ MORE&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes you want something smaller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;truncateByWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mediaTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;16px Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ellipsis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, tiny detail.&lt;/p&gt;

&lt;p&gt;But UI is full of tiny details.&lt;/p&gt;

&lt;h2&gt;
  
  
  DOM free by default
&lt;/h2&gt;

&lt;p&gt;Truncate does not depend on reading layout from the page.&lt;/p&gt;

&lt;p&gt;At the core, it measures through Canvas2D.&lt;/p&gt;

&lt;p&gt;That makes it useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browser apps&lt;/li&gt;
&lt;li&gt;OffscreenCanvas&lt;/li&gt;
&lt;li&gt;Node canvas polyfills&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;li&gt;server side utilities&lt;/li&gt;
&lt;li&gt;pre rendered snippets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes you do not want to wait for the DOM to decide what the text should be.&lt;/p&gt;

&lt;p&gt;You want the string ready before it gets there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yes, it even supports GIFs
&lt;/h2&gt;

&lt;p&gt;Because every tiny UI utility eventually meets the weird case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @tonybonet/truncate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @tonybonet/truncate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Playground:&lt;br&gt;
&lt;a href="https://tonyblu331.github.io/truncate/" rel="noopener noreferrer"&gt;https://tonyblu331.github.io/truncate/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/truncate?tab=readme-ov-file#api" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/truncate?tab=readme-ov-file#api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo:&lt;br&gt;
&lt;a href="https://github.com/tonyblu331/truncate" rel="noopener noreferrer"&gt;https://github.com/tonyblu331/truncate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have a weird truncation case, send it my way.&lt;/p&gt;

&lt;p&gt;That is probably the best test for this.&lt;/p&gt;

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