22 CSS Feature Sections21 / 22

Pure CSSMIT licensed

Feature Section with Floating Phone Mockup

A split feature block with three callouts on the left and a phone drawn entirely in CSS on the right, tilted a few degrees and floating on a soft contact shadow. The frame, bezel, notch and app screen are all elements — no image, no 4MB PNG, no export cycle — so the screen content is real text that reflows and stays readable at any size.

Published Updated

Live Demo
Try it

The code

<section class="fs-21" aria-labelledby="fs-21-heading">
  <div class="fs-21__hero" aria-hidden="true">
    <span class="fs-21__wordmark">Sundial</span>
    <span class="fs-21__heronav">For freelancers who invoice</span>
  </div>

  <div class="fs-21__stage">
    <div class="fs-21__split">
      <div class="fs-21__copy">
        <p class="fs-21__eyebrow">On the phone</p>
        <h2 class="fs-21__heading" id="fs-21-heading">Your books, closed before you leave the site</h2>
        <p class="fs-21__intro">Sundial turns the hour you spend on admin every evening into the ninety seconds it should be.</p>
        <ul class="fs-21__callouts">
          <li class="fs-21__callout">
            <span class="fs-21__icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 10h8M8 14h5"/></svg></span>
            <div><h3 class="fs-21__ctitle">Invoice from the job, not the desk</h3><p class="fs-21__cprose">Finish a job, tap once, and the invoice goes out with the hours and parts already filled in.</p></div>
          </li>
          <li class="fs-21__callout">
            <span class="fs-21__icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 3v18M7 8h10M7 16h10"/></svg></span>
            <div><h3 class="fs-21__ctitle">Chasing that you do not do</h3><p class="fs-21__cprose">Polite reminders at seven, fourteen and thirty days, stopping the moment the money lands.</p></div>
          </li>
          <li class="fs-21__callout">
            <span class="fs-21__icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M4 18V9M10 18V5M16 18v-6M3 21h18"/></svg></span>
            <div><h3 class="fs-21__ctitle">Tax set aside as you earn</h3><p class="fs-21__cprose">A running estimate with a pot that fills automatically, so January is a formality.</p></div>
          </li>
        </ul>
        <a class="fs-21__cta" href="#fs-21-heading">Try it on your next invoice</a>
      </div>

      <div class="fs-21__mockwrap" aria-hidden="true">
        <div class="fs-21__phone">
          <span class="fs-21__notch"></span>
          <div class="fs-21__screen">
            <div class="fs-21__apphead"><b>This month</b><span>August</span></div>
            <div class="fs-21__balance">
              <span class="fs-21__blabel">Paid</span>
              <span class="fs-21__bvalue">£8,420</span>
              <span class="fs-21__bmeta">£2,160 outstanding · 3 invoices</span>
            </div>
            <div class="fs-21__txns">
              <div class="fs-21__txn"><b>Fern Studio</b><i>£1,200 · paid</i></div>
              <div class="fs-21__txn"><b>Orbit Ltd</b><i>£860 · due in 4d</i></div>
              <div class="fs-21__txn fs-21__txn--late"><b>Vale &amp; Co</b><i>£1,300 · 12d late</i></div>
              <div class="fs-21__txn"><b>Basalt Group</b><i>£2,240 · paid</i></div>
            </div>
            <div class="fs-21__tabs"><span class="fs-21__tab" data-on></span><span class="fs-21__tab"></span><span class="fs-21__tab"></span></div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <div class="fs-21__below" aria-hidden="true">iOS and Android · Free for your first five invoices</div>
