Open demo

CSS code

HTML
<button type="button" class="gh">Simple fill</button>
<button type="button" class="gh gh-cyan">Neon fill</button>
<button type="button" class="gh gh-slide">Slide fill <span aria-hidden="true">→</span></button>
<button type="button" class="gh gh-rise">Rise fill <span aria-hidden="true">↑</span></button>
<button type="button" class="gh gh-dashed">Dashed</button>
CSS
/* Ghost buttons: transparent with a 2px border in --gh-c; hover fills with --gh-c and switches the label to --gh-ink. */
.gh{--gh-c:#e2e8f0;--gh-ink:#101623;position:relative;isolation:isolate;overflow:hidden;display:inline-block;
  background:transparent;border:2px solid var(--gh-c);color:var(--gh-c);border-radius:12px;padding:13px 30px;
  font:inherit;font-size:.92rem;font-weight:700;line-height:1.25;cursor:pointer;
  transition:background-color .2s,color .2s,border-color .2s,box-shadow .2s}
.gh:hover{background:var(--gh-c);color:var(--gh-ink)}
.gh:focus-visible{outline:2px solid var(--gh-c);outline-offset:3px}
.gh-cyan{--gh-c:#22d3ee;--gh-ink:#08222a}
.gh-cyan:hover{box-shadow:0 0 24px rgba(34,211,238,.4)}
/* slide and rise: a pseudo-element behind the label does the filling */
.gh-slide{--gh-c:#a78bfa;--gh-ink:#1e1140}
.gh-rise{--gh-c:#fb7185;--gh-ink:#3d0a14}
.gh-slide::before,.gh-rise::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--gh-c);transition:transform .25s ease}
.gh-slide::before{transform:translateX(-101%)}
.gh-rise::before{transform:scaleY(0);transform-origin:bottom;transition-duration:.22s}
.gh-slide:hover,.gh-rise:hover{background:transparent}
.gh-slide:hover::before,.gh-rise:hover::before{transform:none}
.gh-dashed{--gh-c:#94a3b8;border-style:dashed}
.gh-dashed:hover{background:transparent;border-style:solid;border-color:#e2e8f0;color:#e2e8f0}
@media (prefers-reduced-motion:reduce){.gh,.gh::before{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<button type="button" class="btn btn-ghost">Simple fill</button>
<button type="button" class="btn btn-ghost btn-ghost-cyan">Neon fill</button>
<button type="button" class="btn btn-ghost btn-ghost-slide">Slide fill <span aria-hidden="true">→</span></button>
<button type="button" class="btn btn-ghost btn-ghost-rise">Rise fill <span aria-hidden="true">↑</span></button>
<button type="button" class="btn btn-ghost btn-ghost-dashed">Dashed</button>
CSS
/* An outline .btn driven by two colours: --gh-c (border, label, fill) and --gh-ink (label on the fill). */
.btn-ghost{--gh-c:#e2e8f0;--gh-ink:#101623;
  --bs-btn-padding-x:30px;--bs-btn-padding-y:13px;--bs-btn-font-size:.92rem;--bs-btn-font-weight:700;--bs-btn-line-height:1.25;
  --bs-btn-border-width:2px;--bs-btn-border-radius:12px;--bs-btn-focus-box-shadow:none;
  --bs-btn-color:var(--gh-c);--bs-btn-bg:transparent;--bs-btn-border-color:var(--gh-c);
  --bs-btn-hover-color:var(--gh-ink);--bs-btn-hover-bg:var(--gh-c);--bs-btn-hover-border-color:var(--gh-c);
  --bs-btn-active-color:var(--gh-ink);--bs-btn-active-bg:var(--gh-c);--bs-btn-active-border-color:var(--gh-c);
  position:relative;isolation:isolate;overflow:hidden;transition:background-color .2s,color .2s,border-color .2s,box-shadow .2s}
.btn-ghost:focus-visible{outline:2px solid var(--gh-c);outline-offset:3px}
.btn-ghost-cyan{--gh-c:#22d3ee;--gh-ink:#08222a}
.btn-ghost-cyan:hover{box-shadow:0 0 24px rgba(34,211,238,.4)}
.btn-ghost-slide{--gh-c:#a78bfa;--gh-ink:#1e1140}
.btn-ghost-rise{--gh-c:#fb7185;--gh-ink:#3d0a14}
.btn-ghost-slide,.btn-ghost-rise{--bs-btn-hover-bg:transparent;--bs-btn-active-bg:transparent}
.btn-ghost-slide::before,.btn-ghost-rise::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--gh-c);transition:transform .25s ease}
.btn-ghost-slide::before{transform:translateX(-101%)}
.btn-ghost-rise::before{transform:scaleY(0);transform-origin:bottom;transition-duration:.22s}
.btn-ghost-slide:hover::before,.btn-ghost-rise:hover::before{transform:none}
.btn-ghost-dashed{--gh-c:#94a3b8;--bs-btn-hover-bg:transparent;--bs-btn-hover-color:#e2e8f0;--bs-btn-hover-border-color:#e2e8f0;
  --bs-btn-active-bg:transparent;--bs-btn-active-color:#e2e8f0;--bs-btn-active-border-color:#e2e8f0;border-style:dashed}
.btn-ghost-dashed:hover{border-style:solid}
@media (prefers-reduced-motion:reduce){.btn-ghost,.btn-ghost::before{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<button type="button" class="cursor-pointer rounded-xl border-2 bg-transparent px-[30px] py-[13px] text-[.92rem] leading-[1.25] font-bold transition-[color,background-color,border-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-3 motion-reduce:transition-none border-[#e2e8f0] text-[#e2e8f0] hover:bg-[#e2e8f0] hover:text-[#101623] focus-visible:outline-[#e2e8f0]">Simple fill</button>
<button type="button" class="cursor-pointer rounded-xl border-2 bg-transparent px-[30px] py-[13px] text-[.92rem] leading-[1.25] font-bold transition-[color,background-color,border-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-3 motion-reduce:transition-none border-[#22d3ee] text-[#22d3ee] hover:bg-[#22d3ee] hover:text-[#08222a] hover:shadow-[0_0_24px_rgba(34,211,238,.4)] focus-visible:outline-[#22d3ee]">Neon fill</button>
<button type="button" class="cursor-pointer rounded-xl border-2 bg-transparent px-[30px] py-[13px] text-[.92rem] leading-[1.25] font-bold transition-[color,background-color,border-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-3 motion-reduce:transition-none relative isolate overflow-hidden before:absolute before:inset-0 before:-z-10 before:transition-transform before:ease-[ease] motion-reduce:before:transition-none border-[#a78bfa] text-[#a78bfa] before:-translate-x-[101%] before:bg-[#a78bfa] before:duration-[250ms] hover:text-[#1e1140] hover:before:translate-x-0 focus-visible:outline-[#a78bfa]">Slide fill <span aria-hidden="true">→</span></button>
<button type="button" class="cursor-pointer rounded-xl border-2 bg-transparent px-[30px] py-[13px] text-[.92rem] leading-[1.25] font-bold transition-[color,background-color,border-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-3 motion-reduce:transition-none relative isolate overflow-hidden before:absolute before:inset-0 before:-z-10 before:transition-transform before:ease-[ease] motion-reduce:before:transition-none border-[#fb7185] text-[#fb7185] before:origin-bottom before:scale-y-0 before:bg-[#fb7185] before:duration-[220ms] hover:text-[#3d0a14] hover:before:scale-y-100 focus-visible:outline-[#fb7185]">Rise fill <span aria-hidden="true">↑</span></button>
<button type="button" class="cursor-pointer rounded-xl border-2 bg-transparent px-[30px] py-[13px] text-[.92rem] leading-[1.25] font-bold transition-[color,background-color,border-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-3 motion-reduce:transition-none border-dashed border-[#94a3b8] text-[#94a3b8] hover:border-solid hover:border-[#e2e8f0] hover:text-[#e2e8f0] focus-visible:outline-[#94a3b8]">Dashed</button>

About this set

Five transparent outline buttons for dark backgrounds, each with a different way of filling in on hover. Simple fill swaps to a solid light background with a dark label. Neon fill does the same in cyan and adds a soft glow. Slide fill moves a violet layer in from the left, and Rise fill grows a pink layer up from the bottom; both use a pseudo-element placed behind the label inside an isolated, overflow-hidden button, so the label always stays on top. Dashed keeps a grey dashed border that turns solid and brighter on hover. Use ghost buttons as secondary actions on dark heroes, footers and cards, next to a solid primary button. Each colour is two custom properties: –gh-c for the border, label and fill, and –gh-ink for the label once the button is filled, which keeps the hover state readable. All border and label colours reach at least 4.5:1 against the dark #101623 page. The arrows in the Slide and Rise labels are decorative and hidden from screen readers. The Bootstrap version maps the same two colours onto the –bs-btn-* colour variables of .btn.

What’s included

  • Five hover fills: simple, neon glow, slide, rise and dashed-to-solid
  • Pseudo-element fills kept behind the label with isolation: isolate
  • Two custom properties per colour: --gh-c and --gh-ink
  • Readable labels on the dark page and on every fill
  • Transitions removed under prefers-reduced-motion

Accessibility

Each control is a native button named by its visible text; the arrow glyphs are wrapped in aria-hidden spans so they are not read out. Tab reaches every button and focus draws a 2px outline in the button's own colour, 3px outside the border. Under prefers-reduced-motion the colour change and the sliding fills happen without animation.

Customise it

Change --gh-c and --gh-ink on .gh or a modifier such as .gh-slide in plain CSS, or on .btn-ghost-* in Bootstrap, where they feed --bs-btn-color, --bs-btn-border-color and --bs-btn-hover-bg. In Tailwind edit the border-, text-, hover:bg- and before:bg- colour utilities.