Design flourishes with ::before&::after, clip-path

::before and ::after are psuedo elements that can be attached to any regular element, and they provide useful possbilities for adding design flourishes. Clippath allows shaping regular elements with CSS, allowing for some interesting shape design without having to dip into SVG.

Clippath warning; compatibility

clippath is well supported in more browsers, both mobile and desktop, with the exception of Edge. I tried it even on some relatively older Chrome/Firefox/Mobile and it works on some 5-6 year old devices, but not Edge. So if that is a deal-breaker, don’t use it, and use some combination of SVG with either foreginobjects or co-located elements.

before and after reduce adding useless elements

These pseudo elements don’t do anything another element could not, but they do allow avoiding adding more elements that are there for design purposes only. A common use is to add border design to elements. I Also use it to create faux drop-shadows and borders for clippath elements, which is otherwise not possible.

Put them in action with a triangle SVG border

Here is the setup; two divs next to each other, but you want to add a border to make it look like they were pulled apart. To do this I set up down favcing and inverted triangle SVGs with exactly the same dimensions. Then, I attach these to adjacent elements using psuedo elements so that I don’t have any other affect on the layout. I will use flex dipslay for this example.

// Helper functions to set up and encode the SVG
const svgwrap = R.curry((width, height, viewx, viewy, path) => {
  let viewBox=`0 0 ${viewx} ${viewy}` 
  return ( `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="${width}" height="${height}" > ${path} </svg>` )
})
const svgtobase64 = (svg) => {
  return R.concat('data:image/svg+xml;base64,')(btoa(svg)) 
}

// Use the same viewbox and element size for the triangles SVG
const svgwrapTri = svgwrap(30, 10, 30, 10)
const triBase64Svg = R.pipe(
  svgwrapTri,
  svgtobase64,
)

// Then set up the two triangle version so they are like interlocking teeth.
let backgroundTriangle = triBase64Svg(`
  <polygon points="0.1 0 29.9 0 15 10" fill="hsla(215,20%,5%,0.9)" />
`)
let invertTriangle = triBase64Svg(`
  <polygon points="0 0 14.9 10 0 10" fill="hsla(0,0%,90%,0.9)" />
  <polygon points="15.1 10 30 0 30 10" fill="hsla(0,0%,90%,0.9)" />
`)

The divs and attaching the triangles

At this point it is pretty simple. Create flex display divs, make sure they will have some spacing, and then attach the triangles. If the divs have content you may need to alter the bottom/top relative positioning, but should be able to use those adjustments if needed to locate the triangles exactly where you want them.

// These are using emotion styling, if the syntax is unfamiliar
const triAfter = css`
  ::after{
    content: '';
    display: block;
    position: relative;
    bottom: 0px;
    height: 20px;
    width: 100%;
    background-size: contain;
    background-repeat: repeat;
    background-image: url(${backgroundTriangle});
  }
`
const triBefore = css`
  ::before{
    content: '';
    display: block;
    position: relative;
    top: 0px;
    height: 20px;
    width: 100%;
    background-size: contain;
    background-repeat: repeat;
    background-image: url(${invertTriangle});
  }
`

const ToothDemo = () => {
  return(
   <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' />
    <div css={[ triAfter ]} />
    <div css={[ triBefore, { margin: '30px 0 0 0'} ]} />
   </div>
  )
}

Using clippath

As I said, minus the support issue in edge, a very nice tool that can shortcut shoe-horning in SVG shapes. The primary arguments I use with it are either polygon() clips or path() clips. The paths can be svg paths, allowing a high degree of clip complexity. The downside with path() clips is that the sizing, as far as I have seen, needs to absolute relative to the element, so it does not scale well.

Some examples

// Cut off the corners, an octagon type shape
clip-path: polygon(0% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%);
// Looks nice, but does not have relative sizing, so element must
// be of known size
clip-path: path('M 0.5 1 C 0.5 1, 0 0.7, 0 0.3 A 0.25 0.25 1 1 1 0.5 0.3 A 0.25 0.25 1 1 1 1 0.3 C 1 0.7, 0.5 1, 0.5 1 Z');

clippath also removes all borders/outlines

Everything outside the clip path gets clipped path gets clipped, including CSS styling or content, if it falls outside of it.

But you can use pseudo elements to fake them

I wanted shaped images, so I used clippath, but then wanted a border. I decided to use psuedo elements to see if that would work, and it did just fine. Actually, I did it a bit backward, with the clip-path as the outer to make a border and the psuedo element as the inner to hold the image. I could have also used another div inside the outer one, but since this was meant to be a single standalone style piece using the psuedo element just avoiding using another element. Would have been fine that way too though.

// A hover effect to darken the image on hover
const hoverDiamond = css`
  position: absolute;
  left: 0; right: 0; top: 0; bottom: 0;
  &:hover { background-color: rgba(0,0,0,0.6)};
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50% );
  transition: background-color 0.2s ease;
`
// the clippath for the outer diamond 
const diamondStyle = css`
  margin: 1vh;
  position: relative;
  background-color: rgba(0,0,0,0.8);
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50% );
`
// THe style for outer diamond; a transparent black for the border
const diamond1 = css`
  background-color: rgba(0,0,0,0.8);
  z-index: 11;
`

// Use calc() and some slight relative position adjustment to get things
// lined up nicely
const diamondSetup = css`
    content: '';
    display: block;
    position: relative;
    clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50% );
    background-size: cover;
    background-repeat: no-repeat;
    background-color: rgba(0,0,0,0.8);
    height: calc(100% - 1.5vh);
    width: calc(100% - 1.5vh);
    top: 0.75vh;
    left: 0.75vh;
`
// set the image to use
const diamondImage1 = css`
  background-image: url(/path/to/image.png);
`
// Combine the setup and image. I was creating a bunch of diamonds
// so not repeating the setup was nice. This is React styling with the
// Emotion library, but the css above should work in any context.
const diamondBefore1 = css`
  ::before{
     ${diamondSetup};
     ${diamondImage1};
  }
`
// The JSX: a nice image diamond with border and hover.
 <div css={[ diamondStyle, diamond1, diamondBefore1 ]}>
   <a href="#explore" >
     <div css={[ hoverDiamond ]} />
   </a> 
 </div>