</section>
.fs-21 {
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  --page: #f4f2ef;
  --page: oklch(0.965 0.005 70);
  --card: #ffffff;
  --ink: #191817;
  --ink: oklch(0.22 0.004 60);
  --muted: #57534d;
  --muted: oklch(0.47 0.008 65);
  --rule: #e0dcd6;
  --rule: oklch(0.89 0.007 70);
  --accent: #e2604a;
  --accent: oklch(0.65 0.16 30);
  --screen: #faf8f5;
  --phone-w: 268px;
  --tilt: -4deg;
  --font-display: "Iowan Old Style", Georgia, "Times New Roman", serif;
  background: var(--page);
  color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

.fs-21 *,
.fs-21 *::before,
.fs-21 *::after {
  box-sizing: border-box;
}

.fs-21__hero {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  justify-content: space-between;
  align-items: baseline;
  padding: 20px 24px;
  border-block-end: 1px solid var(--rule);
}

.fs-21__wordmark {
  font: 400 20px/1 var(--font-display);
  letter-spacing: -0.01em;
}

.fs-21__heronav {
  font-size: 12.5px;
  color: var(--muted);
}

.fs-21__stage {
  display: grid;
  place-items: center;
  max-inline-size: 100%;
  padding: clamp(30px, 6vw, 68px) 20px;
}

.fs-21__split {
  inline-size: 100%;
  max-inline-size: 64rem;
  min-inline-size: 0;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(24px, 5vw, 56px);
  align-items: center;
}

.fs-21__copy {
  min-inline-size: 0;
}

.fs-21__eyebrow {
  margin: 0 0 14px;
  font: 700 10.5px/1 system-ui, sans-serif;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--accent);
}

.fs-21__heading {
  margin: 0 0 14px;
  max-inline-size: 24ch;
  font: 400 clamp(27px, 5vw, 42px)/1.08 var(--font-display);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.fs-21__intro {
  margin: 0 0 26px;
  max-inline-size: 46ch;
  font-size: 15px;
  line-height: 1.7;
  color: var(--muted);
  text-wrap: pretty;
}

.fs-21__callouts {
  margin: 0 0 28px;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 18px;
}

.fs-21__callout {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

.fs-21__icon {
  display: grid;
  place-items: center;
  inline-size: 38px;
  block-size: 38px;
  border-radius: 50%;
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, var(--page));
}

.fs-21__ctitle {
  margin: 0 0 5px;
  font: 600 15.5px/1.3 system-ui, sans-serif;
  letter-spacing: -0.01em;
}

.fs-21__cprose {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.fs-21__cta {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0 22px;
  font: 600 14px/1 system-ui, sans-serif;
  color: var(--card);
  background: var(--ink);
  border-radius: 999px;
  text-decoration: none;
}

.fs-21__cta:hover {
  background: var(--accent);
}

.fs-21__cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.fs-21__mockwrap {
  display: grid;
  place-items: center;
  min-inline-size: 0;
  transform: rotate(var(--tilt));
}

.fs-21__phone {
  position: relative;
  inline-size: var(--phone-w);
  max-inline-size: 100%;
  aspect-ratio: 9 / 18.5;
  padding: 11px;
  background: #1d1c1b;
  background: color-mix(in oklab, var(--ink) 92%, black);
  border-radius: 40px;
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.14);
  filter: drop-shadow(0 26px 30px rgba(25, 24, 23, 0.28)) drop-shadow(0 6px 10px rgba(25, 24, 23, 0.18));
}

.fs-21__notch {
  position: absolute;
  inset-block-start: 15px;
  inset-inline-start: 50%;
  translate: -50% 0;
  inline-size: 62px;
  block-size: 6px;
  border-radius: 99px;
  background: rgba(255, 255, 255, 0.22);
}

.fs-21__screen {
  block-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 26px 13px 13px;
  background: var(--screen);
  border-radius: 30px;
  overflow: hidden;
}

.fs-21__apphead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 11px;
  color: var(--muted);
}

.fs-21__apphead b {
  font: 400 15px/1 var(--font-display);
  color: var(--ink);
}

.fs-21__balance {
  padding: 13px;
  border-radius: 14px;
  background: var(--ink);
  color: var(--screen);
  display: grid;
  gap: 3px;
}

.fs-21__blabel {
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.7;
}

