Project: Re-creating a Simple Logo with CSS Shapes

You will build a clean, memorable “play badge” logo with nothing but CSS shapes. The mark is a solid circular badge with a right-pointing triangle centered inside. No SVG, no images, and no icon fonts. By the end, you will own a tidy component that scales with a single custom property, themes in one place, and drops into any header, footer, or splash screen.

Why Project: Re-creating a Simple Logo with CSS Shapes Matters

Logos and icons often start life as SVGs, and that is a fine choice for complex marks. For simple, geometric logos, CSS shapes are fast to render, trivial to theme with custom properties, and reduce asset management. You avoid extra HTTP requests and keep color, size, and states in your stylesheet where your design tokens already live. CSS shapes also adapt smoothly to container queries or media queries, so the same logo can serve tiny headers and oversized hero sections with one code path.

Prerequisites

This is a focused build, not a CSS 101. If you have these basics, you are set:

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The logo is a single element with a presentational class. We give it a role for assistive tech and an accessible label that fits the use case. The triangle and decorative ring will come from pseudo-elements, so the markup stays lean. Here is the complete HTML you will use in your page:

<!-- HTML -->
<div class="logo-demo">
  <div class="logo" role="img" aria-label="Play media"></div>
</div>

.logo-demo is only a flex wrapper to center the logo in this tutorial. The .logo element is the entire mark. The circle will be the base of .logo, and the triangle will be a ::before pseudo-element inside it. We will add a subtle highlight ring with ::after to give the badge depth on dark or light backgrounds.

Step 2: The Basic CSS & Styling

Start with a small set of design tokens. Control the size, colors, and ring tint from :root. Use a simple layout container to center the example on the page. The .logo gets position: relative so its pseudo-elements can anchor to it cleanly.

/* CSS */

/* CSS */
:root {
  --logo-size: 120px;
  --logo-bg: #0f172a;        /* page background (slate-900 style) */
  --badge: #ec1d24;          /* badge fill (brand red) */
  --triangle: #ffffff;       /* triangle (white) */
  --ring: rgba(255, 255, 255, 0.18); /* highlight ring tint */
  --shadow: rgba(0, 0, 0, 0.35);     /* drop shadow on badge */
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html, body {
  height: 100%;
}

body {
  margin: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
  background: var(--logo-bg);
  color: #e2e8f0;
  display: grid;
  place-items: center;
}

.logo-demo {
  display: grid;
  gap: 1rem;
  align-items: center;
  justify-items: center;
  padding: 2rem;
}

.logo {
  --s: var(--logo-size);
  width: var(--s);
  aspect-ratio: 1 / 1;
  position: relative;
  border-radius: 50%;
  background: var(--badge);
  box-shadow:
    0 10px 18px var(--shadow),
    inset 0 -6px 12px rgba(0,0,0,0.25);
}

Advanced Tip: Keep all theme values in custom properties. You can drop this logo into any brand color system by changing –badge and –triangle at the component level. It also helps when building dark and light variants using a class or a @media (prefers-color-scheme) query.

Step 3: Building the Circle Badge

The circular badge already exists as the .logo element. Now you will add a subtle, semi-transparent ring to make the mark sit nicely on both dark and light surfaces. The ring is an ::after pseudo-element that uses border-radius and an inset box-shadow to fake a soft highlight.

/* CSS */

/* CSS */
.logo::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  box-shadow:
    inset 0 0 0 calc(var(--s) * 0.03) var(--ring);
  /* Adds a thin inner ring relative to size */
  mix-blend-mode: screen;
  /* Lighten effect on darker backgrounds */
}

How This Works (Code Breakdown)

We keep the base shape clean: border-radius: 50% on .logo yields a perfect circle that scales with width. If you want a deeper primer on circles, the guide on how to make a circle with CSS covers the approach and alternatives.

The ::after pseudo-element sits on top of the badge and uses inset: 0 to fill the circle. box-shadow draws a thin inner ring by using an inset spread value computed from the size variable. This keeps the ring consistent at every size. mix-blend-mode: screen brightens the ring over darker tones while staying soft and unobtrusive. pointer-events: none prevents any interaction side effects in clickable contexts.

The drop shadow on .logo gives the badge lift. The subtle inner shadow adds a slight edge and helps the triangle read crisply once we drop it in. These are presentational enhancements; the shape itself remains a single element.

Step 4: Building the Play Triangle

The triangle is a classic border trick. You create a zero-sized box and define transparent top and bottom borders with a solid left border to craft a right-pointing triangle. Then you center it with absolute positioning and translate. This avoids extra markup and keeps the logo flexible. Add it as ::before so the triangle sits above the badge but below any future sparkle or notification dot.

/* CSS */

/* CSS */
.logo::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  /* Box has no intrinsic size; we build the triangle from borders */
  width: 0;
  height: 0;

  /* Triangle size scales with badge size */
  --tri-w: calc(var(--s) * 0.34); /* base width */
  --tri-h: calc(var(--s) * 0.36); /* total height */

  border-top: calc(var(--tri-h) * 0.5) solid transparent;
  border-bottom: calc(var(--tri-h) * 0.5) solid transparent;
  border-left: var(--tri-w) solid var(--triangle);

  transform: translate(-45%, -50%); /* slight left bias for optical centering */
  filter: drop-shadow(0 2px 1px rgba(0,0,0,0.15));
}

