Why Do We Use width: 0; height: 0;?

width: 0; height: 0; looks like a bug waiting to happen, yet it powers arrows, carets, pointers, and many small UI details across the web. By the end of this article, you will know exactly why developers set both dimensions to zero, how border-only geometry works, and how to build three practical components: a tooltip arrow, a dropdown caret, and a play icon triangle. You will also learn when this trick is the right tool, and when to reach for another shape technique.

Why width: 0; height: 0; Matters

Setting an element to zero width and height leaves no visible content box. Borders still render, though. This is the key. When only borders draw, the meeting point of angled borders forms sharp shapes. Triangles are the most famous example: make two adjacent borders transparent, one border colored, and the fourth border zero, and you get a crisp triangle that scales cleanly on any display. This pattern costs almost nothing to paint, works in all modern browsers, and does not need images or SVG for basic pointers. Many shape recipes in a UI kit lean on it because it is easy to theme, size, and position.

Prerequisites

You do not need a lot to follow along, just a few modern CSS basics and a text editor. We will write small, focused HTML and CSS snippets and keep the JavaScript out of the way.

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The markup below holds three small components that will demonstrate width: 0; height: 0; in action: a button with a caret, a tooltip with an arrow, and a circular play button. Each shape is a span inside the component. That keeps semantics clean while giving us a hook for the border-only triangle.

<!-- HTML -->
<div class="demo">
  <button class="btn btn--dropdown" aria-haspopup="listbox" aria-expanded="false">
    Options
    <span class="caret" aria-hidden="true"></span>
  </button>

  <div class="tooltip" role="tooltip" id="tip-1">
    <div class="tooltip__bubble">Saved</div>
    <span class="tooltip__arrow" aria-hidden="true"></span>
  </div>

  <button class="play" aria-label="Play">
    <span class="play__triangle" aria-hidden="true"></span>
  </button>
</div>

Step 2: The Basic CSS & Styling

Start with a small design system using CSS variables. Colors, sizes, and radii live in :root to make the triangles tunable. We also set up a simple layout so you can see the components side-by-side. None of the shapes exist yet; that comes next.

/* CSS */
:root {
  --bg: #0b0f14;
  --text: #e6edf3;
  --muted: #9fb1c1;
  --surface: #121820;
  --surface-2: #1b2430;
  --accent: #36c;
  --success: #1fa971;

  --r: 10px;
  --gap: 24px;

  /* Triangle sizes */
  --caret-size: 6px;
  --tooltip-arrow: 8px;
  --play-size: 14px;

  /* Motion */
  --easing: cubic-bezier(.2, .6, .2, 1);
  --time: 160ms;
}

*,
*::before,
*::after { box-sizing: border-box; }

html, body {
  height: 100%;
  background: linear-gradient(180deg, var(--bg), #0c1219);
  color: var(--text);
  font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
  margin: 0;
}

.demo {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap);
  padding: 40px;
  max-width: 900px;
  margin-inline: auto;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-radius: var(--r);
  border: 1px solid #243042;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: border-color var(--time) var(--easing), background var(--time) var(--easing);
}