.fs-21__bvalue {
  font: 400 26px/1 var(--font-display);
  letter-spacing: -0.02em;
}

.fs-21__bmeta {
  font-size: 10px;
  opacity: 0.78;
}

.fs-21__txns {
  display: grid;
  gap: 6px;
}

.fs-21__txn {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 11px;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 10px;
}

.fs-21__txn b {
  font-size: 11.5px;
  font-weight: 600;
  min-inline-size: 0;
}

.fs-21__txn i {
  font-style: normal;
  font-size: 10px;
  color: var(--muted);
}

.fs-21__txn--late {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--rule));
  background: color-mix(in oklab, var(--accent) 8%, var(--card));
}

.fs-21__txn--late i {
  color: color-mix(in oklab, var(--accent) 80%, black);
}

.fs-21__tabs {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin-block-start: auto;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--rule);
}

.fs-21__tab {
  inline-size: 26px;
  block-size: 4px;
  border-radius: 99px;
  background: var(--rule);
}

.fs-21__tab[data-on] {
  background: var(--accent);
}

.fs-21__below {
  padding: 18px 24px;
  border-block-start: 1px solid var(--rule);
  font-size: 12.5px;
  color: var(--muted);
}

@media (max-width: 860px) {
  .fs-21__split {
    grid-template-columns: minmax(0, 1fr);
  }

  .fs-21 {
    --phone-w: 232px;
  }
}

@media (max-width: 640px) {
  .fs-21__mockwrap {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .fs-21__mockwrap {
    transform: none;
  }
}

@media (prefers-color-scheme: dark) {
  .fs-21 {
    --page: #131211;
    --card: #1c1a18;
    --ink: #f3f0eb;
    --muted: #a49c92;
    --rule: #2c2926;
    --accent: #ff8168;
    --screen: #201d1a;
  }

  .fs-21__phone {
    background: #0b0a09;
    box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.1);
    filter: drop-shadow(0 26px 34px rgba(0, 0, 0, 0.6));
  }

  .fs-21__balance {
    background: var(--accent);
    color: #1a1210;
  }

  .fs-21__cta {
    color: #131211;
    background: var(--ink);
  }
}

[data-theme="dark"] .fs-21 {
  --page: #131211;
  --card: #1c1a18;
  --ink: #f3f0eb;
  --muted: #a49c92;
  --rule: #2c2926;
  --accent: #ff8168;
  --screen: #201d1a;
}

