border-shape CSS property
Limited availability
This feature is not Baseline because it does not work in some of the most widely-used browsers.
Experimental: This is an experimental technology
Check the Browser compatibility table carefully before using this in production.
The border-shape CSS property enables using <basic-shape> values to define a container's border shape.
Syntax
/* 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 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
)
/* 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: 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;
border-shape: circle(50%) border-box ellipse(40% 30%) view-box;
border-shape: rect(5px 198px 189px 0px round 20px) view-box circle(50%);
/* 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-
The initial value. Specifies that no border shape is defined.
<basic-shape>-
A function defining the shape of the border.
<geometry-box>Optional-
A keyword defining the reference box for the border shape to be drawn relative to. If not included, the shape's reference geometry box defaults to:
half-border-boxif 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-boxfor the first (outer) shape andpadding-boxfor 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 enables you to set the border shape of any element (including inline and pseudo-elements) to any shape creatable by functions defined in the <basic-shape> value. This includes:
inset(),rect(), andxywh(): 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 only use pixel values and it has to be defined as a single string, so custom properties can't be included viavar(). It is advised to useshape()instead.polygon(): Defines any kind of polygon shape via pairs of vertex coordinates. If your desired shape includes smooth curves, you are advised to useshape().shape(): Defines any kind of shape. The syntax ofshape()is more CSS-compatible than that ofpath(), and solves its shortcomings.
The border-shape property has two different modes:
- If a single basic shape is provided in the value, that shape defines the border shape of the element, with defined border styles drawn as a stroke around the shape. This is known as stroke mode.
- If two basic shapes are provided in the value, the first shape defines the outer boundary of the border, the second shape defines in inner boundary of the border, and any defined border color fills the area between the two. This is known as fill mode.
Note: It doesn't make sense to define a bigger shape for the inner boundary 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.
Optionally, you can include a <geometry-box> keyword after each <basic-shape> value, to specify the reference box the shapes should be drawn relative to.
When a border-shape is applied to an element, properties such as border, box-shadow, and outline defined on the element will follow the shape of the border (the outer shape, in fill mode).
The border-shape 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 content and background of the element is clipped by the border-shape (inner shape, in fill mode). When the shape you are specifying is the same size or smaller than the content/background, you don't tend to need to adjust the reference box, unless you want to create some kind of offset effect. However, when the shape you are specifying 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 employ a different reference box to fix the display (see Handling backgrounds inside larger border-shapes for more information).
Limitations on border styles applied to border shapes
There are several limitations on the styles that will be applied to borders with a border-shape property set:
-
border-image: Not applied. -
border-style: Not applied. All borders are rendered with asolidstyle. -
border-color: Border color is applied, however when different colors are applied to different edges of the element, the browser chooses the first edge it finds that has a border color applied, in the following order:- Block start edge
- Inline start edge
- Block end edge
- Inline end edge
It then applies that edge's border color to the entire rendered border.
-
border-width: In stroke mode, border width is applied directly to the rendered border. When different widths are applied to different edges of the element, the browser selects a width to apply to the entire rendered border using the same process as described forborder-color.In fill mode, the border area is defined by the difference in area between the outer and inner shapes, therefore
border-widthdoesn't have any direct effect on the rendered border width. 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>keywords tocontent-boxorpadding-box), therefore you still need to be mindful of theborder-widthset on the underlying element while using fill mode.
As an example, if an element has the following declarations applied:
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, as other styles are ignored. The border width and color will be 40px and hotpink, because the border-left property applies styles to the inline start edge (assuming that the page has a horizontal writing-mode) and that is the first edge with border styles applied in the browser's 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.
If you want to use shaped corners in a border-shape, you will have to draw them directly as part of the shape.
Handling backgrounds inside larger border-shapes
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:
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 Jigsaw 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.
<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.
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 in the previous example, except that 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 is the same as in the previous example.
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.
Comparison between different shape values
In this example, we allow you to set different border-shape values on an element, to allow you to compare and contrast them.
HTML
The HTML is similar the previous example, except that this time we include a bit more text in the <p> element, and we also include a <select> element allowing you to choose different classes to apply to the <p> via JavaScript (we've hidden both the <select> and the JavaScript for brevity).
<p>Fashion is something so ugly it has to be changed every 15 minutes.</p>
The classes set different border-shape values on the <p> element. A class of ellipse is set on it to begin with, so that initially, it has an ellipse() border-shape applied to it.
CSS
In the CSS, we give the box a width of 550px, a thick black border, and a box-shadow.
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:
.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
Use your developer tools to inspect the border-shape values applied to the <p> element at any point, and edit them to get an idea of how the values work.
Animating a border-shape
This example demonstrates how it is possible to animate the border-shape property.
HTML
The same HTML <p> is used as in the previous example, except that this time, we've included a tabindex attribute so it can be focused via the keyboard.
<p tabindex="0">
Fashion is something so ugly it has to be changed every 15 minutes.
</p>
CSS
This time, we apply a polygon() border-shape to the <p>.
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 it is hovered or focused, it smoothy animates between two polygon border shapes for infinite iterations.
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 or focus the paragraph to see the animation.
A jigsaw piece navigation menu
In this example, we show how to use border-shape to create an irregular navigation menu example with each nav item shaped like a jigsaw piece.
HTML
Our HTML is a fairly typical navigation menu — a list of links.
<ul>
<li><a href="#">One</a></li>
<li><a href="#">Two</a></li>
<li><a href="#">Three</a></li>
<li><a href="#">Four</a></li>
</ul>
CSS
There is a fair bit of CSS in this example; we have hidden some of the basic page setup code completely (as with previous examples), then we have split the remaining CSS into two sections. If you want to ignore most of the styling and just read about the code that is most relevant to the border-shape styles, jump to Handling the border shape.
Implementing the general nav styles
First of all, we style our <ul>, removing the default list-style-type and padding, and setting a display value of flex to lay out the contained <li> elements in a row. We then set a gap value of 0 and apply a transition so that, when the <ul> state changes, a change in the gap value will smoothly animate.
ul {
list-style-type: none;
padding: 0;
display: flex;
gap: 0px;
transition: gap 0.6s;
}
Next, we style the <li> elements. We want each nav item to be square, so we set an equal width and height.
li {
width: 160px;
height: 160px;
}
Now we'll go on to style the <a> elements inside the list items. We start off by removing the default text-decoration and setting the color to black. We then set a width and height of 100% to make the <a> elements fill the full area of the <li> elements, then use flexbox to center their text horizontally and vertically.
We then set box-shadow and text-shadow properties on the links, plus a transition so that any property value changes on state changes are animated smoothly.
a {
text-decoration: none;
color: black;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
box-shadow:
2px 0px 2px rgb(0 0 0 / 0.5),
inset 3px 3px 3px rgb(255 255 255 / 0.5);
text-shadow: 1px 1px 1px rgb(0 0 0 / 0.5);
transition: all 0.6s;
}
We then give each jigsaw piece a different color:
li:nth-child(1) a {
background-color: #2de1fc;
}
li:nth-child(2) a {
background-color: #2afc98;
}
li:nth-child(3) a {
background-color: #09e85e;
}
li:nth-child(4) a {
background-color: #16c172;
}
Handling the border shape
We set a downwards-pointing jigsaw piece border-shape on each odd-numbered <a> element, and an upwards-pointing jigsaw piece border-shape on each even-numbered <a> element, for some variety:
li:nth-child(even) a {
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;
}
li:nth-child(odd) a {
border-shape: shape(
from 0% 0%,
hline to 33%,
arc by 33% 0% of 16% 20% small ccw,
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 cw,
hline to 0%,
line to 0% 66%,
arc by 0% -33% of 20% 16% small ccw,
close
)
content-box;
}
This immediately creates an issue — the notches on the jigsaw pieces that extend out from the original <a> area aren't filled by their background colors.
There is a solution to this problem. We've deliberately included the content-box <geometry-box> value after each shape() function in the previous two rules. This means the shapes will be drawn relative to the elements' content boxes, and any applied padding won't be set inside the shape. Instead, the padding will be placed outside the shape, causing it to get smaller and forcing the background color to fill up the notches.
The required padding is set like so:
a {
padding: 24px;
}
Note:
You can see what the background problem looks like by inspecting the live example later on in your DevTools and disabling the padding applied to the <a> elements.
The set padding causes the jigsaw piece shapes to get smaller, so there are gaps between them. We want them to touch initially, so we set a large negative margin-right value on each list item to bring them together again:
li {
margin-right: -47px;
}
A side-effect of the margin-right is that all of the <li> items are moved to the right, so the nav menu is no longer centered. To fix this, we use relative positioning to move the <ul> back to the left:
ul {
position: relative;
right: 23.5px;
}
Finally, we apply some style updates on :hover and :focus that, when combined with the transition properties set earlier, produce some animated effects when the nav items are interacted with. We increase the gap set on the <ul> flexbox layout when it is hovered or focused. To handle the focus state, we use the :has pseudo-class to select the entire <ul> when any <a> inside it is focused.
ul:hover,
ul:has(a:focus) {
gap: 30px;
}
We then set an increased brightness filter, scale factor, and outer box-shadow on the <a> elements themselves when they are hovered or focused, making them appear brighter and raised up when interacted with.
a:hover,
a:focus {
filter: brightness(1.2);
scale: 1.1;
box-shadow:
5px 0px 10px rgb(0 0 0 / 0.5),
inset 3px 3px 3px rgb(255 255 255 / 0.5);
}
Result
Hover or focus the nav items to see the animated effects. Note how naturally the different applied effects work with the applied border-shape values.
Specifications
| Specification |
|---|
| CSS Borders and Box Decorations Module Level 4> # border-shape> |
Browser compatibility
See also
border- CSS borders and box decorations module
- CSS backgrounds and borders module
- border-shape: the future of the non-rectangular web by Una Kravets (2026)