Frames, brackets and borders in interface design
A frame is the cheapest way to tell a reader where something starts and ends. A hairline around a card, brackets in the corners of an image, a thin rule inset from the edge of the browser window: each creates a place, and a place creates focus. Used with restraint, frames give a page the feel of an instrument or a printed plate. Used everywhere, they turn it into a spreadsheet. This is how to choose, build and test them, including one trap: the bracket technique most often shown online disappears for some users.
What a frame does
Four jobs, in order of importance:
- Separates. It says "this is one thing". A frame replaces whitespace where space is tight.
- Directs attention. A framed element looks more important than its neighbours. Frame what matters, not everything.
- Sets a register. Hairlines and brackets suggest precision: viewfinders, drawings, technical sheets. Rounded, filled cards suggest softness. The choice is part of a brand's voice as much as a font.
- Creates rhythm. Repeated frames of the same weight make a grid legible, and a single deviation draws the eye.
The question to ask before adding one: would whitespace or alignment do this job more quietly? If yes, use that.
The vocabulary
| Pattern | What it is | Best for | Watch out for |
|---|---|---|---|
| Hairline border | 1 px solid line in a low-contrast colour | Cards, inputs, dividers | Disappears on some screens if too faint |
| Corner brackets | Short L-shaped marks in each corner | Images, hero plates, feature cards | Look decorative; do not let them replace real affordances |
| Inset frame | A thin rule inset from the viewport or section edge | A page-wide "plate" feel | Eats space on phones; can sit over focus indicators |
| Double rule | Two lines with a gap | Headers, tables, sidebars | Visual noise if repeated |
| Notch or cutout | Border interrupted by a label | Fieldsets, sections with captions | Alignment and background matching |
| Chrome bar | A strip of thin lines and tiny labels along an edge | Technical or editorial identity | Tiny text must still meet contrast and size rules |
Pattern 1: hairline card
.card {
border: 1px solid var(--line);
border-radius: 12px;
padding: 1.25rem;
background: var(--surface);
}
.card:hover { border-color: color-mix(in oklch, var(--line), var(--accent) 50%); }
Two details. First, nested radii: when an image sits inside a padded card, the image's corner radius should be the card's radius minus the padding, floored at zero, or the two curves look uneven. This is a design convention, not a CSS rule. With a 12 px card radius and 8 px of padding the image takes 4 px; with 16 px of padding the formula gives zero, so the image corners stay square. Second, keep the line a step lighter than the surface behind it; on a dark ground that is a small lightness difference, and dark interfaces that stay readable explains when a border must reach 3:1.
Pattern 2: corner brackets on one pseudo-element
All eight strokes can be drawn by one pseudo-element using layered gradients, with no extra markup or images:
@property --len {
syntax: "<length>";
inherits: false;
initial-value: 14px;
}
.bracketed { position: relative; --w: 1px; --c: var(--accent); }
.bracketed::before {
content: "";
position: absolute;
inset: -1px; /* sit on the edge of the box */
pointer-events: none; /* never intercept clicks */
transition: --len 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
background:
linear-gradient(var(--c), var(--c)) top left / var(--len) var(--w) no-repeat,
linear-gradient(var(--c), var(--c)) top left / var(--w) var(--len) no-repeat,
linear-gradient(var(--c), var(--c)) top right / var(--len) var(--w) no-repeat,
linear-gradient(var(--c), var(--c)) top right / var(--w) var(--len) no-repeat,
linear-gradient(var(--c), var(--c)) bottom left / var(--len) var(--w) no-repeat,
linear-gradient(var(--c), var(--c)) bottom left / var(--w) var(--len) no-repeat,
linear-gradient(var(--c), var(--c)) bottom right / var(--len) var(--w) no-repeat,
linear-gradient(var(--c), var(--c)) bottom right / var(--w) var(--len) no-repeat;
}
.bracketed:hover::before { --len: 20px; }
Each layer is a one-colour gradient sized to a short bar and pinned to a corner. Registering --len with @property and a <length> syntax (MDN describes @property as defining a custom property with type checking, a default value and an inheritance setting) is what lets the browser interpolate it in a transition; an unregistered custom property would jump from 14 px to 20 px. Animating --len repaints the pseudo-element on each frame but does not move other boxes, which is why it is preferable to animating inset or width. The movement guidance in easing is a personality applies: a bracket that grows from 14 px to 20 px on hover in 160 ms with a firm ease-out is enough.
The forced-colors trap. MDN states that in forced-colors mode background-image is forced to none for values that are not url-based. Gradients are not url-based. Every bracket above vanishes for a user in Windows High Contrast and similar modes, and so does any frame drawn that way. If the brackets are only ornament, that is acceptable. If they mark the boundary of something a user needs to find, add a fallback built from properties that survive:
@media (forced-colors: active) {
.bracketed::before { display: none; }
.bracketed { outline: 1px solid CanvasText; outline-offset: 2px; }
}
MDN also advises that forced-colors should be used for small tweaks, not for a separate design, so keep the fallback this small.
Pattern 3: the viewport frame
A thin rule inset from the edge of the window makes a whole page feel like a single plate.
.viewport-frame {
position: fixed;
inset: clamp(8px, 1.5vw, 20px);
border: 1px solid var(--line);
pointer-events: none;
z-index: 40;
}
Rules for living with it:
- Reserve the space. Page content needs padding larger than the frame inset plus a gap, or text touches the line. Use one custom property for both.
- Respect the notch. On phones with safe areas, add
env(safe-area-inset-top)and its three siblings to the inset so the frame is not cut by rounded corners or sits under a home indicator. - Prefer
insetover viewport height units for the frame itself, and test it in a real mobile browser, since the layout and visual viewports can differ while the address bar moves. If you size something else against the viewport, MDN describes the small viewport units (svh) as stable and the dynamic ones (dvh) as able to resize content while the user scrolls, with a possible performance cost, so reach forsvhfirst. - Let it step back on phones. Reduce the inset to a few pixels or remove the frame below a narrow breakpoint; every pixel counts at 360 px wide.
Performance
A static frame is almost free. A 1 px border on a 1440 by 900 viewport with a 20 px inset paints about 4,520 pixels (two sides of 1,400 and two of 860), while the viewport has 1,296,000, about 287 times more. That is why a hairline is cheap and a full-viewport effect is not. Problems come from what you do to the frame:
- Animate
transformandopacitywhere you can, instead ofborder-width,insetor the blur radius of abox-shadow, which change layout or force repaints of larger areas. - Be careful combining
backdrop-filterormix-blend-modewith a fixed full-screen element. They make the browser process what lies behind the element, and MDN's backdrop-filter page is a reminder that the effect applies to everything behind it. Test on a mid-range phone before keeping it. - Do not paint a full-viewport shadow. A large blurred glow around the viewport touches a large share of those 1.3 million pixels on every change.
- Keep it out of the scroll path. A fixed element with
pointer-events: noneis easy to scroll under; one with filters and an animation is the version to profile.
Measure with the browser's performance tools instead of guessing; the method is in a performance budget for a site with WebGL.
Accessibility
Decoration is allowed to be decoration. Frames generated by CSS (borders, outlines, pseudo-elements) carry no content, so there is nothing for assistive technology to announce. If you add a dedicated element for a frame, give it aria-hidden="true". A frame should never contain meaning that is not also in text.
Contrast applies when the line carries information. WCAG 2.2 1.4.11 Non-text Contrast requires 3:1 against adjacent colours for visual information needed to identify a component and its states. The Understanding document says a boundary is required only when there is no other visual way to identify that the control is present, such as a text input whose border is the only cue. A decorative hairline around a section has no requirement, but check each one: if removing it would leave a user unsure where a control is, it is not decoration.
Fixed frames must not hide focus. WCAG 2.2 2.4.11 Focus Not Obscured (Minimum) says that when a component receives keyboard focus, it is not entirely hidden by author-created content. The Understanding document names sticky headers, sticky footers and non-modal dialogs as typical culprits, and lists scroll padding as a passing technique. A one-pixel frame cannot hide a control, but a wide opaque band built in the same position can. Set scroll-padding equal to the space those elements occupy, and tab through the page.
Focus indicators and frames are separate systems. Do not use the bracket style as the only focus cue. WCAG 2.4.7 requires a visible keyboard focus indicator, and 1.4.11 gives that indicator the same 3:1 requirement. Give interactive elements a normal outline with outline-offset under :focus-visible. MDN notes that outlines take no space in the layout and follow border-radius, which suits rounded cards.
Forced colors. MDN lists border-color and outline-color among the colours the browser replaces with system colours, so authored border colours do not need a forced-colors override: a real border stays visible. box-shadow is forced to none, so a card whose only edge is a shadow loses its edge. Draw it with a real border instead.
Responsive behaviour
- Set bracket length with
clamp()so it is never larger than a fraction of the card on a phone. - Tiny labels in a "chrome bar" must stay readable. Text under about 12 px rarely survives zoom and glare; hide the label at narrow widths instead of shrinking it.
- Touch targets inside framed regions still need adequate size and spacing. WCAG 2.2 criterion 2.5.8 sets a 24 by 24 CSS pixel minimum for pointer targets, with exceptions that include sufficient spacing between undersized targets, and a frame that crowds buttons works against it.
A test routine
- Turn on a forced-colors or high-contrast mode in the operating system and look at every framed element. Anything drawn only with gradients or shadows should be either ornament or have a fallback.
- Press Tab from the top of the page and watch the focus indicator pass under any fixed or sticky element.
- Narrow the window to 360 px and check that the frame's inset, the content padding and the safe-area insets still leave the text room.
- Open the performance panel, scroll the longest page for five seconds with the frame present, then repeat with it hidden and compare frame times.
- Zoom to 200 percent and confirm the labels and brackets scale without covering text.
Checklist
- Ask whether whitespace could do the job before adding a frame.
- One border weight and one bracket style per site; vary colour and length for emphasis, not construction.
- Build brackets from one pseudo-element with
pointer-events: none, register the animated length, and give them a forced-colors fallback if they matter. - Give fixed frames safe-area insets, reserved padding and a phone breakpoint.
- Check 3:1 where a line identifies a control; use real borders for anything that must survive forced colors.
- Tab through the page and confirm no frame, header or bar covers the focused element.
Questions and answers
Do decorative borders have to meet a contrast ratio?
Not if they are purely decorative. WCAG 2.2 criterion 1.4.11 applies to visual information needed to identify a component or understand a graphic. A hairline that merely decorates a section is exempt; a border that is the only sign an input exists is not.
Are CSS corner brackets expensive to render?
A static set of gradient layers on a pseudo-element is a small paint job. Cost appears when you animate properties that force layout or repaint, paint large blurred shadows, or put filters or blend modes on a full-screen fixed layer. Measure in the browser's performance tools.
Does a fixed frame around the viewport hurt mobile?
It can, by taking space from a small screen and by overlapping content or focused controls. Reduce the inset on phones, include the safe-area insets, and reserve the frame's space in the content padding.
What happens to frames in forced-colors mode?
MDN lists border-color and outline-color among the colours the browser overrides with system colours, box-shadow as forced to none, and background-image as forced to none unless it is a url. Real borders and outlines stay visible; shadows and gradient-drawn marks do not.
Sources
- MDN: outline developer.mozilla.org
- MDN: forced-colors media feature developer.mozilla.org
- WCAG 2.2: Understanding Non-text Contrast w3.org
- WCAG 2.2: Understanding Focus Not Obscured (Minimum) w3.org
- WCAG 2.2 (W3C Recommendation) w3.org