inner-radius
v0.1.0
Published
Framework-agnostic concentric inner border-radius: max(0, parentRadius - parentPadding). Core + React, Vue, and CSS helpers.
Maintainers
Readme
inner-radius
Compute a child element's border-radius from its parent's radius and padding — the concentric corners rule used by design systems (iOS, Figma):
innerRadius = max(0, parentRadius − parentPadding)When a box sits inside a rounded container, its corners only look right if its radius shrinks by the padding. Eyeballing it breaks the moment either value changes. This package computes it for you.
- Framework-agnostic core — a pure function, zero dependencies.
- Adapters for React, Vue, and CSS, behind separate import paths.
- Optional peer deps —
react/vueload only if you import their adapter. - Ships ESM + CJS + TypeScript types, fully tree-shakeable.
Table of contents
Install
npm install inner-radius
# or
pnpm add inner-radius
# or
yarn add inner-radiusreact and vue are optional peer dependencies. You only need the one
whose adapter you import; the core and CSS helper need neither.
Quick start
import { getInnerRadius } from 'inner-radius';
getInnerRadius(16, 8); // 8 → max(0, 16 - 8)
getInnerRadius(8, 16); // 0 → clamped, never negative// React
import { useInnerRadius } from 'inner-radius/react';
const radius = useInnerRadius(16, 8); // 8, memoizedWhy this exists
A child placed inside a rounded parent with padding needs a smaller radius to stay concentric:
┌───────────────────────────┐ ← parent: radius 16, padding 8
│ ┌───────────────────┐ │
│ │ │ │ ← child: radius must be 16 − 8 = 8
│ │ child │ │
│ │ │ │
│ └───────────────────┘ │
└───────────────────────────┘Use the parent's radius directly on the child and the corners look too round;
omit it and they look too sharp. The formula is max(0, radius − padding) — the
max keeps the child from ever going negative when padding exceeds radius.
API reference
getInnerRadius(radius, padding, options?)
The core function. Returns the inner radius as a number.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| radius | number | — | Parent (outer) border-radius. |
| padding | number | — | Gap between the parent edge and the child edge. |
| options.min | number | 0 | Floor — the result never drops below this. |
Returns number — max(min, radius − padding).
getInnerRadius(16, 8); // 8
getInnerRadius(8, 16); // 0 (clamped to default min)
getInnerRadius(8, 16, { min: 4 }); // 4 (custom floor)
getInnerRadius(NaN, 8); // 0 (non-finite input → min)Non-finite inputs (
NaN,Infinity) returnmininstead of producingNaN, so a missing measurement degrades gracefully.
getInnerRadii(radius, padding?, options?)
Per-corner variant. Each corner subtracts its own padding — handy when a container has different padding per side, or you want independent corners.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| radius | number | — | Parent border-radius applied to every corner. |
| padding | number \| Partial<Corners> | 0 | Uniform padding, or per-corner padding. Omitted corners use 0. |
| options.min | number | 0 | Floor passed through to each corner. |
Returns Corners — { topLeft, topRight, bottomRight, bottomLeft }.
getInnerRadii(16, 8);
// { topLeft: 8, topRight: 8, bottomRight: 8, bottomLeft: 8 }
getInnerRadii(16, { topLeft: 8, bottomRight: 4 });
// { topLeft: 8, topRight: 16, bottomRight: 12, bottomLeft: 16 }The returned object maps directly onto the CSS border-radius longhands
(border-top-left-radius, etc.).
innerRadiusCss(radius, padding, min?)
Builds a CSS expression string instead of a number — for stylesheets, inline styles, or CSS-variable-driven layouts where you never see the pixel value in JS.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| radius | number \| string | — | Number → px; string passes through. |
| padding | number \| string | — | Number → px; string passes through. |
| min | number \| string | 0 | Floor; number → px, string passes through. |
Returns string — max(<min>, calc(<radius> − <padding>)).
innerRadiusCss(16, 8); // "max(0px, calc(16px - 8px))"
innerRadiusCss('1rem', '0.5rem'); // "max(0px, calc(1rem - 0.5rem))"
innerRadiusCss('var(--r)', 'var(--p)');
// "max(0px, calc(var(--r) - var(--p)))"Because strings pass through untouched, you can drive it entirely from CSS custom properties and let the browser do the math.
useInnerRadius(radius, padding, options?) — React
Import from inner-radius/react. Wraps getInnerRadius
in useMemo, recomputing only when radius, padding, or options.min
change.
| Param | Type | Description |
| --- | --- | --- |
| radius | number | Parent border-radius. |
| padding | number | Padding. |
| options.min | number | Floor (default 0). |
Returns number.
import { useInnerRadius } from 'inner-radius/react';
const inner = useInnerRadius(16, 8); // 8Requires react >= 16.8 (hooks).
useInnerRadius(radius, padding, options?) — Vue
Import from inner-radius/vue. Accepts refs, getters, or plain values
(MaybeRefOrGetter) and returns a ComputedRef<number> that tracks them.
| Param | Type | Description |
| --- | --- | --- |
| radius | MaybeRefOrGetter<number> | Parent border-radius. |
| padding | MaybeRefOrGetter<number> | Padding. |
| options.min | number | Floor (default 0). |
Returns ComputedRef<number>.
import { ref } from 'vue';
import { useInnerRadius } from 'inner-radius/vue';
const radius = ref(16);
const inner = useInnerRadius(radius, 8); // ComputedRef<number> → 8Requires vue >= 3.3 (for toValue).
Types
interface InnerRadiusOptions {
/** Floor for the result, default 0 — radius can't go negative. */
min?: number;
}
type Corners<T = number> = {
topLeft: T;
topRight: T;
bottomRight: T;
bottomLeft: T;
};Both are exported from the package root (inner-radius).
Examples
Plain JS / TS
import { getInnerRadius } from 'inner-radius';
const card = document.querySelector<HTMLElement>('.card')!;
const inner = document.querySelector<HTMLElement>('.card-image')!;
const radius = parseFloat(getComputedStyle(card).borderRadius);
const padding = parseFloat(getComputedStyle(card).padding);
inner.style.borderRadius = `${getInnerRadius(radius, padding)}px`;React
import { useInnerRadius } from 'inner-radius/react';
function Card({ radius = 16, padding = 8 }: { radius?: number; padding?: number }) {
const inner = useInnerRadius(radius, padding);
return (
<div style={{ borderRadius: radius, padding }} className="card">
<img style={{ borderRadius: inner }} src="/cover.jpg" alt="" />
</div>
);
}Vue
<script setup lang="ts">
import { ref } from 'vue';
import { useInnerRadius } from 'inner-radius/vue';
const radius = ref(16);
const padding = ref(8);
const inner = useInnerRadius(radius, padding); // ComputedRef<number>
</script>
<template>
<div class="card" :style="{ borderRadius: radius + 'px', padding: padding + 'px' }">
<img :style="{ borderRadius: inner + 'px' }" src="/cover.jpg" alt="" />
</div>
</template>CSS / inline styles
Drive everything from custom properties — no pixel value ever touches JS:
import { innerRadiusCss } from 'inner-radius/css';
element.style.borderRadius = innerRadiusCss('var(--card-radius)', 'var(--card-padding)');
// → "max(0px, calc(var(--card-radius) - var(--card-padding)))".card {
--card-radius: 16px;
--card-padding: 8px;
border-radius: var(--card-radius);
padding: var(--card-padding);
}
.card > .inner {
/* the string innerRadiusCss() produces, written by hand */
border-radius: max(0px, calc(var(--card-radius) - var(--card-padding)));
}Per-corner radii
import { getInnerRadii } from 'inner-radius';
const r = getInnerRadii(16, { topLeft: 8, topRight: 8 });
// { topLeft: 8, topRight: 8, bottomRight: 16, bottomLeft: 16 }
element.style.borderTopLeftRadius = `${r.topLeft}px`;
element.style.borderTopRightRadius = `${r.topRight}px`;
element.style.borderBottomRightRadius = `${r.bottomRight}px`;
element.style.borderBottomLeftRadius = `${r.bottomLeft}px`;Entry points
| Import path | Exports | Peer dep |
| --- | --- | --- |
| inner-radius | getInnerRadius, getInnerRadii, innerRadiusCss, InnerRadiusOptions, Corners | none |
| inner-radius/css | innerRadiusCss | none |
| inner-radius/react | useInnerRadius | react >= 16.8 |
| inner-radius/vue | useInnerRadius | vue >= 3.3 |
Each path is a separate bundle, so importing the React adapter never pulls in Vue, and vice versa.
FAQ
What unit are the numbers? Whatever you treat them as — the core is
unitless. Most callers use pixels. innerRadiusCss appends px to bare
numbers but leaves strings (rem, %, var(...)) alone.
What if padding is larger than radius? The result clamps to min (default
0). A box can't have a negative radius.
Does it handle different padding per side? Yes — use
getInnerRadii with a per-corner
padding object.
Is there a Svelte / Solid / Angular adapter? Not yet. The core works in any
of them today (getInnerRadius is a plain function); framework adapters can be
added as new entry points without touching existing code.
