Skewed edges add speed and tension to an interface: slanted headers, angled buttons, diagonal hero stripes. You can build all of that without images by mastering transform: skew(). In this guide you will build a skewed card header, a parallelogram button with upright text, and a small label with a triangle pointer. Along the way you will learn when to counter-skew, how transform-origin affects the result, and how to avoid fuzzy edges.
Why transform: skew() Matters
transform: skew() gives you angled surfaces from plain rectangles, which makes it ideal for ribbons, tags, badges, and diagonal separators. Compared to creating shapes with borders or clipping, skew keeps the original box model intact, so layout stays predictable. You can skew only a decorative layer while leaving the content untouched and readable, or you can skew the component then “unslant” the text inside it for a controlled design. If you only need a pure shape, a true parallelogram is covered in the library, and you will often pair skew with the technique in how to make a parallelogram with CSS.
Prerequisites
You should be comfortable with the basics and ready to read CSS with transforms and pseudo-elements. A design tool is not required; we will author directly in code.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
Here is the complete markup. The card has a header, a body, a skewed button, and a decorative “New” tag. The angled header will be drawn by a pseudo-element so the text remains upright. The button will be skewed, and its inner span will counter the skew so the label reads normally. The tag includes a small triangle pointer on the right edge.
<!-- HTML -->
<div class="wrap">
<article class="card" aria-label="Skew demo card">
<header class="card__header">
<span class="card__eyebrow">transform: skew()</span>
<h3 class="card__title">Angles without images</h3>
</header>
<div class="card__body">
<p>This card uses skew() to create a diagonal header, a parallelogram button, and a compact label with a pointer. The content remains readable because the slant lives on decorative layers.</p>
<a class="btn btn--slant" href="#"><span>Read more</span></a>
<span class="tag" aria-hidden="true"><span>New</span></span>
</div>
</article>
</div>
Step 2: The Basic CSS & Styling
This foundation sets colors, typography, and a centered layout. The card has rounded corners and a subtle border to show how skewed layers sit above a regular rectangle. Custom properties hold the palette and the skew angle, which makes tuning design constants quick.
/* CSS */
:root {
--bg: #0f1226;
--surface: #171a33;
--ink: #e7ecff;
--muted: #aab2d3;
--accent: #76d39c;
--accent-2: #6fb1ff;
--warn: #ffce6b;
--skew: 10deg; /* master angle for this demo */
}
*,
*::before,
*::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
color: var(--ink);
background: radial-gradient(1200px 800px at 20% 10%, #1b2044, var(--bg));
}
.wrap {
min-height: 100vh;
display: grid;
place-items: center;
padding: 4rem 1rem;
}
.card {
width: min(720px, 92vw);
background: var(--surface);
border: 1px solid rgba(255,255,255,.08);
border-radius: 14px;
overflow: hidden;
box-shadow:
0 10px 30px rgba(0,0,0,.35),
inset 0 1px 0 rgba(255,255,255,.04);
}
.card__header {
position: relative;
padding: 2rem 1.5rem 3.25rem;
isolation: isolate; /* keep pseudo-element effects inside */
}
.card__eyebrow {
display: inline-block;
font-size: .875rem;
letter-spacing: .06em;
color: var(--muted);
text-transform: uppercase;
}
.card__title {
margin: .25rem 0 0;
font-size: clamp(1.4rem, 2.4vw + 1rem, 2.1rem);
}
.card__body {
padding: 1.25rem 1.5rem 1.75rem;
color: var(--ink);
}
.card__body p { color: var(--muted); max-width: 68ch; }
Advanced Tip: Store the skew angle in a custom property (for example, –skew). You can reuse it on multiple elements and flip it by multiplying with -1 using calc() for mirrored edges. That keeps angles consistent across the header, tags, and buttons.
Step 3: Building the Skewed Header Band
The header gets a decorative band drawn by ::before. The band is a plain rectangle that we skew along the Y axis to create a diagonal bottom edge. The header text remains upright because the skew lives on the pseudo-element rather than the header itself.
/* CSS */
.card__header::before {
content: "";
position: absolute;
inset: -40px -8% auto -8%; /* bleed beyond edges to hide gaps */
height: 160px;
background:
linear-gradient(110deg, var(--accent), var(--accent-2));
transform-origin: top left;
transform: skewY(calc(var(--skew) * -1));
z-index: -1; /* sit behind the header text */
border-bottom: 1px solid rgba(0,0,0,.25);
}
How This Works (Code Breakdown)
The band is oversized with a negative top offset and wider horizontal insets. Bleeding the rectangle beyond the card edges prevents slivers from appearing along the diagonal where subpixel rounding can reveal the card background. The transform-origin is set to top left so the skew pivots from the top edge, which keeps the diagonal predictable relative to the header spacing.
skewY() slants vertical lines. A negative angle leans the bottom edge to the right, a positive angle leans it left. You can also write transform: skew(ax, ay), where ax maps to skewX() and ay maps to skewY(). For example, transform: skew(0deg, -10deg) matches the code above. CSS accepts deg, rad, grad, and turn units; stick to degrees for a mental model that pairs well with design specs.
The text stays readable because the pseudo-element is the only skewed layer. If you instead skew the header itself, text, icons, and hit targets will all tilt. That look can be valid for logos and badges, but reserve it for short labels. If you need a true angled shape without transforms, the step-by-step recipe in how to make a parallelogram with CSS is a strong companion to this approach.
Step 4: Building the Slanted Button and Tag
The button demonstrates a classic skew pattern: slant the box, then counter-skew the label so the text is upright. The tag is a compact parallelogram with a tiny triangle pointer drawn with borders. The pointer technique mirrors the method in how to make a triangle right with CSS, attached to the skewed label.
/* CSS */
.btn {
--btn-skw: 18deg;
--btn-bg: #0e1729;
--btn-fg: var(--ink);
position: relative;
display: inline-block;
vertical-align: middle;
padding: .9rem 1.25rem; /* base before slant compensation */
color: var(--btn-fg);
text-decoration: none;
font-weight: 700;
border: 1px solid rgba(255,255,255,.14);
background: linear-gradient(180deg, rgba(255,255,255,.06), transparent) var(--btn-bg);
border-radius: 8px;
box-shadow: 0 8px 20px rgba(0,0,0,.25);
}
.btn--slant {
transform: skewX(calc(var(--btn-skw) * -1));
padding-left: 1.6rem; /* add room on the left/right since the slant bites into padding */
padding-right: 1.6rem;
}
.btn--slant > span {
display: inline-block;
transform: skewX(var(--btn-skw)); /* un-skew the text */
}
.btn--slant:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
/* Tag with a pointer */
.tag {
--tag-skw: 14deg;
display: inline-block;
margin-left: .75rem;
background: var(--warn);
color: #3a2b00;
font-weight: 800;
letter-spacing: .02em;
padding: .25rem .5rem;
transform: skewX(calc(var(--tag-skw) * -1));
border-radius: 4px;
position: relative;
top: -2px; /* align baseline with the button */
}
.tag > span {
display: inline-block;
transform: skewX(var(--tag-skw)); /* keep text upright */
}
/* right-pointing triangle pointer */
.tag::after {
content: "";
position: absolute;
right: -10px;
top: 50%;
transform: translateY(-50%);
border-left: 10px solid var(--warn);
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
/* the tag is skewed, but the pointer uses borders so it already looks correct */
}
How This Works (Code Breakdown)
Buttons often want readable labels. The common trick is to skew the button box with skewX(-18deg), then wrap the label in a span that applies the inverse angle with skewX(18deg). The net effect is a parallelogram box with normal text. Because skew changes the box’s x-projection, increase horizontal padding to keep the content breathing room even after the tilt.
The tag uses the same counter-skew pattern. Its pointer is a border-only triangle placed on the right edge. The three borders are transparent except the one that faces the pointer direction. That is the same construction pattern described in the library page for how to make a triangle right with CSS. Combining skew with border triangles is a compact way to build tooltips, banners, and price tags without any images. If you plan to create a full ribbon with tails, the guide for how to make a ribbon banner with CSS pairs nicely with a skewed headline.
One more note about readability: skewing text directly slants glyphs like an artificial italic, which can look harsh at larger angles. Keeping text upright with a counter-skew delivers the visual energy of the shape while preserving legibility.
Advanced Techniques: Adding Animations & Hover Effects
Transforms animate well, so you can add a subtle slide to the button without reflow. The key is to animate translate or scale while leaving the skew fixed. Respect user motion preferences with a media query, and avoid animating box-shadow on large elements.
/* CSS */
.btn--slant {
transition: transform .3s cubic-bezier(.2,.8,.2,1), background .25s linear;
}
.btn--slant:hover {
transform: skewX(calc(var(--btn-skw) * -1)) translateX(2px);
background: linear-gradient(180deg, rgba(255,255,255,.1), transparent) #12203b;
}
.btn--slant:active {
transform: skewX(calc(var(--btn-skw) * -1)) translateX(1px) translateY(1px);
}
/* animated sheen across the header band */
@keyframes sheen {
0% { background-position: -60% 0; }
100% { background-position: 160% 0; }
}
.card__header::before {
background:
linear-gradient(110deg, var(--accent), var(--accent-2)) 0 0 / cover no-repeat,
linear-gradient(90deg, transparent 0 45%, rgba(255,255,255,.35) 50%, transparent 55%) 0 0 / 200% 100% no-repeat;
animation: sheen 3s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.btn--slant { transition: none; }
.btn--slant:hover,
.btn--slant:active { transform: skewX(calc(var(--btn-skw) * -1)); }
.card__header::before { animation: none; }
}
The button keeps its -18deg skew while translateX adds a small nudge for feedback. The header’s ::before layer receives a second gradient that sweeps across with a keyframe. Because the sheen is only a background-position change, it stays smooth. The prefers-reduced-motion query disables both to respect user settings.
Accessibility & Performance
Skew gives you style without breaking layout, but it still needs attention to semantics, motion, and target sizes.
Accessibility
The label “New” is visual only in this demo, so the tag carries aria-hidden=”true”. If the tag conveys status, expose that text instead and remove aria-hidden. The button remains an anchor with a clear text label. Keep focus styles visible; slanted edges can make outlines feel offset, so use outline-offset to lift the ring off the slanted box. If you add motion, pair it with prefers-reduced-motion to respect user needs as shown earlier.
Performance
Transforms run on the compositor on most browsers, which keeps interactions smooth without layout thrash. Prefer transforming paint-only layers such as pseudo-elements or a button wrapper. Keep angles moderate to reduce antialiasing artifacts along diagonals. If you see hairline gaps where skewed layers meet, oversize the decorative layer beyond the container bounds, as done with the header insets. Avoid animating box-shadow on large cards, and avoid blurring filters on skewed layers unless you truly need them.
Angles You Can Reuse Everywhere
You built a diagonal header band with skewY(), a parallelogram button with an upright label using counter-skew, and a compact tag with a triangle pointer. You also learned how transform-origin controls the pivot and how to animate without reflow. With these patterns, you can angle headers, banners, and callouts anywhere in your UI and keep the text crisp. Now you have the tools to shape your own slanted components with intent.