Everything broken...
This commit is contained in:
@@ -0,0 +1,306 @@
|
|||||||
|
# Tailwind Dashed Border Plugin: Technical Description
|
||||||
|
|
||||||
|
This plugin provides a small set of Tailwind CSS utilities for drawing configurable dashed borders with an SVG `<rect>` overlay instead of using the native CSS `border-style: dashed`. The SVG approach makes dash length, gap, offset, stroke width, color, and rounded corners independently controllable.
|
||||||
|
|
||||||
|
## What the plugin does
|
||||||
|
|
||||||
|
The plugin defines:
|
||||||
|
|
||||||
|
- Base CSS custom properties for dashed-border configuration.
|
||||||
|
- Structural utilities for the container, SVG overlay, and SVG rectangle.
|
||||||
|
- Generated utility families for gap, dash length, offset, stroke width, stroke color, and border radius.
|
||||||
|
- Transition helpers so dashed-border visuals can animate consistently with Tailwind transition utilities.
|
||||||
|
|
||||||
|
The plugin expects the dashed border to be rendered by an element structure like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border dashed-border-gap-2 dashed-border-length-4 dashed-border-width-2">
|
||||||
|
<svg class="dashed-border-svg" aria-hidden="true">
|
||||||
|
<rect class="dashed-border-rect" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core structural utilities
|
||||||
|
|
||||||
|
### `.dashed-border`
|
||||||
|
|
||||||
|
This is the root utility for any element using the plugin. It:
|
||||||
|
|
||||||
|
- Sets default CSS variables.
|
||||||
|
- Applies `position: relative` so the SVG overlay can be absolutely positioned.
|
||||||
|
|
||||||
|
Default values:
|
||||||
|
|
||||||
|
- `--tw-dashed-border-gap`: `calc(var(--spacing, 0.25rem) * 2)`
|
||||||
|
- `--tw-dashed-border-length`: `calc(var(--spacing, 0.25rem) * 3)`
|
||||||
|
- `--tw-dashed-border-offset`: `0px`
|
||||||
|
- `--tw-dashed-border-width`: `1px`
|
||||||
|
- `--tw-dashed-border-color`: `currentColor`
|
||||||
|
- `--tw-dashed-border-radius`: `0px`
|
||||||
|
|
||||||
|
### `.dashed-border-svg`
|
||||||
|
|
||||||
|
This utility is intended for the SVG overlay element. It:
|
||||||
|
|
||||||
|
- Makes the SVG fill the container with `position: absolute` and `inset: 0`.
|
||||||
|
- Sets `width: 100%` and `height: 100%`.
|
||||||
|
- Uses `overflow: hidden`.
|
||||||
|
- Uses `pointer-events: none` so the overlay does not block interaction.
|
||||||
|
|
||||||
|
### `.dashed-border-rect`
|
||||||
|
|
||||||
|
This utility is intended for the SVG `<rect>` that draws the actual border. It:
|
||||||
|
|
||||||
|
- Removes fill with `fill: none`.
|
||||||
|
- Uses `stroke` for the border color.
|
||||||
|
- Uses `stroke-dasharray` for dash length and gap.
|
||||||
|
- Uses `stroke-dashoffset` for dash phase shifting.
|
||||||
|
- Uses `vector-effect: non-scaling-stroke` to keep stroke width stable.
|
||||||
|
- Positions the rectangle inset by half the stroke width, so the stroke remains visually inside the element bounds.
|
||||||
|
- Uses `rx` to simulate rounded corners.
|
||||||
|
|
||||||
|
Computed shape rules:
|
||||||
|
|
||||||
|
- `x` / `y`: `calc(var(--tw-dashed-border-width) / 2)`
|
||||||
|
- `width` / `height`: `calc(100% - var(--tw-dashed-border-width))`
|
||||||
|
- `rx`: `max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))`
|
||||||
|
|
||||||
|
Dash pattern:
|
||||||
|
|
||||||
|
```css
|
||||||
|
stroke-dasharray: <length> <gap>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registered custom properties
|
||||||
|
|
||||||
|
The plugin registers these `@property` definitions:
|
||||||
|
|
||||||
|
- `--tw-dashed-border-gap`
|
||||||
|
- `--tw-dashed-border-length`
|
||||||
|
- `--tw-dashed-border-offset`
|
||||||
|
- `--tw-dashed-border-width`
|
||||||
|
- `--tw-dashed-border-radius`
|
||||||
|
|
||||||
|
These registrations provide typed CSS custom properties so transitions and interpolation behave more predictably in supporting browsers.
|
||||||
|
|
||||||
|
## Generated utility families
|
||||||
|
|
||||||
|
### Gap utilities
|
||||||
|
|
||||||
|
Class shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border-gap-*
|
||||||
|
```
|
||||||
|
|
||||||
|
Source values:
|
||||||
|
|
||||||
|
- Tailwind `theme.spacing`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Sets `--tw-dashed-border-gap`
|
||||||
|
- Recomputes `stroke-dasharray`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border dashed-border-gap-4">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dash length utilities
|
||||||
|
|
||||||
|
Class shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border-length-*
|
||||||
|
```
|
||||||
|
|
||||||
|
Source values:
|
||||||
|
|
||||||
|
- Tailwind `theme.spacing`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Sets `--tw-dashed-border-length`
|
||||||
|
- Recomputes `stroke-dasharray`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border dashed-border-length-6">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dash offset utilities
|
||||||
|
|
||||||
|
Class shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border-offset-*
|
||||||
|
```
|
||||||
|
|
||||||
|
Source values:
|
||||||
|
|
||||||
|
- Tailwind `theme.spacing`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Sets `--tw-dashed-border-offset`
|
||||||
|
- Updates `stroke-dashoffset`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border dashed-border-offset-2">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stroke width utilities
|
||||||
|
|
||||||
|
Class shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border-width-*
|
||||||
|
```
|
||||||
|
|
||||||
|
Source values:
|
||||||
|
|
||||||
|
- Tailwind `theme.borderWidth`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Sets `--tw-dashed-border-width`
|
||||||
|
- Updates `stroke-width`
|
||||||
|
- Recomputes `x`, `y`, `width`, `height`, and `rx`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border dashed-border-width-2">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stroke color utilities
|
||||||
|
|
||||||
|
Class shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border-color-*
|
||||||
|
```
|
||||||
|
|
||||||
|
Source values:
|
||||||
|
|
||||||
|
- Flattened Tailwind `theme.colors`
|
||||||
|
- Plus explicit aliases:
|
||||||
|
- `dashed-border-color-current`
|
||||||
|
- `dashed-border-color-inherit`
|
||||||
|
- `dashed-border-color-transparent`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Sets `--tw-dashed-border-color`
|
||||||
|
- Updates SVG `stroke`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border dashed-border-color-slate-500">
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested Tailwind colors are flattened with dash-separated keys, so a theme color like `slate.500` becomes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border-color-slate-500
|
||||||
|
```
|
||||||
|
|
||||||
|
### Radius utilities
|
||||||
|
|
||||||
|
Class shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dashed-border.rounded-*
|
||||||
|
```
|
||||||
|
|
||||||
|
These are not generated as `dashed-border-radius-*`. Instead, the plugin creates compound selectors that combine `.dashed-border` with Tailwind rounded classes.
|
||||||
|
|
||||||
|
Examples of generated selectors:
|
||||||
|
|
||||||
|
- `.dashed-border.rounded`
|
||||||
|
- `.dashed-border.rounded-sm`
|
||||||
|
- `.dashed-border.rounded-lg`
|
||||||
|
- `.dashed-border.rounded-full`
|
||||||
|
|
||||||
|
Source values:
|
||||||
|
|
||||||
|
- Tailwind `theme.borderRadius`
|
||||||
|
|
||||||
|
Effect:
|
||||||
|
|
||||||
|
- Sets `--tw-dashed-border-radius`
|
||||||
|
- Lets the SVG rectangle inherit matching rounded-corner behavior through its computed `rx`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border rounded-lg">
|
||||||
|
```
|
||||||
|
|
||||||
|
This means the expected way to apply radius is:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border rounded-xl">
|
||||||
|
```
|
||||||
|
|
||||||
|
not:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border-radius-xl">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transition behavior
|
||||||
|
|
||||||
|
The plugin adds transition helpers for dashed-border visuals.
|
||||||
|
|
||||||
|
Supported patterns:
|
||||||
|
|
||||||
|
- `.dashed-border-transition`
|
||||||
|
- `.dashed-border.transition`
|
||||||
|
- `.dashed-border.transition-all`
|
||||||
|
- `.dashed-border.transition-colors`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- Inherits Tailwind-style transition duration and timing defaults.
|
||||||
|
- Applies transitions to the inner `.dashed-border-rect`.
|
||||||
|
- `transition` / `transition-all` animate:
|
||||||
|
- `stroke-dasharray`
|
||||||
|
- `stroke-dashoffset`
|
||||||
|
- `stroke-width`
|
||||||
|
- `stroke`
|
||||||
|
- `rx`
|
||||||
|
- `transition-colors` animates only `stroke`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashed-border rounded-lg transition-all dashed-border-color-slate-400 hover:dashed-border-color-slate-900">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected visual model
|
||||||
|
|
||||||
|
When used correctly, the utilities should produce:
|
||||||
|
|
||||||
|
- A container element that establishes the positioning context.
|
||||||
|
- A full-size SVG overlay sitting on top of the container.
|
||||||
|
- A `<rect>` inset by half the stroke width so the line is not clipped.
|
||||||
|
- A dashed stroke whose dash length and gap are independently tunable.
|
||||||
|
- Rounded corners that visually track Tailwind `rounded-*` utilities.
|
||||||
|
- Hover, or others transition triggers should be embedded from main tailwind flow. Use their duration and animation function.
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
- The plugin flattens nested color objects from `theme.colors` into dash-separated utility suffixes.
|
||||||
|
- `DEFAULT` keys are handled specially so default theme values map cleanly to `rounded` and other default-style selectors.
|
||||||
|
- Radius is adjusted by subtracting half the stroke width, which keeps the visible curve aligned with the outer element radius.
|
||||||
|
- The plugin is built around Tailwind CSS v4 plugin APIs: `addBase`, `addUtilities`, and `matchUtilities`.
|
||||||
+46
-29
@@ -11,11 +11,14 @@ const DEFAULT_BORDER_WIDTH = "1px";
|
|||||||
const DEFAULT_OFFSET = "0px";
|
const DEFAULT_OFFSET = "0px";
|
||||||
const DEFAULT_COLOR = "currentColor";
|
const DEFAULT_COLOR = "currentColor";
|
||||||
const DEFAULT_RADIUS = "0px";
|
const DEFAULT_RADIUS = "0px";
|
||||||
const DEFAULT_ALIGN = "calc(var(--tw-dashed-border-width) / 2)";
|
const svgRadius =
|
||||||
const dashedBorderAlign = "var(--tw-dashed-border-align)";
|
"max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))";
|
||||||
const svgRadius = `max(0px, calc(var(--tw-dashed-border-radius) - (${dashedBorderAlign})))`;
|
|
||||||
const dashedBorderTransitionProperties =
|
const dashedBorderTransitionProperties =
|
||||||
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx, x, y, width, height";
|
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx";
|
||||||
|
const strokeDasharrayValue = (
|
||||||
|
length: string,
|
||||||
|
gap: string,
|
||||||
|
): string => `${length} max(${gap}, 0.001px)`;
|
||||||
|
|
||||||
const flattenThemeMap = (
|
const flattenThemeMap = (
|
||||||
input: ThemeValueMap,
|
input: ThemeValueMap,
|
||||||
@@ -40,11 +43,6 @@ const flattenThemeMap = (
|
|||||||
const roundedSelector = (name: string): string =>
|
const roundedSelector = (name: string): string =>
|
||||||
name === "DEFAULT" ? ".rounded" : `.rounded-${name}`;
|
name === "DEFAULT" ? ".rounded" : `.rounded-${name}`;
|
||||||
|
|
||||||
const resolveAlignValue = (value: string): string =>
|
|
||||||
/^-?(?:\d+|\d*\.\d+)$/.test(value)
|
|
||||||
? `calc((0.5 - ${value}) * var(--tw-dashed-border-width))`
|
|
||||||
: `calc((var(--tw-dashed-border-width) / 2) - (${value}))`;
|
|
||||||
|
|
||||||
const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||||
({ addBase, addUtilities, matchUtilities, theme }) => {
|
({ addBase, addUtilities, matchUtilities, theme }) => {
|
||||||
addBase({
|
addBase({
|
||||||
@@ -68,11 +66,6 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
inherits: "true",
|
inherits: "true",
|
||||||
"initial-value": DEFAULT_BORDER_WIDTH,
|
"initial-value": DEFAULT_BORDER_WIDTH,
|
||||||
},
|
},
|
||||||
"@property --tw-dashed-border-align": {
|
|
||||||
syntax: "<length-percentage>",
|
|
||||||
inherits: "true",
|
|
||||||
"initial-value": DEFAULT_ALIGN,
|
|
||||||
},
|
|
||||||
"@property --tw-dashed-border-radius": {
|
"@property --tw-dashed-border-radius": {
|
||||||
syntax: "<length-percentage>",
|
syntax: "<length-percentage>",
|
||||||
inherits: "true",
|
inherits: "true",
|
||||||
@@ -87,7 +80,6 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
"--tw-dashed-border-offset": DEFAULT_OFFSET,
|
"--tw-dashed-border-offset": DEFAULT_OFFSET,
|
||||||
"--tw-dashed-border-width": DEFAULT_BORDER_WIDTH,
|
"--tw-dashed-border-width": DEFAULT_BORDER_WIDTH,
|
||||||
"--tw-dashed-border-color": DEFAULT_COLOR,
|
"--tw-dashed-border-color": DEFAULT_COLOR,
|
||||||
"--tw-dashed-border-align": DEFAULT_ALIGN,
|
|
||||||
"--tw-dashed-border-radius": DEFAULT_RADIUS,
|
"--tw-dashed-border-radius": DEFAULT_RADIUS,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
@@ -104,15 +96,17 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
fill: "none",
|
fill: "none",
|
||||||
stroke: "var(--tw-dashed-border-color)",
|
stroke: "var(--tw-dashed-border-color)",
|
||||||
strokeWidth: "var(--tw-dashed-border-width)",
|
strokeWidth: "var(--tw-dashed-border-width)",
|
||||||
strokeDasharray:
|
strokeDasharray: strokeDasharrayValue(
|
||||||
"var(--tw-dashed-border-length) var(--tw-dashed-border-gap)",
|
"var(--tw-dashed-border-length)",
|
||||||
|
"var(--tw-dashed-border-gap)",
|
||||||
|
),
|
||||||
strokeDashoffset: "var(--tw-dashed-border-offset)",
|
strokeDashoffset: "var(--tw-dashed-border-offset)",
|
||||||
shapeRendering: "geometricPrecision",
|
shapeRendering: "geometricPrecision",
|
||||||
vectorEffect: "non-scaling-stroke",
|
vectorEffect: "non-scaling-stroke",
|
||||||
x: dashedBorderAlign,
|
x: "calc(var(--tw-dashed-border-width) / 2)",
|
||||||
y: dashedBorderAlign,
|
y: "calc(var(--tw-dashed-border-width) / 2)",
|
||||||
width: `calc(100% - (${dashedBorderAlign} * 2))`,
|
width: "calc(100% - var(--tw-dashed-border-width))",
|
||||||
height: `calc(100% - (${dashedBorderAlign} * 2))`,
|
height: "calc(100% - var(--tw-dashed-border-width))",
|
||||||
rx: svgRadius,
|
rx: svgRadius,
|
||||||
},
|
},
|
||||||
".dashed-border-transition, .dashed-border.transition, .dashed-border.transition-all":
|
".dashed-border-transition, .dashed-border.transition, .dashed-border.transition-all":
|
||||||
@@ -139,6 +133,7 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const spacing = theme("spacing", {}) as Record<string, string>;
|
||||||
const borderWidths = theme("borderWidth", {
|
const borderWidths = theme("borderWidth", {
|
||||||
DEFAULT: DEFAULT_BORDER_WIDTH,
|
DEFAULT: DEFAULT_BORDER_WIDTH,
|
||||||
}) as Record<string, string>;
|
}) as Record<string, string>;
|
||||||
@@ -151,26 +146,37 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
{
|
{
|
||||||
"dashed-border-gap": (value: string) => ({
|
"dashed-border-gap": (value: string) => ({
|
||||||
"--tw-dashed-border-gap": value,
|
"--tw-dashed-border-gap": value,
|
||||||
|
"& .dashed-border-rect": {
|
||||||
|
strokeDasharray: strokeDasharrayValue(
|
||||||
|
"var(--tw-dashed-border-length)",
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
"dashed-border-length": (value: string) => ({
|
"dashed-border-length": (value: string) => ({
|
||||||
"--tw-dashed-border-length": value,
|
"--tw-dashed-border-length": value,
|
||||||
|
"& .dashed-border-rect": {
|
||||||
|
strokeDasharray: strokeDasharrayValue(
|
||||||
|
value,
|
||||||
|
"var(--tw-dashed-border-gap)",
|
||||||
|
),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
"dashed-border-offset": (value: string) => ({
|
"dashed-border-offset": (value: string) => ({
|
||||||
"--tw-dashed-border-offset": value,
|
"--tw-dashed-border-offset": value,
|
||||||
}),
|
"& .dashed-border-rect": {
|
||||||
"dashed-border-align": (value: string) => ({
|
strokeDashoffset: value,
|
||||||
"--tw-dashed-border-align": resolveAlignValue(value),
|
},
|
||||||
}),
|
}),
|
||||||
"dashed-border-radius": (value: string) => ({
|
"dashed-border-radius": (value: string) => ({
|
||||||
"--tw-dashed-border-radius": value,
|
"--tw-dashed-border-radius": value,
|
||||||
|
"& .dashed-border-rect": {
|
||||||
|
rx: `max(0px, calc(${value} - (var(--tw-dashed-border-width) / 2)))`,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
values: {
|
values: spacing,
|
||||||
inside: DEFAULT_ALIGN,
|
|
||||||
edge: "0px",
|
|
||||||
outside: "calc(var(--tw-dashed-border-width) / -2)",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,6 +184,14 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
{
|
{
|
||||||
"dashed-border-width": (value: string) => ({
|
"dashed-border-width": (value: string) => ({
|
||||||
"--tw-dashed-border-width": value,
|
"--tw-dashed-border-width": value,
|
||||||
|
"& .dashed-border-rect": {
|
||||||
|
strokeWidth: value,
|
||||||
|
x: `calc(${value} / 2)`,
|
||||||
|
y: `calc(${value} / 2)`,
|
||||||
|
width: `calc(100% - ${value})`,
|
||||||
|
height: `calc(100% - ${value})`,
|
||||||
|
rx: `max(0px, calc(var(--tw-dashed-border-radius) - (${value} / 2)))`,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -189,6 +203,9 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
|||||||
{
|
{
|
||||||
"dashed-border-color": (value: string) => ({
|
"dashed-border-color": (value: string) => ({
|
||||||
"--tw-dashed-border-color": value,
|
"--tw-dashed-border-color": value,
|
||||||
|
"& .dashed-border-rect": {
|
||||||
|
stroke: value,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user