You need a quick, dependable way to confirm success in your UI without adding another icon library. This project builds a compact success message with a circular badge and an animated checkmark using only CSS. No SVG, no images, and no JavaScript. By the end, you will have a reusable pattern that drops into forms, toasts, and modals with minimal code and clean theming.
Why a CSS-only Success Message Matters
Relying on icon fonts or external SVG sprites adds extra files and often forces alignment fixes. A CSS-only checkmark keeps your dependency list short and your styles consistent. You can animate, theme, and scale it with the same CSS variables you already use elsewhere. It also keeps your DOM predictable and easy to test, which helps when you want a status message that renders fast and feels responsive.
Prerequisites
You do not need complex tooling here. A single stylesheet and a basic HTML file will do. You only need working knowledge of HTML, CSS variables, and pseudo-elements.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The markup stays minimal so the CSS can do the heavy lifting. The icon element is empty and marked decorative; screen readers will focus on the message text via role and live region attributes.
<!-- HTML -->
<div class="success" role="status" aria-live="polite">
<span class="success__icon" aria-hidden="true"></span>
<p class="success__text">Profile saved successfully</p>
</div>
Step 2: The Basic CSS & Styling
Start with variables for color, sizing, and timing. The container uses a subtle card style, and the icon gets a fixed square footprint. The checkmark will be drawn with two pseudo-elements in the next steps.
/* CSS */
:root {
--success: #16a34a; /* brand success green */
--success-contrast: #ffffff; /* check color */
--surface: #0b0e14; /* page background */
--panel: #0f172a; /* card background */
--text: #e5e7eb; /* text color */
--muted: #94a3b8; /* secondary text color */
--icon-size: 48px; /* diameter of the badge */
--check-thickness: 4px; /* stroke thickness */
--check-short: 14px; /* left stroke length */
--check-long: 24px; /* right stroke length */
--radius: 12px;
--gap: 12px;
--enter-duration: 320ms;
--draw-short-delay: 120ms;
--draw-long-delay: 280ms;
}
* { box-sizing: border-box; }
html, body {
height: 100%;
}
body {
margin: 0;
display: grid;
place-items: center;
background: radial-gradient(1200px 600px at 50% 0%, #0a0f1d 0%, var(--surface) 55%);
color: var(--text);
font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji";
}
.success {
display: inline-flex;
align-items: center;
gap: var(--gap);
padding: 12px 16px;
background: var(--panel);
color: var(--text);
border: 1px solid rgba(148, 163, 184, 0.15);
border-radius: var(--radius);
box-shadow:
0 10px 20px rgba(0,0,0,0.25),
inset 0 1px 0 rgba(255,255,255,0.02);
animation: pop-in var(--enter-duration) cubic-bezier(.2,.7,.2,1) both;
}
.success__text {
margin: 0;
font-weight: 500;
letter-spacing: 0.2px;
}
/* subtle entrance */
@keyframes pop-in {
from { transform: translateY(6px) scale(.98); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}
Advanced Tip: Expose sizing and timing through CSS variables. Designers can tweak –icon-size or the draw delays without touching selectors. You can also theme the icon for brand palettes by overriding –success and –success-contrast at the component level.
Step 3: Building the Success Badge
The badge is a simple circle with a soft sheen and a faint ring. The border radius handles the circular shape, and a layered background adds depth without extra elements.
/* CSS */
.success__icon {
position: relative;
width: var(--icon-size);
height: var(--icon-size);
flex: 0 0 var(--icon-size);
border-radius: 50%;
background:
radial-gradient(120% 120% at 70% 30%, rgba(255,255,255,.28), rgba(255,255,255,0) 60%),
linear-gradient(180deg, #17b156, var(--success));
box-shadow:
0 0 0 3px rgba(22, 163, 74, 0.25), /* ring */
0 6px 18px rgba(22, 163, 74, 0.35); /* glow */
overflow: hidden; /* tidy pseudo-elements */
isolation: isolate; /* keep blending contained */
}
How This Works (Code Breakdown)
The icon is a fixed square box with width and height driven by –icon-size. Setting border-radius: 50% converts that square into a circle. If you want a refresher on the math-free route to circular shapes, see how to make a circle with CSS. The layered background supplies a slight highlight at the top-right to suggest depth. The radial gradient delivers the sheen, while the linear gradient locks in the base color. The box-shadow creates a soft outer ring and a glow that lifts the badge off the surface. The ring color uses the same green with alpha to keep it on brand without a hard edge. The container uses overflow: hidden to keep the animated strokes clipped inside the disk, keeping the visual tight.
Step 4: Building the Animated Checkmark
The checkmark is two strokes that grow from zero width to their target length. Each stroke is a pseudo-element with a shared thickness and a unique angle. Staggering the animations sells the drawing effect.
/* CSS */
.success__icon::before,
.success__icon::after {
content: "";
position: absolute;
height: var(--check-thickness);
background: var(--success-contrast);
border-radius: 2px;
left: 50%;
top: 50%;
transform-origin: left center;
width: 0; /* animated width */
will-change: width, transform;
}
/* short rising stroke */
.success__icon::before {
/* pivot a bit left-bottom inside the circle */
left: 26%;
top: 58%;
transform: rotate(45deg);
animation: draw-short 240ms var(--draw-short-delay) cubic-bezier(.22,.8,.3,1) forwards;
}
/* long descending stroke */
.success__icon::after {
left: 44%;
top: 48%;
transform: rotate(-45deg);
animation: draw-long 340ms var(--draw-long-delay) cubic-bezier(.22,.8,.3,1) forwards;
}
@keyframes draw-short {
from { width: 0; }
to { width: var(--check-short); }
}
@keyframes draw-long {
from { width: 0; }
to { width: var(--check-long); }
}
How This Works (Code Breakdown)
Both strokes share position: absolute and transform-origin: left center. This sets a clean pivot at the left edge of each bar, so width can animate outward along the stroke direction. The short stroke rotates 45 degrees, giving the upward left leg of the check. The long stroke rotates -45 degrees for the right leg. The left and top values place the pivots so the bars meet neatly within the circle. You can nudge these percentages to taste if you change –icon-size or –check-thickness.
Animating width is the simplest way to draw the check without masks. Each animation ends with forwards so the strokes retain their final length. The easing cubic-bezier(.22,.8,.3,1) gives a crisp settle without jitter. If you want a card-style container for a toast, that is just a rectangle with rounded corners. For a refresher on that shape, see how to make a rectangle with CSS. If you plan to add a little pointer to a toast, you can attach a tiny triangle next to the component; here is how to make a triangle right with CSS to form the pointer.
Advanced Techniques: Adding Animations & Hover Effects
A subtle entrance for the whole component and a soft ring pulse signal success without turning it into a light show. Add a hover to acknowledge interactivity when the message is clickable or dismissible.
/* CSS */
.success {
/* already has pop-in; keep it short */
}
/* pulse the ring once after the check finishes */
.success__icon {
animation:
ring-pulse 520ms calc(var(--draw-long-delay) + 360ms) ease-out 1 both;
}
@keyframes ring-pulse {
0% { box-shadow: 0 0 0 3px rgba(22,163,74,.25), 0 6px 18px rgba(22,163,74,.35); }
40% { box-shadow: 0 0 0 6px rgba(22,163,74,.20), 0 6px 22px rgba(22,163,74,.45); }
100% { box-shadow: 0 0 0 3px rgba(22,163,74,.25), 0 6px 18px rgba(22,163,74,.35); }
}
/* optional hover for clickable success blocks */
.success:hover .success__icon {
filter: saturate(1.15) brightness(1.05);
}
/* theming variants via overrides */
.success--compact {
--icon-size: 36px;
--check-thickness: 3px;
--check-short: 10px;
--check-long: 18px;
padding: 8px 12px;
--gap: 10px;
}
.success--high-contrast {
--panel: #ffffff;
--text: #0b1220;
--success-contrast: #0b1220; /* dark check on light icon */
}
/* respect motion preferences */
@media (prefers-reduced-motion: reduce) {
.success,
.success__icon,
.success__icon::before,
.success__icon::after {
animation: none !important;
transition: none !important;
}
.success__icon::before { width: var(--check-short); }
.success__icon::after { width: var(--check-long); }
}
Accessibility & Performance
Do not treat a success state as decoration. The icon may be decorative, but the message text communicates an outcome and should be announced. The code uses role=”status” and aria-live=”polite” on the container. This lets screen readers read the message when it appears without interrupting the user. The icon is marked aria-hidden=”true” so assistive tech does not describe it redundantly. Keep color contrast strong between text and the card background. The check color is white against a bold green to maintain visibility inside the badge.
Accessibility
When this pattern appears after a form submit, screen readers will announce “Profile saved successfully.” That should be concise and meaningful. If your message needs more context, include it in the same element or add a visually hidden span with extra detail. If you use a dismissible variant, the close control needs an accessible name like “Dismiss success message.” Respect motion preferences with the prefers-reduced-motion media query. The snippet above switches off animation and sets the final widths, which keeps the check visible without motion.
Performance
The animation manipulates width on two absolutely positioned elements and applies transforms. This affects only paint within the icon and avoids layout thrash. The entrance animation for the container uses transform and opacity, which are inexpensive to animate and handled well by the compositor. The ring pulse touches box-shadow for one short burst; use it sparingly if you plan to instantiate dozens of messages at once. Keep the CSS footprint small and reuse variables so the browser can batch style recalculations efficiently.
Build Once, Reuse Everywhere
You now have a success pattern that draws a checkmark inside a circular badge, with no external assets and clean theming. Drop it into forms, toasts, and inline confirmations. Tweak a couple of variables and you have a compact or high-contrast version ready to go. With the same approach, you can compose other UI symbols from primitives and build your own icon set directly in CSS.