Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"bumpp": "^10.1.0",
"chalk": "^4.1.2",
"commit-and-tag-version": "^12.5.0",
"country-flag-icons": "^1.6.20",
"dotenv": "^16.4.7",
"eslint": "^9.39.4",
"eslint-config-prettier": "^9.1.2",
Expand Down
128 changes: 128 additions & 0 deletions packages/components/src/components/Flag/Flag.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
Meta,
Story,
Props,
Status,
} from '../../../../../.storybook/components';

import * as Stories from './Flag.stories';

<Meta of={Stories} />

# Flag

<Status variant="experimental" />

Flag shows a small country flag inside a product UI (inline with text, in lists, options, selects).

It is a thin presentational wrapper: it decorates a flag graphic you provide and does **not** ship
or resolve any flag data. Flag graphics are expected to come from
[`country-flag-icons`](https://www.npmjs.com/package/country-flag-icons) (full ISO 3166-1, redrawn
for small sizes) or any custom source.

## Import

```tsx
import { Flag } from '@koobiq/react-components';
```

## Usage

Provide the flag graphic as `children` — an inline `<svg>` is the recommended path (no extra
requests, no base64), but an `<img>` also works. `country-flag-icons` exposes ready-made React flag
components:

```tsx
import { DE } from 'country-flag-icons/react/3x2';

<Flag label="Germany">
<DE />
</Flag>;
```

<Story of={Stories.Base} />

## Props

<Props of={Stories.Base} />

## Shape

Use the `shape` prop: `rectangle` (default), `square`, or `circle`. `square` and `circle` expect a
1:1 source; `circle` additionally clips the graphic to a circle.

<Story of={Stories.Shape} />

## Shadow

A thin inset hairline separates the flag from the background. It is on by default (`shadow="inset"`)
and adapts to the theme (dark hairline in the light theme, lighter in the dark theme). Turn it off
with `shadow="none"`.

<Story of={Stories.Shadow} />

## Sizing

The flag follows text size — its default height is `1em`. Control it with `font-size`, `width`, or
`height` in CSS. There are no size presets.

<Story of={Stories.Sizes} />

## Aspect ratio

The default box ratio is 3:2. Other ratios (e.g. 4:3) and non-country sources are supported **without
a prop** by overriding the `--kbq-flag-aspect-ratio` CSS variable.

```tsx
<Flag style={{ '--kbq-flag-aspect-ratio': '4 / 3' }}>{/* … */}</Flag>
```

<Story of={Stories.AspectRatio} />

## Missing flag

The library ships no flag data, so guard the country code (e.g. with `hasFlag` from
`country-flag-icons`) and render a neutral placeholder with the `empty` prop when a flag is missing —
the layout never breaks and no broken graphic is shown.

<Story of={Stories.Fallback} />

## Accessibility

A flag must never be a silent, meaning-bearing graphic.

- When the flag carries meaning and has **no adjacent text**, pass a `label`. The host exposes
`role="img"` with the accessible name.
- When adjacent visible text already names the country, mark the flag `decorative`. It is hidden from
assistive technology (`aria-hidden`) to avoid duplication.

<Story of={Stories.Accessibility} />

## Flag is not a language

A flag represents a country, not a language. Use a globe icon (not a flag) for language selection.

<Story of={Stories.NotForLanguage} />

## Custom look

Stylized, decorative treatments (rounded corners, drop shadow, "folds" gradient) are your own CSS
plus the exposed variables — not a library prop. Round the corners with `--kbq-flag-border-radius`
and add a shadow/gradient in your own styles.

<Story of={Stories.Custom} />

## Flags in a Select

<Story of={Stories.FlagsInSelect} />

## CSS variables

Everything visual that isn't a discrete state is an overridable CSS variable:

| Variable | Default | Purpose |
| ----------------------------- | --------------------------------------- | ----------------------------------------------- |
| `--kbq-flag-aspect-ratio` | `3 / 2` | Box ratio. `square`/`circle` set it to `1 / 1`. |
| `--kbq-flag-border-radius` | `0` | Corner radius (rounded / stylized look). |
| `--kbq-flag-shadow-color` | `var(--kbq-line-contrast-fade)` | Inset hairline color; theme-adaptive. |
| `--kbq-flag-empty-background` | `var(--kbq-states-background-disabled)` | Placeholder fill. |
46 changes: 46 additions & 0 deletions packages/components/src/components/Flag/Flag.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.base {
/* Public, overridable knobs (see the component docs). */
--kbq-flag-aspect-ratio: 3 / 2;
--kbq-flag-border-radius: 0;
--kbq-flag-shadow-color: var(--kbq-line-contrast-fade);
--kbq-flag-empty-background: var(--kbq-states-background-disabled);

display: inline-block;
position: relative;
overflow: hidden;
vertical-align: middle;
block-size: 1em;
aspect-ratio: var(--kbq-flag-aspect-ratio);
border-radius: var(--kbq-flag-border-radius);
}

/* The projected flag graphic fills the box (descendant selector supports wrapped graphics). */
.base :is(svg, img) {
display: block;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}

/* Inset hairline separating the flag from the background. */
.base[data-shadow='inset']::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 0 1px var(--kbq-flag-shadow-color);
pointer-events: none;
}

.base[data-shape='square'],
.base[data-shape='circle'] {
--kbq-flag-aspect-ratio: 1 / 1;
}

.base[data-shape='circle'] {
border-radius: 50%;
}

.base[data-empty] {
background-color: var(--kbq-flag-empty-background);
}
21 changes: 21 additions & 0 deletions packages/components/src/components/Flag/Flag.stories.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* Gradient imitating folds, blended over the flag (stylized docs example). */
.stylized::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
background: linear-gradient(
240.64deg,
rgb(255 255 255 / 30%) 0%,
rgb(0 0 0 / 27%) 26.27%,
rgb(255 255 255 / 26%) 37%,
rgb(0 0 0 / 55%) 48.7%,
rgb(0 0 0 / 24%) 59.44%,
rgb(255 255 255 / 30%) 73.64%,
rgb(39 39 39 / 22%) 90.15%,
rgb(0 0 0 / 20%) 100%
);
mix-blend-mode: overlay;
pointer-events: none;
}
Loading
Loading