.btn:hover { border-color: #334355; background: #141c26; }

.btn--dropdown { justify-self: start; }

.tooltip {
  position: relative;
  justify-self: center;
  align-self: start;
}

.tooltip__bubble {
  padding: 10px 12px;
  background: var(--surface-2);
  border-radius: calc(var(--r) - 4px);
  border: 1px solid #2b3a4f;
  color: var(--text);
  box-shadow: 0 6px 24px rgba(0,0,0,.35);
  width: fit-content;
}

.play {
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid #243042;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  justify-self: end;
  transition: transform var(--time) var(--easing), border-color var(--time) var(--easing), background var(--time) var(--easing);
}

.play:hover { transform: translateY(-2px); border-color: #334355; background: #141c26; }

Advanced Tip: Keep triangle sizing in variables (for example, –caret-size). Triangles scale from border widths, not box dimensions. Variables let you change the pointer size in one place without recalculating every border.

Step 3: Building the Tooltip Arrow

The tooltip arrow sits under the bubble and points down at the trigger. This is the classic border-only triangle. The element has width: 0; height: 0; so only borders paint. Two borders are transparent to form the sides, the top border is colored, and the bottom border is zero to remove the extra edge.

/* CSS */
.tooltip {
  /* already positioned in the base styles */
}

.tooltip__arrow {
  position: absolute;
  left: 24px; /* adjust to align with your trigger */
  bottom: calc(-1 * var(--tooltip-arrow));
  width: 0;
  height: 0;

  border-left: var(--tooltip-arrow) solid transparent;
  border-right: var(--tooltip-arrow) solid transparent;
  border-top: var(--tooltip-arrow) solid var(--surface-2);
  border-bottom: 0;

  /* match the stroke of the bubble by adding a shadow layer via outline effect */
  filter: drop-shadow(0 -1px 0 #2b3a4f);
  pointer-events: none;
}

How This Works (Code Breakdown)

Setting width and height to zero means the element contributes no rectangle of its own. Borders still render their mitered corners and meet at a point. The left and right borders are set to the same width and transparent color, which forms the two equal sides of the triangle. The top border carries the tooltip background color, so the arrow visually merges with the bubble. The bottom border is zero, since we do not want a second spike below. That single colored border becomes the triangle face.

Absolute positioning places the triangle directly under the bubble. bottom: calc(-1 * var(–tooltip-arrow)) sinks the arrow by exactly its own height. That aligns the tip with the outer edge of the bubble, avoiding gaps. The drop-shadow trick draws a one-pixel stroke that matches the bubble border, which helps on dark backgrounds.

If you want the pointer above the bubble, flip to border-bottom for the colored side and move the arrow to top: calc(-1 * var(–tooltip-arrow)). A full tutorial on triangles is in the library. For a direct recipe, see the triangle-down guide. If you want to build a full speech bubble with tail variations and rounded corners, the speech bubble with CSS article walks through several patterns.

Note: The triangle is visually part of the bubble, but it should not receive focus or clicks. pointer-events: none on the arrow keeps interactions on the bubble or trigger.

Step 4: Building the Caret and Play Triangle

Carets and play icons are perfect use cases for border-only triangles. The caret is an inline triangle that follows text, while the play icon is a right-pointing triangle centered in a circular button. Both rely on width: 0; height: 0; with three borders shaping the geometry.

/* CSS */
.caret {
  display: inline-block;
  margin-left: 8px;
  width: 0;
  height: 0;

  border-left: var(--caret-size) solid transparent;
  border-right: var(--caret-size) solid transparent;
  border-top: var(--caret-size) solid currentColor; /* down-facing caret */
  transition: transform var(--time) var(--easing);
}

.btn--dropdown[aria-expanded="true"] .caret {
  transform: rotate(180deg); /* points up when open */
}

.play__triangle {
  width: 0;
  height: 0;

  /* right-pointing triangle */
  border-top: calc(var(--play-size) * 0.6) solid transparent;
  border-bottom: calc(var(--play-size) * 0.6) solid transparent;
  border-left: var(--play-size) solid var(--accent);
  border-right: 0;
}

How This Works (Code Breakdown)

The caret uses currentColor for the tinted face so it inherits from the button text. That makes theming trivial. When the dropdown opens, aria-expanded flips to true and we rotate the caret. Rotating the triangle is cheap since the triangle is just borders, and we are requesting a transform, which maps to the compositor.

The play button creates a longer base by placing the colored face on border-left. The top and bottom borders are transparent and set to 60 percent of the base to get a comfortable aspect ratio. If you want a slimmer or wider triangle, adjust those multipliers. A full reference for this shape lives in the triangle-right tutorial. You can also swap to border-right for a left-pointing version.

Both triangles stay crisp at any scale because they are vector-like. No extra HTTP requests, no alignment issues across pixel densities, and minimal painting cost. That is why border triangles still show up in production UI kits even when SVG is available. For very complex icons, SVG is the better choice, but for pointers and chevrons, width: 0; height: 0; is quick and reliable.

Warning: Do not animate border-width on these shapes. Some browsers re-raster borders during the animation, which can look jittery. Animate transform, opacity, or color for smooth results.

Advanced Techniques: Adding Animations & Hover Effects

Small motion can make pointers feel alive without distracting from content. The snippets below rotate the caret during open and add a soft pulse to the play triangle. The tooltip arrow gets a subtle rise on hover to hint at interactivity around it.

/* CSS */
.btn--dropdown:hover .caret {
  transform: translateY(-1px);
}

.play:hover .play__triangle {
  filter: drop-shadow(0 0 6px rgba(54, 102, 204, 0.55));
}

@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-2px); }
}

