25 CSS Button Groups03 / 25
CSS View Transitions Toggle Button Group with Morphing Thumb (Shared-Element FLIP)
A toggle button group where the active highlight doesn't slide — it morphs. Powered by the View Transitions API, the highlight is a shared element that the browser FLIP-animates from the old button to the new one, smoothly stretching across size and position changes. The forward-looking 'CSS View Transitions button group morphing highlight' pattern for 2027 UIs.
Published
The code
<section class="btn-03" aria-label="View transitions morphing toggle group demo">
<div class="btn-03__wrap">
<p class="btn-03__label">Workspace theme</p>
<div class="btn-03__group" id="btn-03-group" role="radiogroup" aria-label="Workspace theme">
<button type="button" class="btn-03__btn" role="radio" aria-checked="true" tabindex="0"><span class="btn-03__hl"></span><span class="btn-03__t">Focus</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Comfortable</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Compact</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Zen</span></button>
</div>
</div>
</section><section class="btn-03" aria-label="View transitions morphing toggle group demo">
<div class="btn-03__wrap">
<p class="btn-03__label">Workspace theme</p>
<div class="btn-03__group" id="btn-03-group" role="radiogroup" aria-label="Workspace theme">
<button type="button" class="btn-03__btn" role="radio" aria-checked="true" tabindex="0"><span class="btn-03__hl"></span><span class="btn-03__t">Focus</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Comfortable</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Compact</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Zen</span></button>
</div>
</div>
</section>.btn-03,
.btn-03 *,
.btn-03 *::before,
.btn-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-03 {
--accent: oklch(0.5 0.22 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg,#f4f5fa,#e7e9f3);
padding: 44px 20px;
}
.btn-03__wrap {
width: min(560px,100%);
}
.btn-03__label {
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 14px 4px;
}
.btn-03__group {
display: inline-flex;
gap: 6px;
padding: 6px;
background: #fff;
border: 1px solid #e2e4f0;
border-radius: 16px;
box-shadow: 0 16px 40px -26px rgba(40,30,120,.5);
}
.btn-03__btn {
position: relative;
border: none;
background: none;
min-height: 46px;
padding: 0 22px;
border-radius: 11px;
font-size: 15px;
font-weight: 700;
color: #5a5f80;
cursor: pointer;
overflow: hidden;
transition: color .2s;
}
.btn-03__hl {
position: absolute;
inset: 0;
border-radius: 11px;
background: var(--accent);
opacity: 0;
}
.btn-03__t {
position: relative;
z-index: 1;
}
.btn-03__btn[aria-checked=true] {
color: #fff;
}
.btn-03__btn[aria-checked=true] .btn-03__hl {
opacity: 1;
view-transition-name: btn-03-thumb;
box-shadow: 0 6px 16px -6px var(--accent);
}
.btn-03__btn:not([aria-checked=true]):hover {
color: #2a2e50;
}
.btn-03__btn:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
::view-transition-group(btn-03-thumb) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.34,1.25,.4,1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(btn-03-thumb) {
animation-duration: 1ms;
}
}.btn-03,
.btn-03 *,
.btn-03 *::before,
.btn-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-03 {
--accent: oklch(0.5 0.22 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg,#f4f5fa,#e7e9f3);
padding: 44px 20px;
}
.btn-03__wrap {
width: min(560px,100%);
}
.btn-03__label {
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 14px 4px;
}
.btn-03__group {
display: inline-flex;
gap: 6px;
padding: 6px;
background: #fff;
border: 1px solid #e2e4f0;
border-radius: 16px;
box-shadow: 0 16px 40px -26px rgba(40,30,120,.5);
}
.btn-03__btn {
position: relative;
border: none;
background: none;
min-height: 46px;
padding: 0 22px;
border-radius: 11px;
font-size: 15px;
font-weight: 700;
color: #5a5f80;
cursor: pointer;
overflow: hidden;
transition: color .2s;
}
.btn-03__hl {
position: absolute;
inset: 0;
border-radius: 11px;
background: var(--accent);
opacity: 0;
}
.btn-03__t {
position: relative;
z-index: 1;
}
.btn-03__btn[aria-checked=true] {
color: #fff;
}
.btn-03__btn[aria-checked=true] .btn-03__hl {
opacity: 1;
view-transition-name: btn-03-thumb;
box-shadow: 0 6px 16px -6px var(--accent);
}
.btn-03__btn:not([aria-checked=true]):hover {
color: #2a2e50;
}
.btn-03__btn:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
::view-transition-group(btn-03-thumb) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.34,1.25,.4,1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(btn-03-thumb) {
animation-duration: 1ms;
}
}Here's a working CSS Button Group from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS View Transitions Toggle Button Group with Morphing Thumb (Shared-Element FLIP)
Source: https://codefronts.com/components/css-button-groups/css-view-transitions-toggle-button-group-with-morphing-thumb-shared-element-flip/
A toggle button group where the active highlight doesn't slide — it morphs. Powered by the View Transitions API, the highlight is a shared element that the browser FLIP-animates from the old button to the new one, smoothly stretching across size and position changes. The forward-looking 'CSS View Transitions button group morphing highlight' pattern for 2027 UIs.
## HTML
```html
<section class="btn-03" aria-label="View transitions morphing toggle group demo">
<div class="btn-03__wrap">
<p class="btn-03__label">Workspace theme</p>
<div class="btn-03__group" id="btn-03-group" role="radiogroup" aria-label="Workspace theme">
<button type="button" class="btn-03__btn" role="radio" aria-checked="true" tabindex="0"><span class="btn-03__hl"></span><span class="btn-03__t">Focus</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Comfortable</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Compact</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Zen</span></button>
</div>
</div>
</section>
```
## CSS
```css
.btn-03,
.btn-03 *,
.btn-03 *::before,
.btn-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-03 {
--accent: oklch(0.5 0.22 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg,#f4f5fa,#e7e9f3);
padding: 44px 20px;
}
.btn-03__wrap {
width: min(560px,100%);
}
.btn-03__label {
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 14px 4px;
}
.btn-03__group {
display: inline-flex;
gap: 6px;
padding: 6px;
background: #fff;
border: 1px solid #e2e4f0;
border-radius: 16px;
box-shadow: 0 16px 40px -26px rgba(40,30,120,.5);
}
.btn-03__btn {
position: relative;
border: none;
background: none;
min-height: 46px;
padding: 0 22px;
border-radius: 11px;
font-size: 15px;
font-weight: 700;
color: #5a5f80;
cursor: pointer;
overflow: hidden;
transition: color .2s;
}
.btn-03__hl {
position: absolute;
inset: 0;
border-radius: 11px;
background: var(--accent);
opacity: 0;
}
.btn-03__t {
position: relative;
z-index: 1;
}
.btn-03__btn[aria-checked=true] {
color: #fff;
}
.btn-03__btn[aria-checked=true] .btn-03__hl {
opacity: 1;
view-transition-name: btn-03-thumb;
box-shadow: 0 6px 16px -6px var(--accent);
}
.btn-03__btn:not([aria-checked=true]):hover {
color: #2a2e50;
}
.btn-03__btn:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
::view-transition-group(btn-03-thumb) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.34,1.25,.4,1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(btn-03-thumb) {
animation-duration: 1ms;
}
}
```Here's a working CSS Button Group from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS View Transitions Toggle Button Group with Morphing Thumb (Shared-Element FLIP)
Source: https://codefronts.com/components/css-button-groups/css-view-transitions-toggle-button-group-with-morphing-thumb-shared-element-flip/
A toggle button group where the active highlight doesn't slide — it morphs. Powered by the View Transitions API, the highlight is a shared element that the browser FLIP-animates from the old button to the new one, smoothly stretching across size and position changes. The forward-looking 'CSS View Transitions button group morphing highlight' pattern for 2027 UIs.
## HTML
```html
<section class="btn-03" aria-label="View transitions morphing toggle group demo">
<div class="btn-03__wrap">
<p class="btn-03__label">Workspace theme</p>
<div class="btn-03__group" id="btn-03-group" role="radiogroup" aria-label="Workspace theme">
<button type="button" class="btn-03__btn" role="radio" aria-checked="true" tabindex="0"><span class="btn-03__hl"></span><span class="btn-03__t">Focus</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Comfortable</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Compact</span></button>
<button type="button" class="btn-03__btn" role="radio" aria-checked="false" tabindex="-1"><span class="btn-03__hl"></span><span class="btn-03__t">Zen</span></button>
</div>
</div>
</section>
```
## CSS
```css
.btn-03,
.btn-03 *,
.btn-03 *::before,
.btn-03 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-03 {
--accent: oklch(0.5 0.22 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg,#f4f5fa,#e7e9f3);
padding: 44px 20px;
}
.btn-03__wrap {
width: min(560px,100%);
}
.btn-03__label {
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 14px 4px;
}
.btn-03__group {
display: inline-flex;
gap: 6px;
padding: 6px;
background: #fff;
border: 1px solid #e2e4f0;
border-radius: 16px;
box-shadow: 0 16px 40px -26px rgba(40,30,120,.5);
}
.btn-03__btn {
position: relative;
border: none;
background: none;
min-height: 46px;
padding: 0 22px;
border-radius: 11px;
font-size: 15px;
font-weight: 700;
color: #5a5f80;
cursor: pointer;
overflow: hidden;
transition: color .2s;
}
.btn-03__hl {
position: absolute;
inset: 0;
border-radius: 11px;
background: var(--accent);
opacity: 0;
}
.btn-03__t {
position: relative;
z-index: 1;
}
.btn-03__btn[aria-checked=true] {
color: #fff;
}
.btn-03__btn[aria-checked=true] .btn-03__hl {
opacity: 1;
view-transition-name: btn-03-thumb;
box-shadow: 0 6px 16px -6px var(--accent);
}
.btn-03__btn:not([aria-checked=true]):hover {
color: #2a2e50;
}
.btn-03__btn:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
::view-transition-group(btn-03-thumb) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.34,1.25,.4,1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(btn-03-thumb) {
animation-duration: 1ms;
}
}
```How this works
Each button owns a highlight layer, but only the active one carries view-transition-name:btn-03-thumb. When selection changes, a ~20-line accessible radiogroup helper wraps the state update in document.startViewTransition(). The browser snapshots the old highlight and the new one, and because they share a transition name it treats them as the same element — morphing position AND size (a real shared-element transition, not a CSS translate).
The result reads like the highlight liquid-morphs between buttons of different widths, something a fixed-width sliding thumb can't do. Keyboard arrow keys and roving tabindex keep it a proper radiogroup; without the API it degrades to an instant, still-correct toggle.
.btn[aria-checked=true] .hl{ view-transition-name:btn-03-thumb }
document.startViewTransition(() => setActive(btn)); // browser FLIP-morphs the highlightTechniques used: View Transitions API shared-element morph (position + size FLIP) · single view-transition-name on the active highlight only · accessible radiogroup with roving tabindex + arrow keys · ::view-transition-group custom easing · reduced-motion instant fallback · progressive enhancement (works with no API)
Make it yours
- Morph feel = the
::view-transition-group(btn-03-thumb)animation duration/easing. - Highlight color =
--accent. - Add options freely — variable widths are exactly what the morph shows off.
- Swap to a pill or underline highlight; the shared-element morph adapts.
Gotchas — read before shipping
- A
view-transition-namemust be unique per snapshot — keep it ONLY on the active highlight, never on all of them. - The View Transitions API is Chrome/Safari; guard with
if (document.startViewTransition)and fall back to an instant update (done here). - Keep it a real radiogroup: roving tabindex + arrow keys, one Tab stop — don't lose keyboard operability for the effect.
- Neutralize the transition under
prefers-reduced-motionby zeroing the ::view-transition-group animation.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 18+ | no | 111+ |
Same-document View Transitions ship in Chrome 111+ and Safari 18+; Firefox not yet. It's a progressive enhancement — the group works everywhere and only gains the morph where supported.