CSS border-shape: draw a border in any shape

border-shape takes a basic shape such as circle() or polygon() and draws the element's border along it. The box keeps its place in the layout, and the shadow follows the new outline.

border-shape is a CSS property that draws an element's border along a shape instead of the box edge. You give it a basic shape, such as circle(), polygon() or shape(), and the border, background and shadow follow that outline.

Try it first. The line under the shapes says whether your browser supports the property.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS border-shape basics</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
  .item { text-align: center; font-size: 12px; color: #5b6270; }
  .item code { display: block; margin-top: 8px; }

  /* 1. One shape: the border is drawn as a stroke along the circle */
  .circle {
    width: 78px; height: 78px; display: grid; place-items: center;
    background: #ffe27a; border: 8px solid #0a7d55;
    border-shape: circle(50%);
  }

  /* 2. Any basic shape works, here an arrow-shaped tag */
  .tag {
    width: 108px; height: 66px; display: grid; place-items: center;
    background: #dcebff; border: 5px solid #1d4ed8;
    border-shape: polygon(0 0, 80% 0, 100% 50%, 80% 100%, 0 100%);
  }

  /* 3. Two shapes: the border color fills the area between them */
  .ring {
    width: 78px; height: 78px; display: grid; place-items: center;
    background: #fff; border: 16px solid #e0457b;
    border-shape: inset(0 round 18px) circle(50%);
  }

  /* Browsers without border-shape: at least keep the circle round */
  @supports not (border-shape: circle(50%)) {
    .circle { border-radius: 50%; }
  }

  #status { margin: 16px 0 0; font-size: 13px; padding: 8px 10px; border-radius: 8px; }
  .yes { background: #d6f2df; color: #0f5132; }
  .no { background: #fde2da; color: #9a3412; }
</style>
</head>
<body>
<div class="row">
  <div class="item"><div class="circle">Hi</div><code>circle(50%)</code></div>
  <div class="item"><div class="tag">Sale</div><code>polygon(...)</code></div>
  <div class="item"><div class="ring">In</div><code>two shapes</code></div>
</div>
<p id="status"></p>

<script>
  // Feature check: tells the reader what their browser is doing
  const ok = CSS.supports('border-shape', 'circle(50%)');
  const status = document.getElementById('status');
  status.className = ok ? 'yes' : 'no';
  status.textContent = ok
    ? 'This browser supports border-shape. You are seeing the real shapes.'
    : 'This browser does not support border-shape, so you see plain boxes (the circle uses a border-radius fallback).';
</script>
</body>
</html>
A circle, an arrow tag and a two-shape ring. Edit the code and the example reruns.

The rule is one line on top of a normal border:

.circle {
  border: 8px solid #0a7d55;
  border-shape: circle(50%);
}

Browser support: check before you rely on it

MDN marks border-shape as experimental and not Baseline. It is defined in CSS Borders and Box Decorations Module Level 4, which is still a Working Draft.

MDN's compatibility data, checked on 30 September 2026, lists support in Chrome and in browsers that share its engine, such as Edge and Opera. It lists no support in Firefox or Safari.

We ran every example on this page in a Chromium build that supports the property.

In a Firefox build, the same pages showed the fallback: plain boxes, with the circle kept round by border-radius. Those browsers ignore the unknown property and draw an ordinary rectangular border.

For that reason, give each shaped element a fallback inside @supports:

@supports not (border-shape: circle(50%)) {
  .circle { border-radius: 50%; }
}

In JavaScript, CSS.supports() returns the same answer. The first example uses it to print the status line:

CSS.supports('border-shape', 'circle(50%)'); // true or false

One shape or two: stroke mode and fill mode

The number of shapes you give changes what the border becomes.

One shape draws the border as a line along the path. Two shapes fill the band between them.
One shape draws the border as a line along the path. Two shapes fill the band between them.
You write Mode What is drawn
One shape Stroke A line along the path, as thick as the border width
Two shapes Fill The area between the first (outer) and second (inner) shape

In both modes the color is the border color. In fill mode, the border width does not set a stroke. It moves the inner shape, as the next section shows.

/* stroke: a line along a circle */
border-shape: circle(50%);

/* fill: a rounded square with a round hole */
border-shape: inset(0 round 18px) circle(50%);

Which box the shape is measured on

A percentage such as circle(50%) needs a box to be measured against. You can name one after each shape, for example circle(50%) content-box. If you do not, the defaults depend on the mode.

With one shape the path runs through the middle of the border. With two, the outer shape uses the border box and the inner one the padding box.
With one shape the path runs through the middle of the border. With two, the outer shape uses the border box and the inner one the padding box.
  • One shape: the default is half-border-box, a box halfway through the border. The stroke extends equally inward and outward from the path, the same way a normal border sits.
  • Two shapes: the outer shape is measured on the border box and the inner one on the padding box. A wider border therefore gives a thicker filled band.

border-shape vs clip-path

clip-path also takes basic shapes, so the two look alike in code. They do different things. clip-path hides every part of the element outside the shape. border-shape moves the border onto the shape.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>border-shape vs clip-path</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .pick { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
  .pick button { font: inherit; font-size: 13px; padding: 6px 10px; border-radius: 8px; border: 1px solid #c9cdd4; background: #fff; cursor: pointer; }
  .pick button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .col { text-align: center; }
  .col h3 { margin: 0 0 12px; font-size: 14px; font-family: ui-monospace, Consolas, monospace; }
  .col p { margin: 12px 0 0; font-size: 12px; color: #5b6270; }

  .box {
    --s: polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%);
    width: 130px; height: 120px; margin: 0 auto;
    display: grid; place-items: center; font-weight: 600;
    background: #ffe27a; border: 6px solid #1d4ed8;
    box-shadow: 8px 8px 0 rgba(0, 0, 0, .25);
  }
  .shaped { border-shape: var(--s); }  /* border and shadow follow the shape */
  .clipped { clip-path: var(--s); }    /* everything outside the shape is cut off */
</style>
</head>
<body>
<div class="pick" id="pick">
  <button aria-pressed="true" data-s="polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%)">Pentagon</button>
  <button data-s="polygon(0 0, 80% 0, 100% 50%, 80% 100%, 0 100%)">Arrow</button>
  <button data-s="ellipse(50% 40%)">Ellipse</button>
</div>
<div class="cols">
  <div class="col"><h3>border-shape</h3><div class="box shaped">Box</div><p>Border and shadow follow the edge.</p></div>
  <div class="col"><h3>clip-path</h3><div class="box clipped">Box</div><p>The border and shadow are cut away.</p></div>
</div>

<script>
  const boxes = document.querySelectorAll('.box');
  const buttons = document.querySelectorAll('#pick button');
  buttons.forEach((btn) => {
    btn.addEventListener('click', () => {
      // one custom property feeds both properties
      boxes.forEach((b) => b.style.setProperty('--s', btn.dataset.s));
      buttons.forEach((x) => x.setAttribute('aria-pressed', x === btn));
    });
  });
</script>
</body>
</html>
The same shape and border on both boxes. Pick a shape with the buttons.
border-shape keeps the border and the shadow. clip-path cuts both away where they fall outside.
border-shape keeps the border and the shadow. clip-path cuts both away where they fall outside.

With border-shape, an outer box-shadow is cast as if the outer shape were solid, so the shadow takes the shape too. With clip-path, the rectangular border is only visible where it overlaps the shape, and the shadow outside the shape is removed.

Use clip-path to cut something out, such as a photo in a hexagon. Use border-shape when the shape needs a visible outline or a shadow.

What it does not change

border-shape is a purely visual effect. The element keeps its rectangular size in the layout, worked out from the normal border widths. Text inside the box flows as it would in a rectangle.

That has three consequences you will run into:

  • Content can stick out. A long line can cross the shaped edge. Add padding, or set overflow: hidden, which clips content to the inner shape.
  • border-radius is ignored. When border-shape is not none, the spec treats border-radius as 0 and ignores corner-shape. Round corners inside the shape instead: inset(0 round 18px). The border-radius guide covers the rectangular case.
  • Only some border properties apply. MDN lists border-color and border-width as used. border-image is not applied, and border-style always renders as solid, so a dashed border comes out as a solid line.

When the four sides have different borders, one side decides. The spec picks the first side, in logical order, whose style is not none. In a normal left-to-right page that is the top border, so its width and color are used.

A finished example: shaped buttons and callouts

These three pieces use nothing but border-shape: a speech bubble with a tail, a ticket button with notches, and a ring badge.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shaped buttons and callouts</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .grid { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }

  /* Speech bubble: the tail is part of the shape, the box stays rectangular in layout */
  .bubble {
    width: 200px; padding: 14px 16px 34px; font-size: 14px; line-height: 1.4;
    background: #fff; border: 3px solid #1d4ed8;
    border-shape: polygon(0 0, 100% 0, 100% 78%, 34% 78%, 18% 100%, 20% 78%, 0 78%);
  }

  /* Ticket button: notches on both sides. Same vertex count in both states, so it animates */
  .ticket {
    font: 600 15px system-ui, sans-serif; color: #0f5132; cursor: pointer;
    padding: 16px 30px; background: #d6f2df; border: 3px solid #0f5132;
    border-shape: polygon(0 0, 100% 0, 100% 35%, 94% 50%, 100% 65%, 100% 100%, 0 100%, 0 65%, 6% 50%, 0 35%);
    transition: border-shape .25s ease;
  }
  .ticket:hover, .ticket:focus-visible {
    border-shape: polygon(0 0, 100% 0, 100% 30%, 88% 50%, 100% 70%, 100% 100%, 0 100%, 0 70%, 12% 50%, 0 30%);
  }

  /* Ring badge: two shapes, the border color fills between them */
  .badge {
    width: 70px; height: 70px; display: grid; place-items: center;
    font: 700 20px system-ui, sans-serif; color: #9a3412;
    background: #fff4ec; border: 12px solid #ea580c;
    border-shape: circle(50%) circle(38%);
  }

  /* Fallback: without border-shape, use rounded boxes so nothing looks broken */
  @supports not (border-shape: circle(50%)) {
    .bubble { border-radius: 12px; padding-bottom: 14px; }
    .ticket { border-radius: 10px; }
    .badge { border-radius: 50%; }
  }

  #log { margin-top: 14px; font-size: 13px; color: #5b6270; }
</style>
</head>
<body>
<div class="grid">
  <div class="bubble">The tail is drawn by border-shape. Layout still sees a rectangle.</div>
  <div>
    <button class="ticket" id="ticket">Get a ticket</button>
    <p id="log">Hover or focus the button to see the notches grow.</p>
  </div>
  <div class="badge" id="badge">3</div>
</div>

<script>
  // Clicking the ticket bumps the badge number
  const badge = document.getElementById('badge');
  document.getElementById('ticket').addEventListener('click', () => {
    badge.textContent = Number(badge.textContent) + 1;
    document.getElementById('log').textContent = 'Ticket added. Badge shows ' + badge.textContent + '.';
  });
</script>
</body>
</html>
Hover or focus the ticket to see the notches grow. Click it to count up the badge.
  • Speech bubble: the tail is part of the polygon. Extra bottom padding keeps the text out of the tail.
  • Ticket: both polygons have ten points, so a transition animates between them.
  • Ring badge: two circles, so the orange border fills the band between them.
  • Fallback: without support, the @supports not block gives each piece rounded corners instead.

Animation only works between shapes that match. Two polygon() values need the same number of points. A circle() changing into a polygon() jumps instead of animating.

When it does not work

What you see Cause Fix
A plain rectangle The browser does not support border-shape Add an @supports not fallback
The dashed border is solid border-style is not applied Expected. Draw dashes another way, such as SVG
border-radius has no effect It is ignored when a shape is set Use inset(0 round 18px)
Text crosses the shaped edge The shape does not change layout More padding, or overflow: hidden
The shape jumps instead of animating The two shapes cannot interpolate Same function, same number of points
The stroke has the wrong width Another side decides Use one border for all four sides

A shaped border is easier to judge in a browser than in a screenshot. Sending the page also lets the other person see whether their own browser draws the shape or the fallback.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can hover and click the shapes themselves. If you change the code later, the same link shows the new version.

Questions people ask

Which browsers support border-shape?

MDN marks border-shape as experimental and not Baseline. Its compatibility data, checked on 30 September 2026, lists support in Chrome and in browsers that share its engine, such as Edge and Opera, and no support in Firefox or Safari. Add an @supports fallback for the rest.

What is the difference between border-shape and clip-path?

clip-path hides every part of the element outside the shape, including the border and the shadow. border-shape changes where the border is drawn, so the border runs along the whole shape and an outer box-shadow is cast from the shape.

Can I use border-radius together with border-shape?

No. When border-shape is not none, the spec says border-radius is ignored as if it were 0, and corner-shape is ignored with it. Put the rounding inside the shape instead, for example inset(0 round 18px).

Does border-shape change the size of the element?

No. The spec calls it a purely visual effect. Layout is still worked out from the normal border widths, and content inside the box flows as it would in a rectangle.

Can border-shape be animated?

Yes, between shapes that can be interpolated: the same function with the same number and type of values, for example two polygon() values with the same number of points. Between other pairs the change is discrete, so it jumps.

Keep reading