border-shape CSS property

Limited availability

This feature is not Baseline because it does not work in some of the most widely-used browsers.

Want more browser support for this feature? Tell us why.

Experimental: This is an experimental technology
Check the Browser compatibility table carefully before using this in production.

The border-shape CSS property defines an element's border shape using <basic-shape> values.

Syntax

css
/* Keyword */
border-shape: none;

/* Single <basic-shape> value */
border-shape: circle(50%);
border-shape: rect(10px 460px 130px 20px round 20px);
border-shape: shape(
  from 5% 0.5rem,
  arc to 80px 1pt of 10% ccw large rotate 25deg
);

/* Two <basic-shape> values */
border-shape: circle(50%) ellipse(40% 30%);
border-shape: polygon(0% 0%, 0% 100%, 100% 0%)
  polygon(10% 10%, 10% 70%, 70% 10%);

/* <basic-shape> and <geometry-box> values */
border-shape: circle(50%) border-box ellipse(40% 30%) view-box;
border-shape: rect(5px 198px 189px 0px round 20px) view-box circle(50%);
border-shape: path(
    "M 35,95 C 35,50 60,15 100,20 C 120,5 160,5 180,22 C 200,5 250,5 270,22 C 295,5 340,5 360,22 C 395,10 440,35 440,75 C 455,90 450,120 430,128 C 400,145 360,145 330,130 C 300,145 260,145 230,130 C 200,145 160,145 130,130 C 80,142 35,120 35,95 Z"
  )
  view-box;

/* Global values */
border-shape: inherit;
border-shape: initial;
border-shape: revert;
border-shape: revert-layer;
border-shape: unset;

The border-shape property may be specified using the keyword none, or one or two space-separated shape definitions, each consisting of a <basic-shape> value or a <basic-shape> value and a <geometry-box> value.

Values

none

Specifies that no border shape is defined. This is the initial value.

<basic-shape>

Defines the shape of the border using one of the <basic-shape> functions.

<geometry-box> Optional

Defines the reference box relative to which the border shape is drawn. If not included, the shape's reference geometry box defaults to:

  • half-border-box if a single basic shape is specified, which means that any defined border is drawn on top of the shape path, with the path going down its center.
  • border-box for the first (outer) shape and padding-box for the second (inner) shape if two basic shapes are specified. The border then occupies the area between the two shapes.

Description

The border-shape property can be applied to elements to create precisely-shaped containers such as speech bubbles, as shown here, or abstract tooltip designs, without having to use workarounds.

Note how the applied border-shape plays well with the border and box-shadow defined on the element — they follow the shape of the border.

Supported shape functions

You can define an element's border-shape (including that of an inline or pseudo-element) using one of the <basic-shape> functions. These functions include:

  • inset(), rect(), and xywh(): Provide different ways to define basic rectangle shapes.
  • circle(): Defines circle shapes.
  • ellipse(): Defines ellipse shapes.
  • path(): Defines any kind of shape using SVG path string syntax. SVG path syntax has limitations — it can use only pixel values and the path has to be defined as a single string, so custom properties can't be included via var(). You are advised to use shape() instead.
  • polygon(): Defines any kind of polygon via pairs of vertex coordinates. If your desired shape includes smooth curves, you are advised to use shape().
  • shape(): Defines any kind of shape. The syntax of shape() is more CSS-compatible than that of path() and solves its shortcomings.

You can also include an optional <geometry-box> keyword after each <basic-shape> value to specify the reference box relative to which the shapes should be drawn.

Stroke and fill modes

The border-shape property has the following two modes:

  • If a single <basic-shape> is provided in the value, that shape defines the shape of the element's border, with the defined border styles drawn as a stroke around the shape, as shown in the previous example. This is known as stroke mode.
  • If two <basic-shape>s are provided in the value, the first shape defines the outer boundary of the border, the second shape defines the inner boundary of the border, and any defined border color fills the area between the two boundaries. This is known as fill mode.

Note: Avoid defining a shape for the inner boundary that's larger than the outer boundary. If you do this, the border area does not render properly; you might end up with no border fill rendered or one shape rendered behind the other.

