You are going to draw a Pokeball with nothing but CSS. By the end, you will have a responsive, crisp, themeable Pokeball built from gradients, borders, and pseudo-elements. You will understand how to slice a circle into halves, add a center band, build the button with concentric rings, and apply a subtle shake animation for polish.
Why Drawing a Pokeball in CSS Matters
CSS art sharpens layout intuition and deepens your knowledge of stacking, shapes, and rendering. A Pokeball is a tidy exercise: one main circle, two color halves, a center band, and a button. You will practice composing shapes with gradients and borders rather than extra DOM nodes or images. This approach keeps icons scalable, avoids network requests, and adapts cleanly to themes. When you can build a Pokeball, you can break down many icons into reusable primitives and render them quickly with CSS.
Prerequisites
You do not need a canvas or SVG here. You only need a basic understanding of the box model and positioning.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The markup stays small on purpose. A single container represents the Pokeball. Two spans handle the band and the button. Pseudo-elements add highlights and depth later, so the DOM remains tidy and easy to drop into any layout.
<!-- HTML -->
<div class="pokeball" role="img" aria-label="Pokeball">
<span class="band" aria-hidden="true"></span>
<span class="button" aria-hidden="true"></span>
</div>
Step 2: The Basic CSS & Styling
Start with a small set of CSS variables for size and color tokens. The Pokeball uses a circle with a bold outer stroke, so we give the container a square footprint with an aspect-ratio and a full border radius. The background uses a vertical split to paint red on top and white on bottom.
/* CSS */
:root {
--size: 240px;
--red: #e53b3b;
--red-dark: #bd1e1e;
--white: #fafafa;
--black: #202224;
--band: #1c1d21;
--shadow: rgba(0, 0, 0, 0.35);
--gloss: rgba(255, 255, 255, 0.65);
}
html, body {
height: 100%;
}
body {
margin: 0;
display: grid;
place-items: center;
background: linear-gradient(180deg, #f6f7fb 0%, #e9ecf6 100%);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.pokeball {
width: var(--size);
aspect-ratio: 1 / 1;
border-radius: 50%;
position: relative;
background:
linear-gradient(to bottom, var(--red) 0 50%, var(--white) 50% 100%);
border: calc(var(--size) * 0.035) solid var(--black);
box-shadow:
0 calc(var(--size) * 0.03) 0 var(--black),
0 calc(var(--size) * 0.08) calc(var(--size) * 0.15) var(--shadow);
}
Advanced Tip: Keep colors and sizes in custom properties so you can theme variations or scale the Pokeball by changing a single –size. This also helps when you want multiple Pokeballs with different sizes on the same page.
Step 3: Building the Shell
The shell consists of the red top half, the white bottom half, and a subtle glossy highlight. The background gradient already splits the circle into two clean halves. Add depth with a soft vignette and a gloss streak that sits in the top-left quadrant. This keeps the surface from looking flat.
/* CSS */
.pokeball::before,
.pokeball::after {
content: "";
position: absolute;
pointer-events: none;
}
.pokeball::before {
/* Top-left gloss streak */
inset: 0;
border-radius: 50%;
background:
radial-gradient(120% 80% at 30% 25%, var(--gloss) 0 20%, transparent 28% 100%);
mix-blend-mode: screen;
opacity: 0.65;
}
.pokeball::after {
/* Subtle inner shading for curvature */
inset: 0;
border-radius: 50%;
background:
radial-gradient(120% 100% at 50% 80%, rgba(0,0,0,0.08) 0 50%, transparent 70%);
opacity: 0.8;
}
How This Works (Code Breakdown)
The container sets position: relative to host its pseudo-elements. That creates a positioning context for ::before and ::after, so they can stretch across the circle with inset: 0 and match the border radius. The background on the main element uses a linear-gradient split at 50% to paint red on the top and white on the bottom. If you need a refresher on circles, see how to make a circle with CSS. That article covers the width, height, and border-radius trick this component uses.
The ::before element draws a gloss streak with a radial-gradient. The gradient is anchored near the top-left (30% 25%), then fades to transparent, which simulates a curved reflection. The mix-blend-mode: screen adds a lightening effect that interacts with the red top for a more natural look. The ::after element adds a faint inner shadow from the bottom, hinting at volume without heavy box-shadow layers.
If you prefer to literally split the circle into halves, you can use separate shapes. For example, building a semicircle top and a matching bottom creates the same visual. The gradient approach keeps the DOM cleaner and renders quickly, while still mapping conceptually to two semicircles.
Step 4: Building the Band and Button
The band is a horizontal black strip that runs through the midpoint. The button is a centered circle with a thick dark ring and a white inner disk, with another small highlight for sheen. These elements complete the visual identity of the Pokeball.
/* CSS */
.band {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 92%;
height: calc(var(--size) * 0.18);
background: var(--band);
border-radius: calc(var(--size) * 0.09);
box-shadow:
inset 0 0 0 calc(var(--size) * 0.02) var(--black),
0 calc(var(--size) * 0.01) calc(var(--size) * 0.02) rgba(0,0,0,0.2);
}
.button {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: calc(var(--size) * 0.32);
height: calc(var(--size) * 0.32);
border-radius: 50%;
background:
radial-gradient(circle at 40% 35%, #ffffff 0 40%, #f2f2f2 45% 100%);
border: calc(var(--size) * 0.04) solid var(--black);
box-shadow:
0 calc(var(--size) * 0.01) 0 var(--black),
0 calc(var(--size) * 0.02) calc(var(--size) * 0.06) rgba(0,0,0,0.25);
}
.button::before {
/* Inner ring */
content: "";
position: absolute;
inset: 18%;
border-radius: 50%;
border: calc(var(--size) * 0.02) solid var(--black);
background: #fbfbfb;
box-shadow: inset 0 0 calc(var(--size) * 0.015) rgba(0,0,0,0.15);
}
.button::after {
/* Small highlight dot */
content: "";
position: absolute;
width: 26%;
height: 26%;
border-radius: 50%;
left: 32%;
top: 28%;
background: radial-gradient(circle at 30% 30%, #ffffff 0 55%, rgba(255,255,255,0) 60%);
opacity: 0.9;
}
How This Works (Code Breakdown)
The band uses an absolute rectangle centered with translate to keep it aligned even as the Pokeball scales. The rounded corners give the strip a softer edge that fits the circular shell. If you want a primer on rectangles, review how to make a rectangle with CSS. The only difference here is that we position and round it for the center band look.
The button is a circle placed on top of the band, with a thick dark border to create the outer ring. A radial gradient adds a slight dome effect. The ::before pseudo-element draws the inner ring with a smaller border, and ::after adds a tiny highlight dot. These layered elements sell the depth without extra markup or images.
Advanced Techniques: Adding Animations & Hover Effects
A small shake and a soft button pulse bring the Pokeball to life. Apply a hover shake that rotates and nudges the ball, and a gentle glow that simulates the button being active. Keep the motion brief and low-amplitude so it reads as playful, not distracting.
/* CSS */
.pokeball {
transition: transform 180ms ease;
}
.pokeball:hover {
transform: translateY(-2px);
cursor: pointer;
}
/* Shake on focus or an "active" class you can toggle in JS */
.pokeball:focus-visible,
.pokeball.is-shaking {
animation: shake 900ms ease-in-out both;
}
@keyframes shake {
0% { transform: translateX(0) rotate(0); }
15% { transform: translateX(-3%) rotate(-4deg); }
30% { transform: translateX(3%) rotate(4deg); }
45% { transform: translateX(-2%) rotate(-3deg); }
60% { transform: translateX(2%) rotate(3deg); }
75% { transform: translateX(-1%) rotate(-2deg); }
100% { transform: translateX(0) rotate(0); }
}
/* Button pulse */
.pokeball.is-capturing .button {
animation: pulse 1200ms ease-in-out infinite;
box-shadow:
0 calc(var(--size) * 0.01) 0 var(--black),
0 0 calc(var(--size) * 0.12) rgba(255, 255, 255, 0.3),
0 calc(var(--size) * 0.02) calc(var(--size) * 0.08) rgba(0,0,0,0.25);
}
@keyframes pulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.15); }
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.pokeball,
.pokeball:hover,
.pokeball.is-shaking,
.pokeball.is-capturing .button {
animation: none !important;
transition: none !important;
transform: none !important;
}
}
Accessibility & Performance
Creating CSS art still carries responsibility for usability, even if the asset looks decorative. Treat your Pokeball like any other icon by deciding whether it conveys meaning or is only visual.
Accessibility
If the Pokeball communicates state or is a control, keep role=”img” with an aria-label that describes its purpose, like “Pokeball loading” or “Capture.” If the icon is purely decorative, remove the label and set aria-hidden=”true” on the container to keep it out of the accessibility tree. The code in this guide uses role=”img” and an aria-label since it might represent a control or brand element.
Keyboard users should be able to reach interactive versions. If you wire it as a button, use a native button element or manage tabindex and ARIA states. Keep hover effects mirrored on focus with :focus-visible. Avoid high-frequency flashing. The prefers-reduced-motion media query disables shake and pulse for users who prefer less motion.
Performance
This technique is light. Gradients and borders are cheap for modern engines. The component scales from one variable, which avoids layout thrash across many nested nodes. The use of a single element with two spans keeps DOM size small. Animations are transform and filter based, which run on the compositor and render smoothly on most devices. Heavy blur filters or large shadow stacks can be costly, so keep shadows shallow and avoid piling on multiple large spreads.
If you render many Pokeballs on screen at once, reduce or remove animations and drop long shadow spreads. Use the same CSS variables across instances so the browser can share computed values. For raster exports or thumbnails, consider pre-rendering a static frame to remove runtime effects entirely.
One Shape, Many Tricks
You built a responsive, themeable Pokeball that uses a split circle, a central band, and a layered button for depth. You saw how gradients and borders replicate classic icon parts without extra markup or images. With these techniques and a refresher on how to make a circle with CSS, how to build a semicircle top, and how to make a rectangle with CSS, you now have the tools to craft your own CSS icon set that scales cleanly across projects.