I'm trying to create something that looks like a mobile phone with HTML and CSS and I'd like the camera to have something like an "inverted border-radius" that connects it with the frame smoothly.
I can't just make it bigger and mask the unwanted area with a pseudoelement with a white background because the screen content might not always be white.
Also, I can't use mask-image
on that same element because that "inverted border-radius" would actually extend past it's bounding box, so I would actually be adding more area rather than subtracting (plus support is really low).
I want to avoid using SVGs if possible.
body {
position: relative;
overflow: hidden;
height: 100vh;
margin: 0;
}
.phone {
width: 420px;
height: 800px;
padding: 12px 12px 24px;
position: absolute;
top: 32px;
left: 50%;
transform: translate(-50%, 0);
background: #000;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, .125);
border-radius: 16px;
}
.screen {
height: 100%;
overflow: hidden;
position: relative;
background: #FFF;
border-radius: 8px;
}
.viewport {
height: 100%;
position: relative;
overflow-x: hidden;
overflow-y: scroll;
}
.notch {
top: 12px;
left: 50%;
width: 24px;
height: 12px;
z-index: 10;
position: absolute;
transform: translate(-50%, 0);
background: #000;
border-bottom-left-radius: 1024px;
border-bottom-right-radius: 1024px;
}
.camera {
top: 0;
left: 50%;
width: 12px;
border: 4px solid #33244A;
height: 12px;
position: absolute;
transform: translate(-50%, -50%);
background: #304A58;
border-radius: 1024px;
box-sizing: border-box;
}
<div class="phone">
<div class="notch">
<div class="camera"></div>
</div>
<div class="screen">
<div class="viewport"></div>
</div>
</div>
There are four ways to do that, from simple to more complex:
Adding 2 pseudoelements with radial-gradient
.
Simplest and well-supported solution. Probably the one I would use.
Adding 2 pseudoelements with mask-image
(same as above, but with worse support).
Quite similar, code-wise, to the previews one, but with really bad support (needs browser prefixes for those that support it).
Adding 2 pseudoelements with a border-radius
, box-shadow
and background: transparent
.
Needs a bit more code, but it looks a bit smoother, at least on Chrome Version 78.0.3904.108
, so maybe it's worth it for you, although the difference is minimal. In any case, the shapes you can do can't be as complex as with the previous alternatives, especially if you want to work with ellipses rather than circles, like in this other question: https://stackoverflow.com/a/59278227/3723993.
Using an SVG.
I think the SVG solution is not worth it here, but it would be a good alternative for more complex shapes or animated/transitioning shapes.
Here you can check the first 3 solutions:
const notch = document.getElementById('notch');
const button = document.getElementById('button');
const xrayCheckbox = document.getElementById('xrayCheckbox');
const xrayLabel = document.getElementById('xrayLabel');
const label = document.getElementById('label');
const solutions = [{
name: 'pseudoelements + radial-gradient',
classes: 'notch notch-gradient'
}, {
name: 'pseudoelements + box-shadow',
classes: 'notch notch-shadow'
}, {
name: 'pseudoelements + mask-image',
classes: 'notch notch-mask'
}];
let currentSolutionIndex = 0;
let currentSolution = solutions[currentSolutionIndex];
let xRayEnabled = false;
button.onclick = () => {
currentSolutionIndex = (currentSolutionIndex + 1) % solutions.length;
currentSolution = solutions[currentSolutionIndex];
updateLabels();
};
xrayCheckbox.onchange = () => {
xRayEnabled = xrayCheckbox.checked;
updateLabels();
};
function updateLabels() {
if (xRayEnabled) {
notch.className = `${ currentSolution.classes }-xray`;
label.innerText = `${ currentSolution.name } (X-Ray)`;
xrayLabel.innerText = 'Disable X-Ray';
} else {
notch.className = currentSolution.classes;
label.innerText = currentSolution.name;
xrayLabel.innerText = 'Enable X-Ray';
}
}
body {
position: relative;
overflow: hidden;
height: 100vh;
margin: 0;
}
.phone {
width: 420px;
height: 800px;
padding: 12px 12px 24px;
position: absolute;
top: 32px;
left: 50%;
transform: translate(-50%, 0);
background: #000;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, .5);
border-radius: 16px;
}
.screen {
height: 100%;
overflow: hidden;
position: relative;
background: #FFF;
border-radius: 8px;
}
.viewport {
height: 100%;
position: relative;
overflow-x: hidden;
overflow-y: scroll;
}
.notch {
top: 12px;
left: 50%;
width: 24px;
height: 12px;
z-index: 10;
position: absolute;
transform: translate(-50%, 0);
background: #000;
border-bottom-left-radius: 1024px;
border-bottom-right-radius: 1024px;
}
.notch::before,
.notch::after {
top: 0;
width: 8px;
height: 8px;
content: "";
position: absolute;
}
.notch-gradient-xray,
.notch-shadow-xray,
.notch-mask-xray {
background: red;
}
/* RADIAL GRADIENT SOLUTION */
.notch-gradient::before {
left: -6px;
background: radial-gradient(circle at bottom left, transparent 0, transparent 70%, black 70%, black 100%);
}
.notch-gradient::after {
right: -6px;
background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
}
.notch-gradient-xray::before {
left: -6px;
background: green radial-gradient(circle at bottom left, transparent 0, transparent 70%, cyan 70%, cyan 100%);
}
.notch-gradient-xray::after {
right: -6px;
background: green radial-gradient(circle at bottom right, transparent 0, transparent 70%, cyan 70%, cyan 100%);
}
/* BOX-SHADOW SOLUTION */
.notch-shadow::before {
left: -6px;
background: transparent;
border-radius: 0 8px 0 0;
box-shadow: 0 -4px 0 0 #000;
}
.notch-shadow::after {
right: -6px;
background: transparent;
border-radius: 8px 0 0 0;
box-shadow: 0 -4px 0 0 #000;
}
.notch-shadow-xray::before {
left: -6px;
background: green;
border-radius: 0 8px 0 0;
box-shadow: 0 -4px 0 0 cyan;
}
.notch-shadow-xray::after {
right: -6px;
background: green;
border-radius: 8px 0 0 0;
box-shadow: 0 -4px 0 0 cyan;
}
/* MASK SOLUTION */
.notch-mask::before {
left: -6px;
background: #000;
-webkit-mask-image: radial-gradient(circle at bottom left, transparent 0, transparent 70%, black 70%, black 100%);
}
.notch-mask::after {
right: -6px;
background: #000;
-webkit-mask-image: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
}
.notch-mask-xray::before {
left: -6px;
background: cyan;
-webkit-mask-image: radial-gradient(circle at bottom left, transparent 0, transparent 70%, black 70%, black 100%);
}
.notch-mask-xray::after {
right: -6px;
background: cyan;
-webkit-mask-image: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
}
.camera {
top: 0;
left: 50%;
width: 12px;
border: 4px solid #33244A;
height: 12px;
position: absolute;
transform: translate(-50%, -50%);
background: #304A58;
border-radius: 1024px;
box-sizing: border-box;
}
#button {
font-family: monospace;
font-size: 16px;
padding: 8px 16px;
margin: 32px auto 16px;
background: transparent;
border: 2px solid black;
display: block;
border-radius: 2px;
}
#xray {
font-family: monospace;
font-size: 16px;
padding: 0 16px;
text-align: center;
display: block;
margin: 0 0 16px;
display: flex;
align-items: center;
justify-content: center;
}
#xrayCheckbox {
margin: 0 8px 0 0;
}
#label {
font-family: monospace;
font-size: 16px;
padding: 0 16px;
text-align: center;
}
<div class="phone">
<div id="notch" class="notch notch-gradient">
<div class="camera"></div>
</div>
<div class="screen">
<div class="viewport">
<button id="button">Change Solution</button>
<label id="xray">
<input id="xrayCheckbox" type="checkbox" />
<span id="xrayLabel">Enable X-Ray</span>
</label>
<div id="label">pseudoelements + radial-gradient</div>
</div>
</div>
</div>