[data-theme="dark"] .fs-21 .fs-21__cta {
  color: #131211;
  background: var(--ink);
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Feature Section 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: Feature Section with Floating Phone Mockup
Source: https://codefronts.com/layouts/css-feature-sections/css-feature-section-with-floating-mockup/

A split feature block with three callouts on the left and a phone drawn entirely in CSS on the right, tilted a few degrees and floating on a soft contact shadow. The frame, bezel, notch and app screen are all elements — no image, no 4MB PNG, no export cycle — so the screen content is real text that reflows and stays readable at any size.
## HTML
```html
<section class="fs-21" aria-labelledby="fs-21-heading">
  <div class="fs-21__hero" aria-hidden="true">
    <span class="fs-21__wordmark">Sundial</span>
    <span class="fs-21__heronav">For freelancers who invoice</span>
  </div>

  <div class="fs-21__stage">
    <div class="fs-21__split">
      <div class="fs-21__copy">
        <p class="fs-21__eyebrow">On the phone</p>
        <h2 class="fs-21__heading" id="fs-21-heading">Your books, closed before you leave the site</h2>
        <p class="fs-21__intro">Sundial turns the hour you spend on admin every evening into the ninety seconds it should be.</p>
        <ul class="fs-21__callouts">
          <li class="fs-21__callout">
            <span class="fs-21__icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 10h8M8 14h5"/></svg></span>
            <div><h3 class="fs-21__ctitle">Invoice from the job, not the desk</h3><p class="fs-21__cprose">Finish a job, tap once, and the invoice goes out with the hours and parts already filled in.</p></div>
          </li>
          <li class="fs-21__callout">
            <span class="fs-21__icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 3v18M7 8h10M7 16h10"/></svg></span>
            <div><h3 class="fs-21__ctitle">Chasing that you do not do</h3><p class="fs-21__cprose">Polite reminders at seven, fourteen and thirty days, stopping the moment the money lands.</p></div>
          </li>
          <li class="fs-21__callout">
            <span class="fs-21__icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M4 18V9M10 18V5M16 18v-6M3 21h18"/></svg></span>
            <div><h3 class="fs-21__ctitle">Tax set aside as you earn</h3><p class="fs-21__cprose">A running estimate with a pot that fills automatically, so January is a formality.</p></div>
          </li>
        </ul>
        <a class="fs-21__cta" href="#fs-21-heading">Try it on your next invoice</a>
      </div>

      <div class="fs-21__mockwrap" aria-hidden="true">
        <div class="fs-21__phone">
          <span class="fs-21__notch"></span>
          <div class="fs-21__screen">
            <div class="fs-21__apphead"><b>This month</b><span>August</span></div>
            <div class="fs-21__balance">
              <span class="fs-21__blabel">Paid</span>
              <span class="fs-21__bvalue">£8,420</span>
              <span class="fs-21__bmeta">£2,160 outstanding · 3 invoices</span>
            </div>
            <div class="fs-21__txns">
              <div class="fs-21__txn"><b>Fern Studio</b><i>£1,200 · paid</i></div>
              <div class="fs-21__txn"><b>Orbit Ltd</b><i>£860 · due in 4d</i></div>
              <div class="fs-21__txn fs-21__txn--late"><b>Vale &amp; Co</b><i>£1,300 · 12d late</i></div>
              <div class="fs-21__txn"><b>Basalt Group</b><i>£2,240 · paid</i></div>
            </div>
            <div class="fs-21__tabs"><span class="fs-21__tab" data-on></span><span class="fs-21__tab"></span><span class="fs-21__tab"></span></div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <div class="fs-21__below" aria-hidden="true">iOS and Android · Free for your first five invoices</div>
</section>
```
## CSS
```css
.fs-21 {
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  --page: #f4f2ef;
  --page: oklch(0.965 0.005 70);
  --card: #ffffff;
  --ink: #191817;
  --ink: oklch(0.22 0.004 60);
  --muted: #57534d;
  --muted: oklch(0.47 0.008 65);
  --rule: #e0dcd6;
  --rule: oklch(0.89 0.007 70);
  --accent: #e2604a;
  --accent: oklch(0.65 0.16 30);
  --screen: #faf8f5;
  --phone-w: 268px;
  --tilt: -4deg;
  --font-display: "Iowan Old Style", Georgia, "Times New Roman", serif;
  background: var(--page);
  color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

.fs-21 *,
.fs-21 *::before,
.fs-21 *::after {
  box-sizing: border-box;
}

.fs-21__hero {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  justify-content: space-between;
  align-items: baseline;
  padding: 20px 24px;
  border-block-end: 1px solid var(--rule);
}

.fs-21__wordmark {
  font: 400 20px/1 var(--font-display);
  letter-spacing: -0.01em;
}

.fs-21__heronav {
  font-size: 12.5px;
  color: var(--muted);
}

.fs-21__stage {
  display: grid;
  place-items: center;
  max-inline-size: 100%;
  padding: clamp(30px, 6vw, 68px) 20px;
}

.fs-21__split {
  inline-size: 100%;
  max-inline-size: 64rem;
  min-inline-size: 0;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(24px, 5vw, 56px);
  align-items: center;
}

.fs-21__copy {
  min-inline-size: 0;
}

.fs-21__eyebrow {
  margin: 0 0 14px;
  font: 700 10.5px/1 system-ui, sans-serif;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--accent);
}

.fs-21__heading {
  margin: 0 0 14px;
  max-inline-size: 24ch;
  font: 400 clamp(27px, 5vw, 42px)/1.08 var(--font-display);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.fs-21__intro {
  margin: 0 0 26px;
  max-inline-size: 46ch;
  font-size: 15px;
  line-height: 1.7;
  color: var(--muted);
  text-wrap: pretty;
}

.fs-21__callouts {
  margin: 0 0 28px;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 18px;
}

.fs-21__callout {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

.fs-21__icon {
  display: grid;
  place-items: center;
  inline-size: 38px;
  block-size: 38px;
  border-radius: 50%;
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, var(--page));
}

.fs-21__ctitle {
  margin: 0 0 5px;
  font: 600 15.5px/1.3 system-ui, sans-serif;
  letter-spacing: -0.01em;
}

.fs-21__cprose {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.fs-21__cta {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0 22px;
  font: 600 14px/1 system-ui, sans-serif;
  color: var(--card);
  background: var(--ink);
  border-radius: 999px;
  text-decoration: none;
}

.fs-21__cta:hover {
  background: var(--accent);
}

.fs-21__cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.fs-21__mockwrap {
  display: grid;
  place-items: center;
  min-inline-size: 0;
  transform: rotate(var(--tilt));
}

.fs-21__phone {
  position: relative;
  inline-size: var(--phone-w);
  max-inline-size: 100%;
  aspect-ratio: 9 / 18.5;
  padding: 11px;
  background: #1d1c1b;
  background: color-mix(in oklab, var(--ink) 92%, black);
  border-radius: 40px;
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.14);
  filter: drop-shadow(0 26px 30px rgba(25, 24, 23, 0.28)) drop-shadow(0 6px 10px rgba(25, 24, 23, 0.18));
}

.fs-21__notch {
  position: absolute;
  inset-block-start: 15px;
  inset-inline-start: 50%;
  translate: -50% 0;
  inline-size: 62px;
  block-size: 6px;
  border-radius: 99px;
  background: rgba(255, 255, 255, 0.22);
}

.fs-21__screen {
  block-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 26px 13px 13px;
  background: var(--screen);
  border-radius: 30px;
  overflow: hidden;
}

.fs-21__apphead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 11px;
  color: var(--muted);
}

.fs-21__apphead b {
  font: 400 15px/1 var(--font-display);
  color: var(--ink);
}

.fs-21__balance {
  padding: 13px;
  border-radius: 14px;
  background: var(--ink);
  color: var(--screen);
  display: grid;
  gap: 3px;
}

.fs-21__blabel {
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.7;
}

.fs-21__bvalue {
  font: 400 26px/1 var(--font-display);
  letter-spacing: -0.02em;
}

.fs-21__bmeta {
  font-size: 10px;
  opacity: 0.78;
}

.fs-21__txns {
  display: grid;
  gap: 6px;
}

.fs-21__txn {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 11px;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 10px;
}

.fs-21__txn b {
  font-size: 11.5px;
  font-weight: 600;
  min-inline-size: 0;
}

.fs-21__txn i {
  font-style: normal;
  font-size: 10px;
  color: var(--muted);
}

.fs-21__txn--late {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--rule));
  background: color-mix(in oklab, var(--accent) 8%, var(--card));
}

.fs-21__txn--late i {
  color: color-mix(in oklab, var(--accent) 80%, black);
}

.fs-21__tabs {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin-block-start: auto;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--rule);
}

.fs-21__tab {
  inline-size: 26px;
  block-size: 4px;
  border-radius: 99px;
  background: var(--rule);
}

.fs-21__tab[data-on] {
  background: var(--accent);
}

.fs-21__below {
  padding: 18px 24px;
  border-block-start: 1px solid var(--rule);
  font-size: 12.5px;
  color: var(--muted);
}

@media (max-width: 860px) {
  .fs-21__split {
    grid-template-columns: minmax(0, 1fr);
  }

  .fs-21 {
    --phone-w: 232px;
  }
}

@media (max-width: 640px) {
  .fs-21__mockwrap {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .fs-21__mockwrap {
    transform: none;
  }
}

@media (prefers-color-scheme: dark) {
  .fs-21 {
    --page: #131211;
    --card: #1c1a18;
    --ink: #f3f0eb;
    --muted: #a49c92;
    --rule: #2c2926;
    --accent: #ff8168;
    --screen: #201d1a;
  }

  .fs-21__phone {
    background: #0b0a09;
    box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.1);
    filter: drop-shadow(0 26px 34px rgba(0, 0, 0, 0.6));
  }

  .fs-21__balance {
    background: var(--accent);
    color: #1a1210;
  }

  .fs-21__cta {
    color: #131211;
    background: var(--ink);
  }
}

[data-theme="dark"] .fs-21 {
  --page: #131211;
  --card: #1c1a18;
  --ink: #f3f0eb;
  --muted: #a49c92;
  --rule: #2c2926;
  --accent: #ff8168;
  --screen: #201d1a;
}

[data-theme="dark"] .fs-21 .fs-21__cta {
  color: #131211;
  background: var(--ink);
}
```

How this works

The frame is nested boxes, and the shadow is a filter. An outer shell with a large border-radius, an inner screen inset by the bezel width, and a notch pill absolutely positioned at the top. The float uses filter: drop-shadow() rather than box-shadow, because drop-shadow follows the element's actual alpha silhouette — including the rounded corners — while a box-shadow on a rotated element still traces its rectangle and gives away the trick at the corners.

Draw the screen, do not screenshot it. A CSS-drawn app UI is a handful of elements, weighs nothing, retints with the theme, stays sharp on any display and never needs re-exporting when the product changes a label. It also stays readable: real text at 11-12px inside the frame beats a downscaled screenshot where the type is unreadable mush, which is the usual outcome of dropping a 1290px-wide capture into a 260px frame.

The tilt goes on the wrapper. Rotating the phone element itself rotates its text too, and rotated text renders noticeably softer. Instead rotate an outer wrapper by a few degrees and let the inner screen sit square within it — or accept the rotation only on the frame chrome and keep the screen at rotate(0). Small angles work best: three to five degrees reads as intentional, ten reads as a mistake.

The trap is a mockup that eats the mobile viewport. A 280px-wide phone frame inside a 320px viewport leaves no room for the copy that actually sells the feature. Below the fold point, drop the mockup with display: none — it is decorative here, the callouts carry the content — and give the text the full width. Never let the frame set a min-width that the page has to honour.

Make it yours

  • Change --phone-w to resize the whole device; every inner part is proportional.
  • Adjust --tilt between 0 and 6 degrees for more or less float.
  • Retint the app screen by editing --screen and --accent.
  • Replace the drawn screen with an <img> at the same aspect ratio if you have a real capture.
  • Swap the notch pill for a punch-hole circle by narrowing it and raising its radius.
  • Show the mockup on mobile instead by removing the display: none rule and shrinking --phone-w.

Gotchas — read before shipping

  • box-shadow on a rotated rounded frame still traces the element's rectangle at the corners; use filter: drop-shadow().
  • Rotating the element that contains the screen text makes that text render visibly softer — rotate a wrapper instead.
  • A fixed-width phone frame that stays visible at 320px squeezes the copy to nothing; hide the decorative mockup on narrow viewports.
  • A downscaled real screenshot inside a small frame is unreadable and adds hundreds of kilobytes — draw the UI instead.
  • Absolutely positioned notches and bezels drift when the frame is resized unless every inset is expressed in the same relative unit.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

oklch() dictates the floor (Chromium 111, Safari 15.4, Firefox 113), with sRGB hex declared first throughout. The phone frame is drawn with border-radius, aspect-ratio and filter: drop-shadow(), all Baseline well before that, so there are no image dependencies and no gates.

Techniques used in this demo

Search CodeFronts

Loading…