Open demo

CSS code

HTML
<div class="demo-row">
  <button class="tb tb-underline" type="button">Offset underline</button>
  <button class="tb tb-wipe" type="button">Wipe underline</button>
  <a class="tb tb-arrow" href="#guide">Read the guide <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
  <button class="tb tb-danger" type="button">Delete account</button>
  <button class="tb" type="button" disabled>Unavailable</button>
</div>
CSS
/* Text buttons: no fill, the underline carries the affordance. Buttons for actions, <a> for navigation. */
.tb{display:inline-flex;align-items:center;gap:8px;border:0;background:none;padding:6px 2px;font:inherit;
  font-size:.94rem;font-weight:700;line-height:normal;color:#4f46e5;text-decoration:none;cursor:pointer}
.tb:focus-visible{outline:2px solid #4f46e5;outline-offset:3px;border-radius:2px}
/* 1. offset underline: always on, only its colour changes on hover */
.tb-underline{text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:4px;
  text-decoration-skip-ink:auto;transition:text-decoration-color .2s}
.tb-underline:hover{text-decoration-color:#a5b4fc}
/* 2. wipe underline: drawn in from the left on hover and focus */
.tb-wipe{position:relative}
.tb-wipe::after{content:"";position:absolute;left:2px;right:2px;bottom:2px;height:2px;background:currentColor;
  transform:scaleX(0);transform-origin:right;transition:transform .26s cubic-bezier(.4,0,.2,1)}
.tb-wipe:hover::after,.tb-wipe:focus-visible::after{transform:scaleX(1);transform-origin:left}
/* 3. arrow link: a real link, the arrow nudges right on hover */
.tb-arrow{color:#344054;transition:color .18s}
.tb-arrow svg{display:block;width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2.2;transition:transform .22s}
.tb-arrow:hover{color:#4f46e5}
.tb-arrow:hover svg{transform:translateX(4px)}
/* 4. destructive */
.tb-danger{color:#b91c1c}
.tb-danger:hover,.tb-danger:focus-visible{text-decoration:underline;text-underline-offset:4px;text-decoration-thickness:2px}
.tb-danger:focus-visible{outline-color:#b91c1c}
/* 5. disabled */
.tb:disabled{color:#98a2b3;cursor:not-allowed}
@media (prefers-reduced-motion:reduce){
  .tb-underline,.tb-wipe::after,.tb-arrow,.tb-arrow svg{transition:none}
  .tb-arrow:hover svg{transform:none}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="demo-row">
  <button class="btn btn-link tb tb-underline" type="button">Offset underline</button>
  <button class="btn btn-link tb tb-wipe" type="button">Wipe underline</button>
  <a class="btn btn-link tb tb-arrow" href="#guide">Read the guide <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
  <button class="btn btn-link tb tb-danger" type="button">Delete account</button>
  <button class="btn btn-link tb" type="button" disabled>Unavailable</button>
</div>
CSS
/* Text buttons: Bootstrap's .btn-link restyled through --bs-btn-* variables. Buttons for actions, <a> for navigation. */
.tb{--bs-btn-padding-x:2px;--bs-btn-padding-y:6px;--bs-btn-font-size:.94rem;--bs-btn-font-weight:700;
  --bs-btn-line-height:normal;--bs-btn-border-width:0;--bs-btn-border-radius:2px;
  --bs-btn-color:#4f46e5;--bs-btn-hover-color:#4f46e5;--bs-btn-active-color:#4f46e5;
  --bs-btn-disabled-color:#98a2b3;--bs-btn-disabled-opacity:1;
  --bs-btn-focus-box-shadow:0 0 0 3px #f4f5f9,0 0 0 5px #4f46e5;
  display:inline-flex;align-items:center;gap:8px;text-decoration:none}
.tb.btn-link:hover{text-decoration:none}
/* 1. offset underline: always on, only its colour changes on hover */
.tb-underline,.tb-underline.btn-link:hover{text-decoration:underline;text-decoration-thickness:2px;
  text-underline-offset:4px;text-decoration-skip-ink:auto;transition:text-decoration-color .2s}
.tb-underline.btn-link:hover{text-decoration-color:#a5b4fc}
/* 2. wipe underline */
.tb-wipe{position:relative}
.tb-wipe::after{content:"";position:absolute;left:2px;right:2px;bottom:2px;height:2px;background:currentColor;
  transform:scaleX(0);transform-origin:right;transition:transform .26s cubic-bezier(.4,0,.2,1)}
.tb-wipe:hover::after,.tb-wipe:focus-visible::after{transform:scaleX(1);transform-origin:left}
/* 3. arrow link */
.tb-arrow{--bs-btn-color:#344054;--bs-btn-hover-color:#4f46e5;--bs-btn-active-color:#4f46e5;transition:color .18s}
.tb-arrow svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2.2;transition:transform .22s}
.tb-arrow:hover svg{transform:translateX(4px)}
/* 4. destructive */
.tb-danger{--bs-btn-color:#b91c1c;--bs-btn-hover-color:#b91c1c;--bs-btn-active-color:#b91c1c;
  --bs-btn-focus-box-shadow:0 0 0 3px #f4f5f9,0 0 0 5px #b91c1c}
.tb-danger.btn-link:hover,.tb-danger.btn-link:focus-visible{text-decoration:underline;text-underline-offset:4px;
  text-decoration-thickness:2px}
@media (prefers-reduced-motion:reduce){
  .tb-underline,.tb-wipe::after,.tb-arrow,.tb-arrow svg{transition:none}
  .tb-arrow:hover svg{transform:none}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="flex flex-wrap items-center justify-center gap-[30px]">
  <button type="button" class="inline-flex cursor-pointer items-center gap-2 rounded-[2px] px-0.5 py-1.5 text-[.94rem] font-bold no-underline focus-visible:outline-2 focus-visible:outline-offset-[3px] text-[#4f46e5] underline decoration-2 underline-offset-4 transition-[text-decoration-color] duration-200 hover:decoration-[#a5b4fc] focus-visible:outline-[#4f46e5] motion-reduce:transition-none">Offset underline</button>
  <button type="button" class="inline-flex cursor-pointer items-center gap-2 rounded-[2px] px-0.5 py-1.5 text-[.94rem] font-bold no-underline focus-visible:outline-2 focus-visible:outline-offset-[3px] relative text-[#4f46e5] after:absolute after:right-0.5 after:bottom-0.5 after:left-0.5 after:h-0.5 after:origin-right after:scale-x-0 after:bg-current after:transition-transform after:duration-[260ms] after:ease-[cubic-bezier(.4,0,.2,1)] after:content-[''] hover:after:origin-left hover:after:scale-x-100 focus-visible:outline-[#4f46e5] focus-visible:after:origin-left focus-visible:after:scale-x-100 motion-reduce:after:transition-none">Wipe underline</button>
  <a href="#guide" class="group inline-flex cursor-pointer items-center gap-2 rounded-[2px] px-0.5 py-1.5 text-[.94rem] font-bold no-underline focus-visible:outline-2 focus-visible:outline-offset-[3px] text-[#344054] transition-[color] duration-[180ms] hover:text-[#4f46e5] focus-visible:outline-[#4f46e5] motion-reduce:transition-none">Read the guide <svg viewBox="0 0 24 24" aria-hidden="true" class="block size-[15px] fill-none stroke-current stroke-[2.2] transition-transform duration-[220ms] group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
  <button type="button" class="inline-flex cursor-pointer items-center gap-2 rounded-[2px] px-0.5 py-1.5 text-[.94rem] font-bold no-underline focus-visible:outline-2 focus-visible:outline-offset-[3px] text-[#b91c1c] decoration-2 underline-offset-4 hover:underline focus-visible:underline focus-visible:outline-[#b91c1c]">Delete account</button>
  <button type="button" disabled class="inline-flex cursor-pointer items-center gap-2 rounded-[2px] px-0.5 py-1.5 text-[.94rem] font-bold no-underline focus-visible:outline-2 focus-visible:outline-offset-[3px] text-[#4f46e5] focus-visible:outline-[#4f46e5] disabled:cursor-not-allowed disabled:text-[#98a2b3]">Unavailable</button>
</div>

About this set

Five buttons with no fill, where the text and its underline do the work. Offset underline keeps a 2 pixel underline four pixels below the text at all times and only lightens it on hover. Wipe underline draws its line in from the left on hover or focus and wipes it out to the right. Read the guide is a link rather than a button, because it navigates, and its arrow nudges four pixels to the right on hover. Delete account is a red destructive action that underlines on hover and focus, and Unavailable is a disabled button in grey. Use the offset underline anywhere the button might sit in running text, and keep the wipe and destructive styles for toolbars, card footers and dialogs, where the position already says it is a control. Actions are button elements and navigation is an anchor, so each announces its real role. The Bootstrap version restyles .btn-link through the –bs-btn-* variables, and the Tailwind version builds the wipe line with after: utilities and the arrow nudge with group-hover.

What’s included

  • Always-on offset underline that changes colour on hover
  • Wipe-in underline drawn with a scaled pseudo-element
  • Arrow link for navigation, built on an anchor
  • Destructive and disabled text buttons
  • Underline and arrow motion off under reduced motion

Accessibility

Actions are native buttons and Read the guide is a real link, so each is announced with the right role, and the arrow SVG is aria-hidden. Label colours reach at least 5.7:1 against the page, and the disabled button uses the disabled attribute. Keyboard focus draws a 2px outline in the button's own colour, and the wipe and destructive underlines also appear on focus. Under prefers-reduced-motion the underline and colour transitions are removed and the arrow no longer moves.

Customise it

Change the colour on .tb and each variant, and text-underline-offset and text-decoration-thickness on .tb-underline. In Bootstrap edit --bs-btn-color and --bs-btn-hover-color on .tb and its variants; in Tailwind edit the text-, decoration- and underline-offset- utilities.