How This Works (Code Breakdown)

A triangle from borders relies on the way CSS miter joins meet at a point. The element has no width or height, so the visible area comes entirely from border widths. Two transparent borders (top and bottom) plus one colored border (left) form a crisp wedge. A full write-up appears in the guide on how to make a triangle right with CSS, which also covers variants for each direction.

The triangle’s size scales with –s so the logo grows or shrinks by adjusting one variable. The proportions above yield a familiar media icon look, but you control the values to match your own brand. The transform uses -45% on X instead of -50% because the triangle’s visual center leans right. That small nudge keeps the triangle from feeling back-weighted.

We add a soft drop-shadow via filter to lift the triangle off the badge, improving contrast on mid-tones and against photography if you place the mark over images. The triangle remains pure CSS, no extra wrapper needed.

If you prefer to mock a rectangular placeholder during layout, a fast refresher on how to make a rectangle with CSS can help you stage elements before switching back to the triangle.

Advanced Techniques: Adding Animations & Hover Effects

Motion gives the logo presence during hover or on a landing screen. You will add a gentle scale on hover and an optional “pulse ring” that radiates out, then fades. Use transform and opacity for the animation path so the effect looks smooth and stays light on the main thread.

/* CSS */

/* CSS */
.logo {
  transition: transform 200ms cubic-bezier(.2,.8,.2,1);
  cursor: pointer;
}

.logo:hover {
  transform: scale(1.04);
}

/* Pulse ring using a second pseudo-element */
.logo::after {
  /* Keep the highlight ring from Step 3 */
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  box-shadow: inset 0 0 0 calc(var(--s) * 0.03) var(--ring);
  mix-blend-mode: screen;
}

.logo::marker { /* noop safeguard in some lists */ }

/* Create the pulse as a new element we animate behind the badge */
.logo::part(pulse) { /* not used; shown for pattern awareness */ }

/* Use a generated element for the pulse with ::after clone via ::backdrop? Not supported.
   Instead, add a dedicated pulse element with a second shadow layer: */
.logo::before {
  /* triangle from Step 4 remains here */
}

/* Add a separate pulse layer using an extra wrapper if you need strict layering,
   but we can fake a ring pulse by animating a spread shadow on the badge itself: */
@keyframes ring-pulse {
  0%   { box-shadow: 0 10px 18px var(--shadow), inset 0 -6px 12px rgba(0,0,0,0.25), 0 0 0 0 rgba(255,255,255,0.35); }
  70%  { box-shadow: 0 10px 18px var(--shadow), inset 0 -6px 12px rgba(0,0,0,0.25), 0 0 0 calc(var(--s) * 0.25) rgba(255,255,255,0); }
  100% { box-shadow: 0 10px 18px var(--shadow), inset 0 -6px 12px rgba(0,0,0,0.25), 0 0 0 calc(var(--s) * 0.28) rgba(255,255,255,0); }
}

.logo.is-pulsing {
  animation: ring-pulse 1100ms ease-out 1;
}

/* Respect user motion settings */
@media (prefers-reduced-motion: reduce) {
  .logo,
  .logo.is-pulsing {
    transition: none;
    animation: none;
  }
}

The hover transform is subtle on purpose. It signals interactivity without turning the logo into a jelly. The pulse uses a layered box-shadow that expands and fades. For one-off hero moments, toggle .is-pulsing with JS on page load or when the logo scrolls into view. If you plan to pulse repeatedly, use a longer interval and keep opacity modest so it does not distract from content.

Accessibility & Performance

Logos can be decorative or they can convey function, like a “play” button. Treat the component based on the context. Accessibility and performance are not optional polish; they are part of the design.

Accessibility

The example uses role=”img” with aria-label=”Play media” so screen readers can announce meaning. If the logo appears near text that already states the brand or action, remove the label and add aria-hidden=”true” to keep it out of the accessibility tree. If the mark triggers playback, use a <button> with an accessible name and place the visual logo inside it, then rely on focus styles that meet contrast targets.

Motion needs care. Many users prefer calmer interfaces. The @media (prefers-reduced-motion: reduce) query disables hover scaling and pulse animation for those visitors. Your final design should follow the same pattern for any looping or flashy effects.

Performance

The shapes here are cheap to render. border-radius and border-built triangles are fast in modern engines. Animations that use transform and opacity stay on the compositor path, which yields smoother frames under load. Be mindful with box-shadow animations. One pulse on page load is usually fine, but constant shadows that expand or blur can tax mid-range devices. If you want a repeating pulse, consider a separate absolutely positioned ring element that animates transform: scale and opacity instead of box-shadow growth. For a heavier mark with multiple layers, keep reflows in check by avoiding layout thrash and sticking to GPU-friendly properties.

Ship Your Own CSS Logo Kit

You have a fully responsive logo built from a circle and a triangle, wired with custom properties, and ready for hover and pulse states. You saw how to scale it with a single variable, label it for assistive tech, and respect motion preferences. Keep exploring the shape library to push the mark further: you can swap the triangle for a star or add a clipped ribbon tail. Now you have the baseline to craft a tailored icon set with consistent geometry and zero image assets.

Leave a Comment