Share Button
Press Share and the options slide out from behind it, the last one pushing the pill open, while the arrow takes off and a cross draws itself. Copy link turns into a check; X and LinkedIn open in a small window.
View codeA pill that tightens into a circle on hover while its label gives way to an arrow. The clickable area never moves. Pure CSS.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Make my View project link a Kodza Morph Button.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<a class="kodza-morph-button" href="#">
<span class="kodza-morph-button__label">Take me there</span>
<span class="kodza-morph-button__mark" aria-hidden="true"><svg class="kodza-morph-button__icon" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6" /></svg></span>
</a>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Morph Button
On hover the pill closes in on itself until only a circle is left: the words are cut by the
closing edges as they move on, and an arrow slides into the circle from behind them. Only
the painted shape changes: the clickable area keeps its size, so the button never slips
away from under the pointer. Pure CSS, tuned with the --kz-morph-* variables. */
.kodza-morph-button {
--kz-morph-bg: #0a0a0a;
--kz-morph-fg: #ffffff;
--kz-morph-height: 52px;
--kz-morph-pad-x: 34px;
--kz-morph-duration: 0.6s;
--kz-morph-ease: cubic-bezier(0.76, 0, 0.24, 1);
--kz-morph-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
display: inline-grid;
place-items: center;
height: var(--kz-morph-height);
padding: 0 var(--kz-morph-pad-x);
border: 0;
background: none;
color: var(--kz-morph-fg);
font: inherit;
font-weight: 500;
letter-spacing: -0.01em;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
isolation: isolate;
-webkit-tap-highlight-color: transparent;
}
/* The shape: a pill that closes in on itself until only a circle is left */
.kodza-morph-button::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: 999px;
background: var(--kz-morph-bg);
clip-path: inset(0 0 round 999px);
transition: clip-path var(--kz-morph-duration) var(--kz-morph-ease), transform 0.25s var(--kz-morph-ease);
}
.kodza-morph-button__label,
.kodza-morph-button__mark {
grid-area: 1 / 1;
}
/* The words: cut by the same closing edges as the shape, moving on as they go */
.kodza-morph-button__label {
clip-path: inset(-4px 0);
transition:
clip-path var(--kz-morph-duration) var(--kz-morph-ease),
transform var(--kz-morph-duration) var(--kz-morph-ease),
opacity calc(var(--kz-morph-duration) * 0.5) ease calc(var(--kz-morph-duration) * 0.2);
}
/* The arrow lives in a window the size of the final circle, and slides in from behind */
.kodza-morph-button__mark {
display: grid;
place-items: center;
width: var(--kz-morph-height);
height: var(--kz-morph-height);
overflow: hidden;
border-radius: 50%;
pointer-events: none;
}
.kodza-morph-button__icon {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
transform: translateX(-34px);
transition:
transform calc(var(--kz-morph-duration) * 0.8) var(--kz-morph-ease),
opacity calc(var(--kz-morph-duration) * 0.3) ease;
}
.kodza-morph-button:hover::before,
.kodza-morph-button:focus-visible::before {
clip-path: inset(0 calc(50% - var(--kz-morph-height) / 2) round 999px);
}
.kodza-morph-button:hover .kodza-morph-button__label,
.kodza-morph-button:focus-visible .kodza-morph-button__label {
clip-path: inset(-4px calc(50% - var(--kz-morph-height) * 0.44));
opacity: 0;
transform: translateX(26px);
transition:
clip-path var(--kz-morph-duration) var(--kz-morph-ease),
transform var(--kz-morph-duration) var(--kz-morph-ease),
opacity calc(var(--kz-morph-duration) * 0.45) ease calc(var(--kz-morph-duration) * 0.3);
}
.kodza-morph-button:hover .kodza-morph-button__icon,
.kodza-morph-button:focus-visible .kodza-morph-button__icon {
opacity: 1;
transform: none;
transition:
transform calc(var(--kz-morph-duration) * 0.85) var(--kz-morph-ease) calc(var(--kz-morph-duration) * 0.25),
opacity calc(var(--kz-morph-duration) * 0.3) ease calc(var(--kz-morph-duration) * 0.3);
}
.kodza-morph-button:active::before {
transform: scale(0.94);
}
.kodza-morph-button:focus-visible {
outline: none;
}
.kodza-morph-button:focus-visible::after {
content: "";
position: absolute;
inset: -5px calc(50% - var(--kz-morph-height) / 2 - 5px);
border: 2px solid var(--kz-morph-bg);
border-radius: 999px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-morph-button::before,
.kodza-morph-button__label,
.kodza-morph-button__icon {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
/* Theming example — on a dark background:
.dark-section .kodza-morph-button {
--kz-morph-bg: #ffffff;
--kz-morph-fg: #0a0a0a;
}
*/
/* kodza-guard f3edeae8b93f — generated by Kodza, do not edit.
Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
component. :not(#_) is always true and weighs as much as an id, so these lines win over
theme rules made of classes and tags. Customise the component through its --kz-* variables. */
/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */
:where(.kodza-morph-button):not(#_) {
outline: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-morph-button:not(#_) {
border-radius: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-height: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
min-height: revert;
}
.kodza-morph-button__label:not(#_),
.kodza-morph-button__mark:not(#_) {
box-sizing: revert;
}
.kodza-morph-button__icon:not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-morph-button__icon:not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-morph-button__icon:not(#_):where([hidden]) {
display: none;
}
.kodza-morph-button:not(#_)::before,
.kodza-morph-button:not(#_)::after {
box-sizing: revert;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-morph-button:not(#_) {
position: relative;
display: inline-grid;
place-items: center;
height: var(--kz-morph-height);
padding: 0 var(--kz-morph-pad-x);
border: 0;
background: none;
color: var(--kz-morph-fg);
font: inherit;
font-weight: 500;
letter-spacing: -0.01em;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
isolation: isolate;
-webkit-tap-highlight-color: transparent;
}
.kodza-morph-button:not(#_)::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: 999px;
background: var(--kz-morph-bg);
clip-path: inset(0 0 round 999px);
transition: clip-path var(--kz-morph-duration) var(--kz-morph-ease), transform 0.25s var(--kz-morph-ease);
}
.kodza-morph-button__icon:not(#_) {
transition: transform calc(var(--kz-morph-duration) * 0.8) var(--kz-morph-ease),
opacity calc(var(--kz-morph-duration) * 0.3) ease;
}
.kodza-morph-button:hover:not(#_)::before,
.kodza-morph-button:focus-visible:not(#_)::before {
clip-path: inset(0 calc(50% - var(--kz-morph-height) / 2) round 999px);
}
.kodza-morph-button:hover .kodza-morph-button__icon:not(#_),
.kodza-morph-button:focus-visible .kodza-morph-button__icon:not(#_) {
transition: transform calc(var(--kz-morph-duration) * 0.85) var(--kz-morph-ease) calc(var(--kz-morph-duration) * 0.25),
opacity calc(var(--kz-morph-duration) * 0.3) ease calc(var(--kz-morph-duration) * 0.3);
}
.kodza-morph-button:active:not(#_)::before {
transform: scale(0.94);
}
.kodza-morph-button:focus-visible:not(#_) {
outline: none;
}
.kodza-morph-button:focus-visible:not(#_)::after {
content: "";
position: absolute;
inset: -5px calc(50% - var(--kz-morph-height) / 2 - 5px);
border: 2px solid var(--kz-morph-bg);
border-radius: 999px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-morph-button:not(#_)::before {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-morph-button__icon:not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
'use client';
// Kodza — Morph Button, for React. From https://kodza.dev/components/button-svg-morph/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: button-svg-morph.css, the component's CSS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
import { useRef, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './button-svg-morph.css';
export type MorphButtonProps = Omit<ComponentPropsWithoutRef<'a'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-morph-bg"?: string | number;
/** default: #ffffff */
"--kz-morph-fg"?: string | number;
/** default: 52px */
"--kz-morph-height"?: string | number;
/** default: 34px */
"--kz-morph-pad-x"?: string | number;
/** default: 0.6s */
"--kz-morph-duration"?: string | number;
/** default: cubic-bezier(0.76, 0, 0.24, 1) */
"--kz-morph-ease"?: string | number;
/** default: cubic-bezier(0.34, 1.56, 0.64, 1) */
"--kz-morph-spring"?: string | number;
};
/** The text */
children?: string;
className?: string;
};
export function MorphButton({ vars, children = "Take me there", className, style, ...rest }: MorphButtonProps) {
const ref = useRef<ComponentRef<'a'>>(null);
const classes = ["kodza-morph-button", className].filter(Boolean).join(' ');
// Everything the markup is made of: when it changes, React gets a fresh element (see the top)
const key = JSON.stringify([classes, vars, children, style, rest]);
return (
<a href="#" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<span className="kodza-morph-button__label">{children}</span>
{' '}
<span className="kodza-morph-button__mark" aria-hidden="true">
<svg className="kodza-morph-button__icon" viewBox="0 0 24 24">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</span>
</a>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.