Image
Use CImage when content needs one native image with an explicit text alternative, intrinsic dimensions, responsive candidates, and optional visual loading or error treatments. The browser still owns fetching, candidate selection, decoding, caching, CSP, CORS, and native image behavior.
Use the WAI alternative-text decision tree when the image's purpose is not obvious.
Start with alternative text and geometry
src, alt, width, and height are required. Use concise meaningful text for informative images. Use alt="" only when the image is truly decorative or repeats nearby content. The dimensions reserve the native aspect ratio before bytes arrive and do not force the final CSS size.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CImage
citry.register_library(citry_ui)
class BasicImage(Component):
class Kwargs:
pass
class Slots:
pass
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, Any]:
return {
"python_image": CImage(
src="/static/img/ui/image/orion-nebula-1280.jpg",
alt="Orion Nebula, captured from Northstar Ridge",
width=1280,
height=720,
loading="eager",
)
}
template = """
<section class="image-basic">
<article>
<h3>Template composition</h3>
<c-CImage
src="/static/img/ui/image/orion-nebula-1280.jpg"
alt="Orion Nebula, captured from Northstar Ridge"
c-width="1280"
c-height="720"
loading="eager"
/>
</article>
<article>
<h3>Python composition</h3>
{{ python_image }}
</article>
<p>
Both forms keep one native image semantic, required alternative text,
and a reserved 16:9 box before the files finish loading.
</p>
</section>
"""
css = """
:where(.image-basic) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-basic article) { display: grid; gap: 0.5rem; }
:where(.image-basic h3, .image-basic p) { margin: 0; }
:where(.image-basic p) { grid-column: 1 / -1; }
:where(.image-basic [data-citry-ui-part="image-root"]) {
inline-size: 100%;
}
"""
preview = BasicImage()
preview # noqa: B018
Choose the alternative for the image's purpose in context. An image-only link needs destination text. A complex chart needs an adjacent data equivalent. A caption does not replace alt.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImageAlternativeText(Component):
template = """
<section class="image-alt">
<article>
<h3>Informative</h3>
<c-CImage
src="/static/img/ui/image/orion-nebula-640.jpg"
alt="Pink and blue clouds in the Orion Nebula"
c-width="640"
c-height="360"
/>
<p>The alternative conveys the observation's useful content.</p>
</article>
<article>
<h3>Decorative</h3>
<c-CImage
src="/static/img/ui/image/orion-nebula-640.jpg"
c-alt="''"
c-width="640"
c-height="360"
/>
<p>The nearby heading already supplies all meaning, so alt is empty.</p>
</article>
<article>
<h3>Functional</h3>
<a href="#full-observation">
<c-CImage
src="/static/img/ui/image/horsehead-nebula-640.jpg"
alt="Open the full Horsehead Nebula observation"
c-width="640"
c-height="360"
/>
</a>
<p>The image is the link's only content, so alt names the destination.</p>
</article>
<article>
<h3>Complex observation</h3>
<figure>
<c-CImage
src="/static/img/ui/image/lunar-terminator-1280.jpg"
alt="Lunar terrain map; values follow in the table"
c-width="1280"
c-height="640"
/>
<figcaption>Exposure measurements along the lunar terminator.</figcaption>
</figure>
<table>
<caption>Equivalent exposure data</caption>
<thead><tr><th>Region</th><th>Seconds</th></tr></thead>
<tbody><tr><td>North rim</td><td>0.008</td></tr><tr><td>South basin</td><td>0.013</td></tr></tbody>
</table>
</article>
</section>
"""
css = """
:where(.image-alt) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-alt article) { display: grid; gap: 0.5rem; align-content: start; }
:where(.image-alt h3, .image-alt p, .image-alt figure) { margin: 0; }
:where(.image-alt [data-citry-ui-part="image-root"]) { inline-size: 100%; }
:where(.image-alt a:focus-visible) { outline: 3px solid Highlight; outline-offset: 3px; }
:where(.image-alt table) { border-collapse: collapse; font-size: 0.8rem; }
:where(.image-alt th, .image-alt td) { border: 1px solid GrayText; padding: 0.25rem; }
"""
preview = ImageAlternativeText()
preview # noqa: B018
Size and crop the rendered image
Use ordinary CSS to constrain rendered size. fit and position control the pixels inside that box. Native width and height remain intrinsic metadata. Public variables can override the aspect ratio, crop, position, radius, and state colors without relying on private classes.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImageFitAndGeometry(Component):
template = """
<section class="image-fit-grid">
<article>
<h3>Contain</h3>
<c-CImage
src="/static/img/ui/image/lunar-terminator-1280.jpg"
alt="Lunar craters along the terminator, contained"
c-width="1280"
c-height="640"
fit="contain"
class_="image-fit-grid__frame"
/>
</article>
<article>
<h3>Cover, left focus</h3>
<c-CImage
src="/static/img/ui/image/lunar-terminator-1280.jpg"
alt="Lunar craters along the terminator, closely cropped"
c-width="1280"
c-height="640"
fit="cover"
position="20% 50%"
class_="image-fit-grid__frame"
/>
</article>
<article>
<h3>Scale down</h3>
<c-CImage
src="/static/img/ui/image/lunar-terminator-1280.jpg"
alt="Lunar craters along the terminator, scaled down"
c-width="1280"
c-height="640"
fit="scale-down"
class_="image-fit-grid__frame image-fit-grid__square"
c-style="{'--cui-image-radius':'1.25rem'}"
/>
</article>
</section>
"""
css = """
:where(.image-fit-grid) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-fit-grid article) { display: grid; gap: 0.5rem; }
:where(.image-fit-grid h3) { margin: 0; }
:where(.image-fit-grid__frame) {
inline-size: 100%;
--cui-image-aspect-ratio: 4 / 3;
--cui-image-background: light-dark(#e7e5e4, #1c1917);
}
:where(.image-fit-grid__square) { --cui-image-aspect-ratio: 1 / 1; }
"""
preview = ImageFitAndGeometry()
preview # noqa: B018
Author responsive sources
Pass ordered frozen CImageSource records to emit a native <picture>. The records are data, not component declarations. Native first-match order matters. Width-descriptor srcset requires sizes, and arbitrary media text remains browser syntax that the application must validate.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CImage, CImageSource
citry.register_library(citry_ui)
class ResponsiveImageSources(Component):
class Kwargs:
pass
class Slots:
pass
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, Any]:
sources = (
CImageSource(
media="(max-width: 47.99rem)",
srcset="/static/img/ui/image/observatory-portrait-640.jpg",
width=640,
height=960,
),
CImageSource(
media="(min-width: 64rem)",
type="image/avif",
srcset="/static/img/ui/image/observatory-wide-1280.avif",
width=1280,
height=720,
),
)
return {
"sources": sources,
"responsive_srcset": (
"/static/img/ui/image/observatory-portrait-640.jpg 640w, "
"/static/img/ui/image/observatory-wide-1280.jpg 1280w"
),
"python_image": CImage(
src="/static/img/ui/image/observatory-wide-1280.jpg",
alt="Northstar Observatory beneath the Milky Way",
width=1280,
height=720,
srcset=(
"/static/img/ui/image/observatory-portrait-640.jpg 640w, "
"/static/img/ui/image/observatory-wide-1280.jpg 1280w"
),
sizes="(max-width: 48rem) 100vw, 48rem",
sources=sources,
),
}
template = """
<section
class="image-responsive"
x-data="{selected:'Waiting for native selection'}"
>
<p>
Resize across 48rem and 64rem. The browser selects the first matching
source, then the final image candidates.
</p>
<div class="image-responsive__pair">
<article>
<h3>Template-fed records</h3>
<c-CImage
src="/static/img/ui/image/observatory-wide-1280.jpg"
alt="Northstar Observatory beneath the Milky Way"
c-width="1280"
c-height="720"
c-srcset="responsive_srcset"
sizes="(max-width: 48rem) 100vw, 48rem"
c-sources="sources"
$c-props="{
onStatusChange:(detail)=>{
const value=detail.current_src || detail.src;
selected=value.split('/').pop().split('?')[0];
},
}"
/>
</article>
<article>
<h3>Python records</h3>
{{ python_image }}
</article>
</div>
<output x-text="`Selected local fixture: ${selected}`">
Selected local fixture: Waiting for native selection
</output>
</section>
"""
css = """
:where(.image-responsive) {
display: grid;
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-responsive__pair) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
}
:where(.image-responsive article) { display: grid; gap: 0.5rem; }
:where(.image-responsive h3, .image-responsive p) { margin: 0; }
:where(.image-responsive [data-citry-ui-part="image-root"]) { inline-size: 100%; }
"""
preview = ResponsiveImageSources()
preview # noqa: B018
Choose native loading and priority hints
Use loading="eager" and fetch_priority="high" only for a genuinely important above-fold image. Keep ordinary archive media at native lazy or auto priority. Image adds no observer, data-src indirection, preload, or custom decode gate, so the resource stays discoverable in server HTML.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImageLoadingPriority(Component):
template = """
<section class="image-priority">
<article>
<p class="image-priority__eyebrow">Above the fold</p>
<h3>Tonight's featured field</h3>
<c-CImage
src="/static/img/ui/image/orion-nebula-1280.jpg"
alt="Orion Nebula selected as tonight's featured field"
c-width="1280"
c-height="720"
loading="eager"
decoding="async"
fetch_priority="high"
/>
<p>Reserve high priority for the small number of likely LCP images.</p>
</article>
<button
type="button"
@click="document.querySelector('#image-priority-archive').scrollIntoView({behavior:'auto'})"
>Scroll to the archive image</button>
<div class="image-priority__spacer" aria-hidden="true"></div>
<article id="image-priority-archive">
<p class="image-priority__eyebrow">Below the fold</p>
<h3>Archive plate</h3>
<c-CImage
src="/static/img/ui/image/horsehead-nebula-1280.jpg"
alt="Horsehead Nebula archive plate"
c-width="1280"
c-height="720"
loading="lazy"
decoding="async"
fetch_priority="auto"
/>
<p>Lazy loading remains a native hint and needs no data-src indirection.</p>
</article>
</section>
"""
css = """
:where(.image-priority) {
display: grid;
gap: 1rem;
max-inline-size: 46rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-priority article) { display: grid; gap: 0.5rem; }
:where(.image-priority h3, .image-priority p) { margin: 0; }
:where(.image-priority__eyebrow) { color: GrayText; font-weight: 700; }
:where(.image-priority__spacer) { block-size: 70vh; border-block: 1px dashed GrayText; }
:where(.image-priority [data-citry-ui-part="image-root"]) { inline-size: 100%; }
"""
preview = ImageLoadingPriority()
preview # noqa: B018
Add visual loading and error treatments
The placeholder and fallback slots are inert visual layers. They never replace the native <img> or its alt. With JavaScript disabled, both custom layers stay hidden and the browser shows the native image or broken-image text fallback. Put meaningful error copy, retry controls, and live announcements outside Image.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImagePlaceholderAndError(Component):
template = """
<section
class="image-feedback"
x-data="{
source:'/static/img/ui/image/horsehead-nebula-1280.jpg?generation=1',
status:'waiting',
}"
>
<div class="image-feedback__controls">
<button
type="button"
@click="source='/static/img/ui/image/horsehead-nebula-1280.jpg?generation=2'"
>Load a valid generation</button>
<button
type="button"
@click="source='/static/img/ui/image/missing-observation.jpg?generation=3'"
>Load a broken generation</button>
<button
type="button"
@click="source='/static/img/ui/image/horsehead-nebula-640.jpg?generation=4'"
>Recover with a small image</button>
</div>
<div class="image-feedback__grid">
<article>
<h3>Visual placeholder and fallback</h3>
<c-CImage
src="/static/img/ui/image/horsehead-nebula-1280.jpg"
alt="Horsehead Nebula behind a dark dust cloud"
c-width="1280"
c-height="720"
$c-props="{
src:source,
onStatusChange:(detail)=>status=detail.status,
}"
>
<c-fill name="placeholder">
<div class="image-feedback__placeholder">
<c-CSkeleton height="100%" animation="wave" />
</div>
</c-fill>
<c-fill name="fallback">
<span class="image-feedback__fallback">Plate unavailable</span>
</c-fill>
</c-CImage>
<output x-text="`Normalized status: ${status}`">Normalized status: waiting</output>
</article>
<article>
<h3>Native broken-image fallback</h3>
<c-CImage
src="/static/img/ui/image/missing-native-observation.jpg"
alt="Missing Northstar archive observation"
c-width="640"
c-height="360"
/>
<p>No custom fallback is supplied, so native broken rendering and alt remain.</p>
</article>
</div>
</section>
"""
css = """
:where(.image-feedback) {
display: grid;
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-feedback__controls) { display: flex; flex-wrap: wrap; gap: 0.5rem; }
:where(.image-feedback__grid) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
}
:where(.image-feedback article) { display: grid; gap: 0.5rem; align-content: start; }
:where(.image-feedback h3, .image-feedback p) { margin: 0; }
:where(.image-feedback [data-citry-ui-part="image-root"]) { inline-size: 100%; }
:where(.image-feedback__placeholder, .image-feedback__fallback) {
display: grid;
place-items: center;
inline-size: 100%;
block-size: 100%;
}
:where(.image-feedback__fallback) { padding: 1rem; font-weight: 700; }
@media (forced-colors: active) {
:where(.image-feedback__fallback) { border: 1px solid CanvasText; }
}
@media print {
:where(.image-feedback__controls, .image-feedback output) { display: none; }
}
"""
preview = ImagePlaceholderAndError()
preview # noqa: B018
Observe and update a request
Client props can update the resource, semantics, dimensions, hints, fit, and callback. onStatusChange reports normalized loading, loaded, and error settlement plus current_src, natural_width, and natural_height. The current_src value snapshots the native currentSrc; treat it as potentially sensitive application data and redact it before logging.
Responsive settlement follows native event truth. A browser may select a broken <picture> candidate without emitting error; in that case Image keeps the last accepted status and callback ledger. It does not invent an observer or synthetic failure signal.
Native @load and @error listeners belong in img_attrs. Those events do not bubble to root attrs. Native listeners run in isolated expression scope, where $event, $store, $dispatch, and globals work but an ancestor's local x-data identifiers do not cross the component boundary. The component callback is the owner-local surface and also covers cached completion.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ReactiveImage(Component):
class Kwargs:
pass
class Slots:
pass
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, Any]:
return {
"image_attrs": {
"@load": "$dispatch('image-native-load')",
"@error": "$dispatch('image-native-error')",
"data-native-events": "bridged",
}
}
template = """
<section
class="image-reactive"
x-data="{
source:'/static/img/ui/image/horsehead-nebula-1280.jpg?frame=slow-red',
status:'waiting',
selected:'none',
callbacks:0,
nativeLoads:0,
nativeErrors:0,
redact:(value)=>value ? value.split('/').pop().split('?')[0] : 'none',
}"
@image-native-load="nativeLoads++"
@image-native-error="nativeErrors++"
>
<div class="image-reactive__controls">
<button
type="button"
@click="source='/static/img/ui/image/horsehead-nebula-1280.jpg?frame=slow-red'"
>Frame A</button>
<button
type="button"
@click="source='/static/img/ui/image/orion-nebula-1280.jpg?frame=fast-blue'"
>Frame B</button>
<button
type="button"
@click="source='/static/img/ui/image/missing-live-frame.jpg?frame=broken'"
>Broken</button>
<button
type="button"
@click="
source='/static/img/ui/image/horsehead-nebula-1280.jpg?frame=rapid-a';
queueMicrotask(()=>source='/static/img/ui/image/orion-nebula-640.jpg?frame=rapid-b');
"
>Rapid A then B</button>
</div>
<c-CImage
src="/static/img/ui/image/horsehead-nebula-1280.jpg"
alt="Live survey frame from Northstar Ridge"
c-width="1280"
c-height="720"
c-img_attrs="image_attrs"
$c-props="{
src:source,
onStatusChange:(detail)=>{
callbacks++;
status=detail.status;
selected=redact(detail.current_src || detail.src);
},
}"
>
<c-fill name="fallback">Survey frame unavailable</c-fill>
</c-CImage>
<output
x-text="
`Status ${status}; selected ${selected}; callbacks ${callbacks};
native load/error ${nativeLoads}/${nativeErrors}`
"
>Status waiting; selected none; callbacks 0; native load/error 0/0</output>
<p>
The output redacts paths to filenames. Native events use an img_attrs
$dispatch bridge; onStatusChange is the owner-local cached-race surface.
</p>
<div id="image-reactive-shadow-host" aria-label="Open ShadowRoot fixture"></div>
</section>
"""
css = """
:where(.image-reactive) {
display: grid;
gap: 1rem;
max-inline-size: 44rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-reactive__controls) { display: flex; flex-wrap: wrap; gap: 0.5rem; }
:where(.image-reactive p) { margin: 0; }
:where(.image-reactive [data-citry-ui-part="image-root"]) { inline-size: 100%; }
"""
preview = ReactiveImage()
preview # noqa: B018
Compose with native and Citry structure
Image is not a figure, Card, link, button, Skeleton, lightbox, or gallery. Wrap it in those structures when they own the semantic job. A neighboring Skeleton remains decorative, while the real image retains its alternative text.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImageComposition(Component):
template = """
<section class="image-composition">
<c-CCard tag="article" variant="outline">
<c-fill name="media">
<c-CImage
src="/static/img/ui/image/observatory-wide-1280.jpg"
alt="Northstar Observatory below the Milky Way"
c-width="1280"
c-height="720"
fit="cover"
/>
</c-fill>
<c-fill name="header"><h3>Northstar Ridge</h3></c-fill>
<c-fill name="default">A clear archive exposure from the western dome.</c-fill>
</c-CCard>
<article class="image-composition__delayed">
<h3>Loading layout reservation</h3>
<div class="image-composition__pair">
<c-CSkeleton height="7rem" animation="wave" />
<c-CImage
src="/static/img/ui/image/horsehead-nebula-1280.jpg?delayed=1"
alt="Horsehead Nebula exposure loading beside a decorative skeleton"
c-width="1280"
c-height="720"
loading="lazy"
/>
</div>
</article>
<figure>
<c-CImage
src="/static/img/ui/image/lunar-terminator-1280.jpg"
alt="Craters and mountain shadows along the lunar terminator"
c-width="1280"
c-height="640"
/>
<figcaption>Exposure notes: 0.008 seconds at the north rim.</figcaption>
</figure>
<a class="image-composition__link" href="#observation-42">
<c-CImage
src="/static/img/ui/image/orion-nebula-640.jpg"
alt="Open observation 42, Orion Nebula"
c-width="640"
c-height="360"
/>
</a>
</section>
"""
css = """
:where(.image-composition) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-composition h3, .image-composition figure) { margin: 0; }
:where(.image-composition [data-citry-ui-part="image-root"]) { inline-size: 100%; }
:where(.image-composition__pair) { display: grid; gap: 0.5rem; }
:where(.image-composition__link) { align-self: start; border-radius: 0.75rem; }
:where(.image-composition__link:focus-visible) { outline: 3px solid Highlight; outline-offset: 3px; }
"""
preview = ImageComposition()
preview # noqa: B018
Keep delivery policy explicit
cross_origin and referrer_policy select native request modes; they do not grant canvas access or repair server headers. img-src CSP remains authoritative. Relative, HTTP, HTTPS, data, blob, raster, and SVG image URLs are consumer-owned resource references, not sanitized or fetched by Citry. Active javascript: and vbscript: schemes are rejected. Blob lifetime, data-URL size, metadata privacy, origin policy, and remote tracking remain application responsibilities.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImageDeliveryAndSecurity(Component):
template = """
<section class="image-delivery">
<article>
<h3>Same-origin archive</h3>
<c-CImage
src="/static/img/ui/image/orion-nebula-640.jpg"
alt="Same-origin Orion Nebula plate"
c-width="640"
c-height="360"
referrer_policy="strict-origin-when-cross-origin"
/>
<p>Native request policy applies before src. Citry does not proxy bytes.</p>
</article>
<article>
<h3>Credential-free CORS mode</h3>
<c-CImage
src="https://cross-origin.citry.test/northstar/observatory-1280.jpg"
alt="Observatory plate requested in anonymous CORS mode"
c-width="1280"
c-height="720"
cross_origin="anonymous"
referrer_policy="no-referrer"
>
<c-fill name="fallback">Cross-origin plate unavailable in this preview</c-fill>
</c-CImage>
<p>The response still needs matching server headers for CORS use.</p>
</article>
<article>
<h3>CSP and unavailable resources</h3>
<c-CImage
src="https://blocked-images.citry.test/northstar/blocked.jpg"
alt="Archive plate unavailable under the current image policy"
c-width="640"
c-height="360"
>
<c-fill name="fallback">Blocked or unavailable plate</c-fill>
</c-CImage>
<p>Browser CSP remains authoritative and failures settle as image error.</p>
</article>
<aside>
<h3>Application trust boundary</h3>
<ul>
<li>Relative, HTTPS, data, blob, raster, and SVG URLs remain consumer-owned.</li>
<li>Do not log signed URLs, currentSrc, query strings, or response bodies.</li>
<li>Blob lifetime, data-URL size, metadata privacy, and remote tracking are application policy.</li>
</ul>
<output>Safe request summary: request modes configured; URLs omitted</output>
</aside>
</section>
"""
css = """
:where(.image-delivery) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-delivery article, .image-delivery aside) { display: grid; gap: 0.5rem; align-content: start; }
:where(.image-delivery h3, .image-delivery p, .image-delivery ul) { margin: 0; }
:where(.image-delivery [data-citry-ui-part="image-root"]) { inline-size: 100%; }
"""
preview = ImageDeliveryAndSecurity()
preview # noqa: B018
Understand lifecycle and fallback
Equal retained-node server morphs preserve the active request and status. Changing request fields starts one new generation. Replacing the native image, removing the owner, invalid structure, a closed ShadowRoot, or cross-document adoption requires fresh ownership. Late work from an old generation cannot notify a replacement owner.
Show code
from __future__ import annotations
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ImageLifecycle(Component):
class Kwargs:
step: int = 0
class Slots:
pass
class Events:
def retain(self) -> ImageLifecycle:
return ImageLifecycle(step=1)
def change_resource(self) -> ImageLifecycle:
return ImageLifecycle(step=2)
def replace(self) -> ImageLifecycle:
return ImageLifecycle(step=3)
def remove(self) -> ImageLifecycle:
return ImageLifecycle(step=4)
def restore(self) -> ImageLifecycle:
return ImageLifecycle(step=5)
def remove_again(self) -> ImageLifecycle:
return ImageLifecycle(step=6)
def restore_again(self) -> ImageLifecycle:
return ImageLifecycle(step=7)
def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, object]: # noqa: ARG002
source = "/static/img/ui/image/horsehead-nebula-1280.jpg?plate=baseline"
if kwargs.step >= 2:
source = "/static/img/ui/image/orion-nebula-1280.jpg?plate=changed"
image_key = "image-lifecycle-retained" if kwargs.step < 3 else f"image-lifecycle-{kwargs.step}"
return {
"image_key": image_key,
"include_image": kwargs.step not in {4, 6},
"source": source,
"step": kwargs.step,
}
template = """
<section
class="image-lifecycle"
x-data="{status:'waiting',selected:'none'}"
>
<div class="image-lifecycle__controls">
<button type="button" @c-click="retain">Retain equal server output</button>
<button type="button" @c-click="change_resource">Change the resource</button>
<button type="button" @c-click="replace">Replace native ownership</button>
<button type="button" @c-click="remove">Remove the Image</button>
<button type="button" @c-click="restore">Restore a fresh Image</button>
<button type="button" @c-click="remove_again">Remove it again</button>
<button type="button" @c-click="restore_again">Restore it again</button>
<button
type="button"
@click="
const root=$root.querySelector('#image-lifecycle-target');
if (root) root.setAttribute('data-status','forged');
"
>Test hostile status fail-closed</button>
<button
type="button"
@click="
const root=$root.querySelector('#image-lifecycle-target');
if (root) root.after(root.cloneNode(true));
"
>Insert an unowned clone</button>
</div>
<p>Signed server step: <output data-image-lifecycle-step>{{ step }}</output></p>
<c-if cond="include_image">
<c-CImage
#c-key="image_key"
c-src="source"
alt="Nightly Northstar calibration plate"
c-width="1280"
c-height="720"
c-attrs="{
'id':'image-lifecycle-target',
'data-quality-states':
'lifecycle retained-root replacement-root morph-target removal restore '
+ 'cleanup owner-token shadow-root clone hostile-fail-closed',
}"
$c-props="{
onStatusChange:(detail)=>{
status=detail.status;
selected=(detail.current_src || detail.src).split('/').pop().split('?')[0];
},
}"
>
<c-fill name="placeholder">Loading calibration plate</c-fill>
<c-fill name="fallback">Calibration plate unavailable</c-fill>
</c-CImage>
</c-if>
<output x-text="`Status ${status}; selected ${selected}`">
Status waiting; selected none
</output>
<div id="image-lifecycle-shadow-host" aria-label="Open ShadowRoot move fixture"></div>
</section>
"""
css = """
:where(.image-lifecycle) {
display: grid;
gap: 1rem;
max-inline-size: 44rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.image-lifecycle__controls) { display: flex; flex-wrap: wrap; gap: 0.5rem; }
:where(.image-lifecycle p) { margin: 0; }
:where(.image-lifecycle [data-citry-ui-part="image-root"]) { inline-size: 100%; }
"""
preview = ImageLifecycle()
preview # noqa: B018
Without JavaScript, the server-rendered native image, ordered responsive sources, required alt, dimensions, loading hints, CORS mode, and referrer policy remain useful. Custom placeholder and fallback slots stay hidden so they cannot cover the native result.
Image is not form-associated and adds no keyboard, focus, overlay, gesture, retry, upload, editing, canvas, image-map, or lightbox behavior. Important print images should use eager loading because printing does not guarantee a lazy request will start before pagination.
API reference
Inputs
CImage server inputs
Server inputs are passed in a template through <c-CImage ... /> or in Python through CImage(...).
| Input | Type | Default | Effect |
|---|---|---|---|
src | str | required | Sets the required nonempty escaped fallback image URL. |
alt | str | required | Sets the required native text alternative; an exact empty string is an intentional decorative choice. |
width | positive int | required | Sets native intrinsic width and reserves geometry; bool is rejected. |
height | positive int | required | Sets native intrinsic height and reserves geometry; bool is rejected. |
srcset | str | None | None | Adds native responsive candidates; width descriptors require sizes. |
sizes | str | None | None | Adds native image source sizes; auto sizes require lazy loading. |
sources | Sequence[CImageSource] | () | Snapshots up to 32 ordered source records and emits picture only when nonempty. |
loading | "eager" | "lazy" (CImageLoading) | "eager" | Selects the native loading hint. |
decoding | "auto" | "sync" | "async" (CImageDecoding) | "auto" | Selects the native decoding hint. |
fetch_priority | "auto" | "high" | "low" (CImageFetchPriority) | "auto" | Selects native fetchpriority; the application owns scarcity policy. |
cross_origin | CImageCrossOrigin | None (CImageCrossOrigin) | None | Selects anonymous or credentialed native CORS mode. |
referrer_policy | CImageReferrerPolicy | None (CImageReferrerPolicy) | None | Selects the native image request referrer policy. |
fit | "contain" | "cover" | "fill" | "none" | "scale-down" (CImageFit) | "contain" | Sets effective object fit and the root mirror. |
position | str | "50% 50%" | Sets validated object-position text; the browser owns final CSS grammar. |
draggable | bool | False | Sets the exact native draggable reflection. |
onStatusChange | browser callback | None | None | Sets the owner-local normalized status callback. |
class_ | CClassValue | None (CClassValue) | None | Adds root classes and merges them with attrs. |
style | CStyleValue | None (CStyleValue) | None | Adds root styles and merges them with attrs before owned style fallbacks. |
attrs | Mapping[str, object] | None | None | Adds copied allowed neutral-root attributes and isolated-scope native listeners. |
img_attrs | Mapping[str, object] | None | None | Adds copied allowed native image attributes and load or error listeners without replacing owned resources, semantics, dimensions, or policy. |
CImage client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CImage />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
src | str | Uses the immutable server baseline; null has the same effect. | Replaces the fallback URL and begins a new request generation. |
alt | str | Uses the immutable server baseline; null has the same effect. | Updates the native alternative without creating a second semantic owner. |
width | positive integer | Uses the immutable server baseline; null has the same effect. | Updates native intrinsic width. |
height | positive integer | Uses the immutable server baseline; null has the same effect. | Updates native intrinsic height. |
srcset | string | null | Uses the immutable server baseline. | Updates final-image candidates and begins a new request generation when effective selection metadata changes. |
sizes | string | null | Uses the immutable server baseline. | Updates final-image sizes and request selection. |
loading | "eager" | "lazy" (CImageLoading) | Uses the immutable server baseline; null has the same effect. | Updates the native loading hint without fabricating settlement. |
decoding | "auto" | "sync" | "async" (CImageDecoding) | Uses the immutable server baseline; null has the same effect. | Updates the native decoding hint. |
fetchPriority | "auto" | "high" | "low" (CImageFetchPriority) | Uses the immutable server baseline; null has the same effect. | Updates native fetch priority. |
crossOrigin | CImageCrossOrigin | null (CImageCrossOrigin) | Uses the immutable server baseline. | Updates or clears native CORS mode before a resource write. |
referrerPolicy | CImageReferrerPolicy | null (CImageReferrerPolicy) | Uses the immutable server baseline. | Updates or clears native referrer policy before a resource write. |
fit | "contain" | "cover" | "fill" | "none" | "scale-down" (CImageFit) | Uses the immutable server baseline; null has the same effect. | Updates object fit and the root reflection. |
position | string | Uses the immutable server baseline; null has the same effect. | Updates validated object-position text. |
draggable | boolean | Uses the immutable server baseline; null has the same effect. | Updates native draggable. |
onStatusChange | function | Uses the server callback. | Replaces the owner-local normalized status callback; null clears it. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CImage slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
placeholder | no | none | No custom loading layer; native pending rendering remains visible. |
fallback | no | none | No custom error layer; native broken-image rendering and alt remain visible. |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CImage events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onStatusChange | (detail: CImageStatusChangeDetail) => void (CImageStatusChangeDetail) | Initial loading ownership, a new accepted request generation, cached completion, or matching trusted native success or error settlement for a selected currentSrc change. | {status, src, current_src, natural_width, natural_height} (CImageStatusChangeDetail) | Runs after native attributes and public mirrors synchronize; it is not cancelable and stale generations cannot notify. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CImage CSS variables
Apply these variables to CImage or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-image-aspect-ratio | positive CSS ratio or auto | Overrides the rendered native image ratio without changing intrinsic metadata. | auto |
--cui-image-fit | object-fit value | Overrides the effective fit input for pixels inside the media box. | Effective fit input |
--cui-image-position | object-position value | Overrides effective pixel position inside the media box. | Effective position input |
--cui-image-radius | length or percentage | Media box corner radius. | var(--cui-radius-md) |
--cui-image-background | color or image | Loading and native contain-area background. | transparent |
--cui-image-fallback-color | color | Visual fallback foreground. | var(--cui-color-muted-fg) |
--cui-image-fallback-background | color or image | Visual fallback background. | var(--cui-color-muted-bg) |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CImage attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
data-status | Image root | "loading" | "loaded" | "error" (CImageStatus) | Mirrors the last accepted normalized status after readiness; a silent browser candidate change does not invent settlement. |
data-fit | Image root | "contain" | "cover" | "fill" | "none" | "scale-down" (CImageFit) | Mirrors effective configured fit before a public CSS variable override. |
data-has-placeholder | Image root | present | absent | Reports whether placeholder slot content exists. |
data-has-fallback | Image root | present | absent | Reports whether fallback slot content exists. |
data-citry-image-initialized | Image root | present | absent | Marks a live settled runtime owner; the copyable attribute alone is not authority. |
CImage attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
alt | Native img | string | Required native alternative text and sole image semantic name. |
width | Native img or source | positive integer | Native intrinsic width metadata. |
height | Native img or source | positive integer | Native intrinsic height metadata. |
src | Native img | nonempty URL string | Required fallback request URL. |
srcset | Native img or source | native candidate string | Browser-owned responsive candidate set. |
sizes | Native img or source | native sizes string | Browser-owned rendered-size hint. |
media | Native source | media query string | Browser-owned art-direction discriminator. |
type | Native source | image MIME essence | Browser-owned format discriminator. |
loading | Native img | "eager" | "lazy" (CImageLoading) | Native request scheduling hint. |
decoding | Native img | "auto" | "sync" | "async" (CImageDecoding) | Native decode scheduling hint. |
fetchpriority | Native img | "auto" | "high" | "low" (CImageFetchPriority) | Native relative fetch-priority hint. |
crossorigin | Native img | "anonymous" | "use-credentials" | absent (CImageCrossOrigin) | Native CORS request mode. |
referrerpolicy | Native img | CImageReferrerPolicy | absent (CImageReferrerPolicy) | Native request referrer policy. |
draggable | Native img | "true" | "false" | Exact native drag reflection. |
hidden | Placeholder or fallback wrapper | present in server output | Keeps custom visual layers from obscuring native no-JavaScript output. |
aria-hidden | Placeholder or fallback wrapper | "true" | Keeps visual slot copy out of the accessibility tree. |
inert | Placeholder or fallback wrapper | present | Prevents visual slot descendants from becoming interaction surfaces. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CImage selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="image-root"] | Neutral root span | Lifecycle owner and class_, style, and attrs destination. |
[data-citry-ui-part="picture"] | Native picture | Ordered responsive source-selection context present only when sources is nonempty. |
[data-citry-ui-part="image"] | Sole native img | Semantic and request owner plus img_attrs destination. |
[data-citry-ui-part="placeholder"] | Inert visual span | Optional loading-only visual layer. |
[data-citry-ui-part="fallback"] | Inert visual span | Optional error-only visual layer. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] |
CImageFit | Literal["contain", "cover", "fill", "none", "scale-down"] |
CImageLoading | Literal["eager", "lazy"] |
CImageDecoding | Literal["auto", "sync", "async"] |
CImageFetchPriority | Literal["auto", "high", "low"] |
CImageCrossOrigin | Literal["anonymous", "use-credentials"] |
CImageReferrerPolicy | Literal["no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin", "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url"] |
CImageStatus | Literal["loading", "loaded", "error"] |
CImageSource
| Field | Type | Default | Meaning |
|---|---|---|---|
srcset | str | - | Required native candidate string for one ordered source. |
media | str | None | - | Optional native media discriminator. |
type | str | None | - | Optional image MIME essence discriminator. |
sizes | str | None | - | Optional native source sizes; required for width descriptors. |
width | positive int | None | - | Optional native source width paired with height. |
height | positive int | None | - | Optional native source height paired with width. |
CImageStatusChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
status | CImageStatus (CImageStatus) | - | Accepted normalized loading, loaded, or error state at callback time. |
src | string | - | Current authored fallback URL snapshot. |
current_src | string | - | Browser-selected absolute URL snapshot, which may be sensitive. |
natural_width | integer | - | Native selected resource width, including zero for pending, error, or valid zero-size resources. |
natural_height | integer | - | Native selected resource height, including zero for pending, error, or valid zero-size resources. |
Translation keys
-