Effect on layout and rendering

The border-shape property creates a purely visual effect — the element's layout is still computed using the underlying rectangular box definition, and the content flow is not affected.

The element's content and background are clipped by the border-shape property (the inner shape in fill mode). When the specified border-shape is the same size or smaller than the content/background, you won't need to adjust the reference box, unless you want to create some kind of offset effect. However, when the specified border-shape is larger than the content/background, you will see gaps between the edge of the background and the shape(s). In such cases, you may need to use a different reference box to fix the display (see Handling border-shapes larger than the element's background for more information).

Limitations on border styles applied to border shapes

Not all border styles are applied to elements with a border-shape property set. The following list explains how each property is affected:

  • border-color: This property is applied. However, when multiple element borders have different colors applied, the browser chooses the first edge with a border color, in the following order:

    • Block start edge
    • Inline start edge
    • Block end edge
    • Inline end edge

    The browser then applies that edge's border color to the entire rendered border-shape.

  • border-image: Not applied.

  • border-style: Not applied. All borders are rendered with a solid style.

  • border-width: In stroke mode, border-width is applied directly to the rendered border-shape. When multiple edges have different border widths, the browser selects a width to apply to the entire border using the same process as described for border-color.

    In fill mode, the border area is defined by the difference between the areas of the outer and inner shapes; therefore, border-width doesn't have any direct effect on the width of the rendered border. However, it does have an indirect effect — it still affects the size of the reference boxes the shapes are drawn relative to (unless you set their <geometry-box> to content-box or padding-box). You therefore still need to be mindful of the border-width set on the underlying element while using fill mode.

As an example, if an element has the following declarations applied:

css
border-shape: rect(5px 198px 189px 0px round 20px);
border-bottom: 30px dashed blue;
border-left: 40px dotted hotpink;
border-right: 50px double yellow;

The rendered box will have a rectangular border with rounded corners. The border style will be solid because other styles are ignored. The border width and color will be 40px and hotpink, respectively – this is because the border-left property applies styles to the inline start edge (assuming that the page has a horizontal writing-mode), which is the first edge with border styles; this gets chosen by the browser per the priority list described earlier.

Interaction with border-radius and corner-shape

The border-radius and corner-shape properties are incompatible with border-shape. When a border-shape is set on an element, any set border-radius is ignored, therefore corner-shape will also have no effect. The border-shape and border-radius/corner-shape properties have different effects, and are used separately.

If you want to use shaped corners in a border-shape, you will have to draw them directly as part of the shape.

border-shape versus clip-path

The clip-path property takes values similar to border-shape and produces similar effects. Both properties can be used to change the shape of an element and therefore the element's hit area, altering the boundary inside which :hover effects and pointer-related events activate.

However, there is a fundamental difference in how the two properties work:

  • clip-path hides the area of the element that sits outside the region defined by the provided shape.
  • border-shape changes the visual rendering of the element so that it sits inside the region defined by the provided shape.

This means border-shape clips the element's content while still letting the overflow property control its display, whereas clip-path hides the element's content altogether, so overflow control is not possible.

More significantly, properties such as box-shadow and outline do not follow the shape created by clip-path — it chops off the outside of the element, meaning that such effects are truncated in an ugly fashion or removed altogether. The border-shape property, on the other hand, creates a differently-shaped border that is neatly followed by such effects.

Handling border-shapes larger than the element's background

As mentioned earlier, one issue with border-shape is that when you define a shape that is larger than the element's content/background, you can end up with a gap between the background and the border.

The recommended approach to fixing this is to set the reference <geometry-box> to content-box. and then use padding to fill in the gaps between the content and the border. For example:

css
border-shape: shape(
    from 0% 0%,
    hline to 33%,
    arc by 33% 0% of 16% 20% small cw,
    hline to 100%,
    line to 100% 33%,
    arc by 0% 33% of 20% 16% small cw,
    line to 100% 100%,
    hline to 66%,
    arc by -33% 0% of 16% 20% small ccw,
    hline to 0%,
    line to 0% 66%,
    arc by 0% -33% of 20% 16% small ccw,
    close
  )
  content-box;
padding: 24px;

This way, the padding will be set outside the shape, causing it to get smaller, and forcing the background to fill up the parts of the shape extending outside the content area. You can see this technique in action in our irregular jigsaw piece navigation menu example.

Formal definition

Value not found in DB!

Formal syntax

border-shape = 
none |
[ <basic-shape> <geometry-box>? ]{1,2}

<basic-shape> =
<basic-shape-rect> |
<circle()> |
<ellipse()> |
<polygon()> |
<path()> |
<shape()>

<geometry-box> =
<shape-box> |
fill-box |
stroke-box |
view-box

<basic-shape-rect> =
<inset()> |
<rect()> |
<xywh()>

<circle()> =
circle( <radial-size>? [ at <position> ]? )

<ellipse()> =
ellipse( <radial-size>? [ at <position> ]? )

<polygon()> =
polygon( <'fill-rule'>? [ round <length> ]? , [ <length-percentage> <length-percentage> ]# )

<path()> =
path( <'fill-rule'>? , <string> )

<shape()> =
shape( <'fill-rule'>? from <position> , <shape-command># )

<shape-box> =
<visual-box> |
margin-box |
half-border-box

<inset()> =
inset( <length-percentage>{1,4} [ round <'border-radius'> ]? )

<rect()> =
rect( <top> , <right> , <bottom> , <left> )

<xywh()> =
xywh( <length-percentage>{2} <length-percentage [0,∞]>{2} [ round <'border-radius'> ]? )

<radial-size> =
<radial-extent> |
<length [0,∞]> |
<length-percentage [0,∞]>{2}

<position> =
<position-one> |
<position-two> |
<position-four>

<fill-rule> =
nonzero |
evenodd

<length-percentage> =
<length> |
<percentage>

<shape-command> =
<move-command> |
<line-command> |
close |
<horizontal-line-command> |
<vertical-line-command> |
<curve-command> |
<smooth-command> |
<arc-command>

<visual-box> =
content-box |
padding-box |
border-box

<border-radius> =
<length-percentage [0,∞]>{1,4} [ / <length-percentage [0,∞]>{1,4} ]?

<radial-extent> =
closest-corner |
closest-side |
farthest-corner |
farthest-side

<position-one> =
left |
center |
right |
top |
bottom |
x-start |
x-end |
y-start |
y-end |
block-start |
block-end |
inline-start |
inline-end |
<length-percentage>

<position-two> =
[ left | center | right | x-start | x-end ] && [ top | center | bottom | y-start | y-end ] |
[ left | center | right | x-start | x-end | <length-percentage> ] [ top | center | bottom | y-start | y-end | <length-percentage> ] |
[ block-start | center | block-end ] && [ inline-start | center | inline-end ] |
[ start | center | end ]{2}

<position-four> =
[ [ left | right | x-start | x-end ] <length-percentage> ] && [ [ top | bottom | y-start | y-end ] <length-percentage> ] |
[ [ block-start | block-end ] <length-percentage> ] && [ [ inline-start | inline-end ] <length-percentage> ] |
[ [ start | end ] <length-percentage> ]{2}

<move-command> =
move <command-end-point>

<line-command> =
line <command-end-point>

<horizontal-line-command> =
hline [ to [ <length-percentage> | left | center | right | x-start | x-end ] | by <length-percentage> ]

<vertical-line-command> =
vline [ to [ <length-percentage> | top | center | bottom | y-start | y-end ] | by <length-percentage> ]

<curve-command> =
curve [ [ to <position> with <control-point> [ / <control-point> ]? ] | [ by <coordinate-pair> with <relative-control-point> [ / <relative-control-point> ]? ] ]

<smooth-command> =
smooth [ [ to <position> [ with <control-point> ]? ] | [ by <coordinate-pair> [ with <relative-control-point> ]? ] ]

<arc-command> =
arc <command-end-point> [ [ of <length-percentage>{1,2} ] && <arc-sweep>? && <arc-size>? && [ rotate <angle> ]? ]

<command-end-point> =
to <position> |
by <coordinate-pair>

<control-point> =
<position> |
<relative-control-point>

<coordinate-pair> =
<length-percentage>{2}

<relative-control-point> =
<coordinate-pair> [ from [ start | end | origin ] ]?

<arc-sweep> =
cw |
ccw

<arc-size> =
large |
small

Examples

Basic border-shape stroke usage

This example shows how to use border-shape in stroke mode.

HTML

The markup for this example contains a single <p> element.

html
<p>Circle</p>

CSS

We give the box a width of fit-content and an aspect-ratio of 1/1 to neatly fit the content inside a square. We also set a thick black border and a box-shadow before setting a border-shape of circle(50%) to create a circular border that neatly fits the content and background.

css
p {
  width: fit-content;
  aspect-ratio: 1/1;
  border: 15px solid black;
  box-shadow: 5px 5px 10px rgb(0 0 0 / 0.5);
  border-shape: circle(50%);
}

Result

Note how the border and box-shadow neatly follow the defined shape.

Basic border-shape fill usage

This example builds on the previous one, showing how to use border-shape in fill mode to create an irregular filled border.

The HTML is the same as in the previous example.

CSS

The CSS is the same as in the previous example, except this time we color the border hotpink, and we include two <basic-shape> definitions inside the border-shape value. There's an outer rectangle that covers the whole area of the content and an inner circle that's the same as in the previous example.

css
p {
  width: fit-content;
  aspect-ratio: 1/1;
  border: 15px solid hotpink;
  box-shadow: 5px 5px 10px rgb(0 0 0 / 0.5);
  border-shape: rect(0% 100% 100% 0% round 20px) circle(50%);
}

Result

Note how this time, the border covers the area between the rectangle and the circle, and it adopts the color set in the border declaration.

Comparing border-shape values

In this example, we let you select from different border-shape values to apply to an element, so you can compare and contrast how they render.

HTML

The HTML is similar to that in the previous example, except this time we include a bit more text in the <p> element. We've also included a <select> element so you can choose different classes to apply to the <p> via JavaScript (we've hidden both the <select> and the JavaScript for brevity).

html
<p>Blessed are the cheesemakers.</p>

The classes set different border-shape values on the <p> element. To begin with, a class of ellipse is set on the <p> element, so initially it has an ellipse() border-shape.

CSS

In the CSS, we give the box a width of 550px, a thick black border, and a box-shadow.

css
p {
  width: 550px;
  border: 15px solid black;
  box-shadow: 5px 5px 10px rgb(0 0 0 / 0.5);
}

Next, we define the rules for each of the classes that will be applied when you select the different options in the <select> element:

css
.circle {
  border-shape: circle(60%);
}

.ellipse {
  border-shape: ellipse(50% 40%);
}

.inset {
  border-shape: inset(10px 20px 10px 20px round 20px);
}

.path {
  border-shape: path(
      "M 35,95 C 35,50 60,15 100,20 C 120,5 160,5 180,22 C 200,5 250,5 270,22 C 295,5 340,5 360,22 C 460,10 477,35 496,75 C 515,157 450,120 430,128 C 400,145 360,145 330,130 C 300,145 260,145 230,130 C 200,145 160,145 130,130 C 80,142 35,120 35,95 Z"
    )
    view-box;
}

.polygon {
  border-shape: polygon(0% 60%, 0% 85%, 8% 100%, 18% 88%, 30% 100%, 42% 88%, 55% 100%, 68% 88%, 80% 100%, 86% 88%, 90% 75%, 100% 60%, 90% 30%, 85% 5%, 75% 18%, 65% 3%, 52% 16%, 40% 3%, 27% 16%, 15% 3%, 5% 18%) view-box;
}

.rect {
  border-shape: rect(10px 500px 130px 20px round 20px);
}

.shape {
  border-shape: shape(
    from 0% 64.5%,
    curve to 15.71% 8.26% with 0% 30.76%/6.04% 4.51%,
    curve to 35.05% 9.76% with 20.55% -2.99%/30.21% -2.99%,
    curve to 56.8% 9.76% with 39.88% -2.99%/51.97% -2.99%,
    curve to 78.56% 9.76% with 62.84% -2.99%/73.72% -2.99%,
    curve to 97.89% 49.5% with 87.02% 0.76%/97.89% 19.51%,
    curve to 95.47% 89.25% with 101.52% 60.75%/100.31% 83.25%,
    curve to 71.3% 90.75% with 88.22% 102%/78.56% 102%,
    curve to 47.13% 90.75% with 64.05% 102%/54.38% 102%,
    curve to 22.96% 90.75% with 39.88% 102%/30.21% 102%,
    curve to 0% 64.5% with 10.88% 99.75%/0% 83.25%,
    close
  );
}

.two-polygons {
  border-shape: polygon(0% 60%, 0% 85%, 8% 100%, 18% 88%, 30% 100%, 42% 88%, 55% 100%, 68% 88%, 80% 105%, 86% 88%, 91% 75%, 101% 60%, 93% 30%, 86% 5%, 75% 18%, 65% 3%, 52% 16%, 40% 3%, 27% 16%, 15% 3%, 5% 18%)
    polygon(0% 55%, 0% 90%, 6% 104%, 17% 93%, 30% 100%, 43% 93%, 56% 102%, 69% 93%, 81% 102%, 88% 93%, 94% 78%, 100% 58%, 94% 24%, 88% -2%, 76% 13%, 64% -4%, 51% 11%, 39% -4%, 26% 11%, 13% -4%, 3% 13%);
}

.xywh {
  border-shape: xywh(5% 5% 90% 90% round 20px);
}

Result

Select different options from the drop-down menu to see how the different border shapes render. At any point, feel free to use your browser's developer tools to inspect the border-shape values applied to the <p> element and edit them to get an idea of how the values work.

Animating a border-shape

This example demonstrates how you can animate the border-shape property.

HTML

The same <p> element is used as in the previous example, except this time we've included a tabindex attribute so it can be focused via the keyboard.

html
<p tabindex="0">Blessed are the cheesemakers.</p>

CSS

For this example, we apply a polygon() border-shape to the <p>.

css
p {
  width: 550px;
  border: 15px solid black;
  box-shadow: 5px 5px 10px rgb(0 0 0 / 0.5);
  border-shape: polygon(0% 60%, 0% 85%, 8% 100%, 18% 88%, 30% 100%, 42% 88%, 55% 100%, 68% 88%, 80% 100%, 86% 88%, 90% 75%, 100% 60%, 90% 30%, 85% 5%, 75% 18%, 65% 3%, 52% 16%, 40% 3%, 27% 16%, 15% 3%, 5% 18%) view-box;
}

We also set an animation on the <p> element's :hover and :focus states, so that when the element is hovered or focused, it smoothly animates back and forth between two polygon border shapes for infinite iterations.

css
p:hover,
p:focus {
  animation: morph 1s ease-in-out infinite alternate;
}

@keyframes morph {
  from {
    border-shape: polygon(0% 60%, 0% 85%, 8% 100%, 18% 88%, 30% 100%, 42% 88%, 55% 100%, 68% 88%, 80% 100%, 86% 88%, 90% 75%, 100% 60%, 90% 30%, 85% 5%, 75% 18%, 65% 3%, 52% 16%, 40% 3%, 27% 16%, 15% 3%, 5% 18%) view-box;
  }
  to {
    border-shape: polygon(0% 55%, 0% 90%, 6% 104%, 17% 93%, 30% 100%, 43% 93%, 56% 102%, 69% 93%, 81% 102%, 88% 93%, 94% 78%, 100% 58%, 94% 24%, 88% -2%, 76% 13%, 64% -4%, 51% 11%, 39% -4%, 26% 11%, 13% -4%, 3% 13%) view-box;
  }
}

Result

Hover over or focus the paragraph to see the animation.

Specifications

Specification
CSS Borders and Box Decorations Module Level 4
# border-shape

Browser compatibility

See also