.tooltip:hover .tooltip__arrow {
  animation: nudge 600ms var(--easing);
}

/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
  .btn--dropdown:hover .caret,
  .play:hover,
  .tooltip:hover .tooltip__arrow {
    animation: none;
    transition: none;
    transform: none;
  }
}

The caret hover uses a tiny translateY to make it feel responsive. The play triangle gets a drop-shadow glow on hover, which pairs well with an accent color. The tooltip arrow animation runs once on hover using a simple keyframe. A media query silences motion for users who prefer less movement.

Accessibility & Performance

Accessibility

Keep triangles decorative unless they convey status on their own. The caret and the tooltip arrow do not carry meaning; they only support nearby text. Mark them aria-hidden=”true”. The play button is an interactive control, so it needs an accessible name. aria-label=”Play” on the button is clear and predictable. For toggles, sync the caret with aria-expanded; screen readers announce state, and the visual and semantic states match.

Targets should still be easy to hit. The triangle itself is tiny, so the interactive area should live on a larger button or link. This avoids frustration on touch screens. When a tooltip is purely visual, you can use role=”tooltip” on the bubble and point it at a trigger with aria-describedby. Triangles that are part of text (for example, a caret in a heading) do not need such wiring.

Performance

Border-based triangles are cheap. They use only borders and a single layer, so the paint and memory costs are small. Transforms and opacity animate via the compositor on most devices, which keeps frames smooth. Avoid animating border-width or box-shadow continually, as those can trigger repaints on every frame. If you need more complex geometry than triangles or need to morph shapes, consider SVG for clarity and maintainability. For basic UI pointers, width: 0; height: 0; hits a sweet spot between quality and speed.

Tip: If you see blurry edges on scaled triangles, check fractional positioning. Place the triangle on whole pixels or wrap it in a positioned container and align with translate(-50%) to reduce subpixel artifacts.

Where width: 0; height: 0; Shines, and Where It Does Not

This pattern shines when you need crisp directional markers: arrows for tooltips, dropdown carets, tabs with notches, ribbons with cut corners, and small pointers on callouts. It saves you from adding extra markup or shipping assets. It also works well with themes since border colors can use currentColor or CSS variables.

There are places where it is the wrong tool. If you need a perfect circle, use border-radius or a dedicated recipe like the circle guide, not borders on a zero-sized element. For full callout components that mix bubbles, tails, and badges, a layout-driven approach may be clearer. When your shape needs rounded, asymmetric, or multi-segment edges, take a look at other entries in the shapes library, such as ribbons or callouts. Triangle-based tails still help in those builds even if the main shape is drawn another way.

The last closing paragraph

From an empty box to a full UI detail, width: 0; height: 0; turns borders into reliable geometry. You built a tooltip arrow, a dropdown caret, and a play icon triangle, and learned why this method remains a staple in production CSS. Keep this trick handy, and apply it alongside the rest of your shape toolkit as you design sharper interfaces.

Leave a Comment